【Unity】UGUI系列教程——OSU!Battle!
前言
有些認真的讀者反映OSU!和本教程的表現方式是有差異的。因為我做的遊戲來講解功能的主要目的是讓給多想學習Unity製作遊戲的讀者更有興趣來學習教程,而不是枯燥的背組件的使用方式和參數作用。我更想讓UGUI偏向實用方向講解,因此每次寫教程之前我都是需要自己花時間想下怎麼用最少的知識點完成我們想要效果。本期根據上期的預告,將會對OSU!的Battle部分進行簡單實現和講解。能想學習Unity的UGUI功能的讀者也能夠有所受用,這是我的初衷,而若是能讓想製作音樂遊戲的讀者能有所啟發,那真是再好不過了。
預覽效果
這裡只實現點擊和拖拽,轉動圓盤的效果會在之後的教程中介紹實現方法。這裡由於時間原因沒有擱置未實現。
遊戲需要的知識
序列幀動畫:
序列幀動畫原理是和我們看電視上動畫的原理一樣,當圖片不停按順序切換,利用視覺殘留效果,會顯示出運動的感覺。因此我們只用對Image圖片做固定時間切換就可以實現,腳本方法不闡述了,這裡說一個Unity的簡單實現方法
創建一個Animator掛載到需要顯示序列幀圖片的地方
直接將序列幀圖片拖到Animation窗口的動畫Clip中
記得動畫Clip資源設置成循環播放。
九宮格圖片:
九宮格圖片在UI中廣泛運用,為了優化資源大小,我們做中間過渡簡單的背景圖和長條UI的時候並不會實際畫遊戲中需要的圖片大小。而是利用設置九宮格圖片拉伸得到。
選擇你要設置的九宮格圖片
點擊Spite Editor按鈕,出現九宮格編輯界面
我們如果這樣設置九宮格,那麼在中間正方形區域將會被拉伸,而外圍區域的圖片將會保持形狀處理。
這裡我們需要將一個圓形拉伸成膠囊形狀的UI,於是這樣設置,只給中間留2像素就夠了
對Image組件的Image Type選擇Sliced就後,調節Width就好了
小知識點:
對你想統一修改某掛點下所有UI物體的透明度,掛載Canvas Group組件就好了。
搭建Note界面
點擊圈的界面很簡單,只需要一個可以點擊按鈕Btn_Judge,一個提示作用的白色圈Img_AimCircle。
點擊後根據點擊準確度打開得分提示GoodState、PerfectState、FailState就好了。
滑動條需要增加跟隨小球移動的操作,需要增加移動位置開始點Tran_StartPoint,移動結束位置點Tran_EndPoint
因為得分提示UI和黃色的範圍提示UI要跟著小球一起移動,我們便創建一個移動物體掛點Tran_MovePos,將這幾個需要一起移動的UI放在下面。
邏輯功能的實現:
針對很多新手程序員來說,寫腳本最難的在於實現功能的模塊化處理。腳本與腳本之間的重複代碼過多,耦合過多,這樣很不利於維護和處理業務邏輯。
OSU!的點擊圈和滑動條效果其實有很多相似的地方,比如他們都需要開始的延遲時間,這個時候其實是讓玩家做好下一步的準備,他們都有一個判定時間,在這個判定時間內點擊到判定區域開始計算得分,而滑動條只多了一步滑動操作。都有結算顯示,根據操作來打開不同的得分提示,最後統一的刪除清理。
我們先將統一部分的功能實現,創建一個NoteLogic腳本來做為公共的邏輯腳本處理。
NoteLogic的主要函數:
時間變化,在特定的狀態計時,達到目標值後進行狀態切換
private void Update()n {n switch (curState)n {n case eState.Delay:n {n curTime += Time.deltaTime;n if (curTime > delayTime)n {n curTime = 0;n SetCurState(eState.Wait);n }n }n break;n case eState.Operation:n case eState.Wait:n {n curTime += Time.deltaTime;n if (curTime > startTime+judgeTime+0.3f)n {n curTime = 0;n SetCurState(eState.Over);n }n }n break;n case eState.Over:n {n curTime += Time.deltaTime;n if (curTime > desTime)n {n SetCurState(eState.None);n Destroy(gameObject);n }n }n break;n }n }n
設置當前狀態函數,通關枚舉類型變化來實現狀態切換
public void SetCurState(eState rState)n {n curState = rState;n n switch (curState)n {n //界面在延遲等待的階段處理漸入效果n case eState.Delay:n curTime = 0;n var canvasGroup = gameObject.GetComponent<CanvasGroup>();n canvasGroup.alpha = 0;n gameObject.GetComponent<CanvasGroup>().DOFade(1, delayTime);n break;n //等待判定階段,將圓圈圖片做縮放動畫n case eState.Wait:n if (curType != LevelNoteData.eNoteType.Disk)n {n circleTipObj.gameObject.SetActive(true);n circleTipObj.transform.DOScale(1, startTime).OnComplete(() => { circleTipObj.gameObject.SetActive(false); });n }n break;n //操作階段調用虛方法,讓繼承的類來自定義該狀態功能n case eState.Operation:n if (curType != LevelNoteData.eNoteType.Disk)n {n circleTipObj.gameObject.SetActive(false);n }n OnJudgetOperation();n break;n //結束狀態打開得分提示n case eState.Over:n curTime = 0;n ShowScore();n break;n }n }n
虛函數,繼承的子類來實現這裡的功能
/// <summary>n /// 做判定操作使用的虛函數n /// </summary>n public virtual void OnJudgetOperation()n {nn }n
打開的得分提示UI,這裡設置角度的原因是部分Note會旋轉位置,而打開的提示UI不能隨著父物體旋轉而旋轉
public void ShowScore()n {n if (mainShowObj != null)n {n mainShowObj.gameObject.SetActive(false);n }nn switch (curScore)n {n case eScore.Good:n statePointArr[0].gameObject.SetActive(true);n statePointArr[0].transform.eulerAngles = Vector3.zero;n break;n case eScore.Perfect:n statePointArr[1].gameObject.SetActive(true);n statePointArr[1].transform.eulerAngles = Vector3.zero;n break;n case eScore.Fail:n statePointArr[2].gameObject.SetActive(true);n statePointArr[2].transform.eulerAngles = Vector3.zero;n break;n }n }n
HitCircle和Slider實現
創建HitCircle和Slider腳本並繼承NoteLogic類,通過重載OnJudgetOperation函數來做各自獨立的功能處理。
HitCircle腳本只用點擊後判斷出得分,改變當前的狀態為Over就結束了。
using System.Collections;nusing System.Collections.Generic;nusing UnityEngine;nusing UnityEngine.UI;nusing DG.Tweening;nnpublic class HitCircle : NoteLogicn{nn public override void OnJudgetOperation()n {n //這裡簡單的通過時間的差值來判斷得分n float dValue = Mathf.Abs(curTime - startTime);nn if (dValue < startTime * 0.35f)n {n curScore = eScore.Perfect;n }n else if (dValue < startTime * 0.7f)n {n curScore = eScore.Good;n }n elsen {n curScore = eScore.Fail;n }nn SetCurState(eState.Over);n }n n}n
而Slider腳本需要額外擴展掛點,因此可以直接在子類聲明變數。
[Header("移動掛點")]Unity的一個屬性,它能再Inspector界面的變數上顯示你添加的字元串。
我們直接在功能處理中讓移動點移動到目標位置就好了,當移動到目標點後判定得分,改變狀態為結束。
using System.Collections;nusing System.Collections.Generic;nusing UnityEngine;nusing UnityEngine.UI;nusing DG.Tweening;nnpublic class Slider : NoteLogicn{n [Header("移動掛點")]n public GameObject movePoint;n [Header("移動開始掛點")]n public GameObject moveStartPoint;n [Header("移動目標掛點")]n public GameObject moveTargetPoint;n [Header("滾動球物體")]n public GameObject rollBar;n [Header("移動提示圈")]n public RectTransform moveTipObj;nn public override void OnJudgetOperation()n {n movePoint.transform.position = moveStartPoint.transform.position;n rollBar.gameObject.SetActive(true);nn Tweener tween = null;n //Dotween可以很方便各種鏈接Update功能和Complete功能n tween = movePoint.transform.DOMove(moveTargetPoint.transform.position, judgeTime).OnComplete(() =>n {n //當球移動到目標位置後調用nn float dValue = Mathf.Abs(startTime+judgeTime -curTime);nn //和點擊圓圈的判定一樣,其實也可以做不同的處理n if (dValue < startTime * 0.35f)n {n curScore = eScore.Perfect;n }n else if (dValue < startTime * 0.7f)n {n curScore = eScore.Good;n }n elsen {n curScore = eScore.Fail;n }nn rollBar.gameObject.SetActive(false);n SetCurState(eState.Over);nn }).OnUpdate(() =>n {n //在每一幀移動中判斷滑鼠是否超出跟隨小球移動的黃色圓圈n if (Vector3.Distance(Input.mousePosition, moveTipObj.position) > moveTipObj.sizeDelta.x * 0.5f)n {n if (tween != null)n {n tween.Kill();n }n curScore = eScore.Fail;n rollBar.gameObject.SetActive(false);n SetCurState(eState.Over);n }n });n }n n}n
可以繼續鏈加Update方法來每幀判斷處理滑鼠是否移出判定範圍了
將腳本掛在顯示的界面物體上,看下效果
HitCircle:
Slider:
關卡配置
通用UI組件
之前我們說過了UI做成預提體的方便之處,這裡我們要將這兩個非界面類型的HitCircle和Slider物體也做成預製體,這樣通用組件化可以很方便我們動態搭建出界面效果。
將HitCircle和Slider保存成預製體
然後我們只用按照音樂播放的時間,在特定時刻動態讀取創建出Com_HitCircle和Com_Slider,設置好位置和角度,再給腳本傳入延遲、等待、判定、銷毀時間就可以實現一個簡單音樂戰鬥了。
配置我們的關卡數據
OSU!的Battle流程其實就是一個根據時間排序好的Note列表,我們先創建一個LevelNoteData類來儲存數據。
儲存關卡信息就先臨時放在Page_GamePlay界面的UI腳本上吧,其實正式數據是決不能這樣做的,這裡是想快速實現效果避免創建過多的類導致說明混亂。
我們先規定levelNoteList存儲的信息都是按照時間從小到大排序的,每幀判定當前時間是否滿足最近的快要創建的Note的創建時間,若滿足就判斷最近的Note類型,創建出對應的UI通用組件,並對腳本賦值。
創建完成後就可以將這個Note移出關卡信息列表了。
簡單介紹關卡編輯
創建一個Editor腳本添加[CustomEditor(typeof(Page_GamePlay))]屬性便可以修改所有用到掛載Page_GamePlay腳本的Inspector界面信息。
這樣寫完之後,我們看到的腳本參數是這樣的。
這裡我就不細講Editor腳本的用法了,因為這裡做腳本數據儲存的方式並不常規,而且這種編輯關卡的方法太過容易失誤。我這裡主要是為了完成教程說明,才臨時用這用方法存儲數據。
總結:
OSU!的戰鬥功能先簡單講解了點擊圓圈和跟隨Slider兩種,下一期將會講缺失的畫圈和一個背景視頻插入的效果。這一期重點在於介紹UI系統相關的九宮格和序列幀動畫,以及遊戲玩法相關的腳本功能和信息編輯存儲。有興趣的讀者朋友可以下載Demo了解,最後附上下載地址。
chs71371/OSU_Battle
對遊戲開發感興趣的同學,歡迎圍觀我們:【皮皮關遊戲開發教育】 ,會定期更新各種教程乾貨,更有別具一格的線下小班教育。在你學習進步的路上,有皮皮關陪你!~
我們的官網地址:http://levelpp.com/
我們的遊戲開發技術交流群:610475807
我們的微信公眾號:皮皮關
推薦閱讀:
※骨骼與動畫重定向——Unity探索筆記之視覺解決方案(一)
※Unity移動端性能優化
※Unity3D 正六邊形,環狀擴散,緊密分布,的程序
※從零開始學基於ARKit的Unity3d遊戲開發系列11
※Unity 內存優化 和 內存池使用實踐