【Unity】UGUI系列教程——OSU!Battle!

前言
有些認(rèn)真的讀者可能會(huì)注意到,OSU!和本教程的表現(xiàn)方式是有差異的。因?yàn)槲易龅挠螒騺?lái)講解功能的主要目的是讓給多想學(xué)習(xí)Unity制作游戲的讀者更有興趣來(lái)學(xué)習(xí)教程,而不是枯燥的背組件的使用方式和參數(shù)作用。我更想讓UGUI偏向?qū)嵱梅较蛑v解,因此每次寫(xiě)教程之前我都是需要自己花時(shí)間想下怎么用最少的知識(shí)點(diǎn)完成我們想要效果。本期根據(jù)上期的預(yù)告,將會(huì)對(duì)OSU!的Battle部分進(jìn)行簡(jiǎn)單實(shí)現(xiàn)和講解。能想學(xué)習(xí)Unity的UGUI功能的讀者也能夠有所受用,這是我的初衷,而若是能讓想制作音樂(lè)游戲的讀者能有所啟發(fā),那真是再好不過(guò)了。
預(yù)覽效果

這里只實(shí)現(xiàn)點(diǎn)擊和拖拽,轉(zhuǎn)動(dòng)圓盤(pán)的效果會(huì)在之后的教程中介紹實(shí)現(xiàn)方法。這里由于時(shí)間原因沒(méi)有擱置未實(shí)現(xiàn)。
游戲需要的知識(shí)
序列幀動(dòng)畫(huà):
序列幀動(dòng)畫(huà)原理是和我們看電視上動(dòng)畫(huà)的原理一樣,當(dāng)圖片不停按順序切換,利用視覺(jué)殘留效果,會(huì)顯示出運(yùn)動(dòng)的感覺(jué)。因此我們只用對(duì)Image圖片做固定時(shí)間切換就可以實(shí)現(xiàn),腳本方法不闡述了,這里說(shuō)一個(gè)Unity的簡(jiǎn)單實(shí)現(xiàn)方法
創(chuàng)建一個(gè)Animator掛載到需要顯示序列幀圖片的地方

直接將序列幀圖片拖到Animation窗口的動(dòng)畫(huà)Clip中

記得動(dòng)畫(huà)Clip資源設(shè)置成循環(huán)播放。
九宮格圖片:
九宮格圖片在UI中廣泛運(yùn)用,為了優(yōu)化資源大小,我們做中間過(guò)渡簡(jiǎn)單的背景圖和長(zhǎng)條UI的時(shí)候并不會(huì)實(shí)際畫(huà)游戲中需要的圖片大小。而是利用設(shè)置九宮格圖片拉伸得到。
選擇你要設(shè)置的九宮格圖片

點(diǎn)擊Spite Editor按鈕,出現(xiàn)九宮格編輯界面

我們?nèi)绻@樣設(shè)置九宮格,那么在中間正方形區(qū)域?qū)?huì)被拉伸,而外圍區(qū)域的圖片將會(huì)保持形狀處理。
這里我們需要將一個(gè)圓形拉伸成膠囊形狀的UI,于是這樣設(shè)置,只給中間留2像素就夠了

對(duì)Image組件的Image Type選擇Sliced就后,調(diào)節(jié)Width就好了

小知識(shí)點(diǎn):
對(duì)你想統(tǒng)一修改某掛點(diǎn)下所有UI物體的透明度,掛載Canvas Group組件就好了。

搭建Note界面

點(diǎn)擊圈的界面很簡(jiǎn)單,只需要一個(gè)可以點(diǎn)擊按鈕Btn_Judge,一個(gè)提示作用的白色圈Img_AimCircle。
點(diǎn)擊后根據(jù)點(diǎn)擊準(zhǔn)確度打開(kāi)得分提示GoodState、PerfectState、FailState就好了。

滑動(dòng)條需要增加跟隨小球移動(dòng)的操作,需要增加移動(dòng)位置開(kāi)始點(diǎn)Tran_StartPoint,移動(dòng)結(jié)束位置點(diǎn)Tran_EndPoint
因?yàn)榈梅痔崾綰I和黃色的范圍提示UI要跟著小球一起移動(dòng),我們便創(chuàng)建一個(gè)移動(dòng)物體掛點(diǎn)Tran_MovePos,將這幾個(gè)需要一起移動(dòng)的UI放在下面。
邏輯功能的實(shí)現(xiàn):
針對(duì)很多新手程序員來(lái)說(shuō),寫(xiě)腳本最難的在于實(shí)現(xiàn)功能的模塊化處理。腳本與腳本之間的重復(fù)代碼過(guò)多,耦合過(guò)多,這樣很不利于維護(hù)和處理業(yè)務(wù)邏輯。
OSU!的點(diǎn)擊圈和滑動(dòng)條效果其實(shí)有很多相似的地方,比如他們都需要開(kāi)始的延遲時(shí)間,這個(gè)時(shí)候其實(shí)是讓玩家做好下一步的準(zhǔn)備,他們都有一個(gè)判定時(shí)間,在這個(gè)判定時(shí)間內(nèi)點(diǎn)擊到判定區(qū)域開(kāi)始計(jì)算得分,而滑動(dòng)條只多了一步滑動(dòng)操作。都有結(jié)算顯示,根據(jù)操作來(lái)打開(kāi)不同的得分提示,最后統(tǒng)一的刪除清理。
我們先將統(tǒng)一部分的功能實(shí)現(xiàn),創(chuàng)建一個(gè)NoteLogic腳本來(lái)做為公共的邏輯腳本處理。
NoteLogic的主要函數(shù):
時(shí)間變化,在特定的狀態(tài)計(jì)時(shí),達(dá)到目標(biāo)值后進(jìn)行狀態(tài)切換
private void Update()
{
switch (curState)
{
case eState.Delay:
{
curTime += Time.deltaTime;
if (curTime > delayTime)
{
curTime = 0;
SetCurState(eState.Wait);
}
}
break;
case eState.Operation:
case eState.Wait:
{
curTime += Time.deltaTime;
if (curTime > startTime+judgeTime+0.3f)
{
curTime = 0;
SetCurState(eState.Over);
}
}
break;
case eState.Over:
{
curTime += Time.deltaTime;
if (curTime > desTime)
{
SetCurState(eState.None);
Destroy(gameObject);
}
}
break;
}
}
設(shè)置當(dāng)前狀態(tài)函數(shù),通關(guān)枚舉類型變化來(lái)實(shí)現(xiàn)狀態(tài)切換
public void SetCurState(eState rState)
{
curState = rState;
switch (curState)
{
//界面在延遲等待的階段處理漸入效果
case eState.Delay:
curTime = 0;
var canvasGroup = gameObject.GetComponent<CanvasGroup>();
canvasGroup.alpha = 0;
gameObject.GetComponent<CanvasGroup>().DOFade(1, delayTime);
break;
//等待判定階段,將圓圈圖片做縮放動(dòng)畫(huà)
case eState.Wait:
if (curType != LevelNoteData.eNoteType.Disk)
{
circleTipObj.gameObject.SetActive(true);
circleTipObj.transform.DOScale(1, startTime).OnComplete(() => { circleTipObj.gameObject.SetActive(false); });
}
break;
//操作階段調(diào)用虛方法,讓繼承的類來(lái)自定義該狀態(tài)功能
case eState.Operation:
if (curType != LevelNoteData.eNoteType.Disk)
{
circleTipObj.gameObject.SetActive(false);
}
OnJudgetOperation();
break;
//結(jié)束狀態(tài)打開(kāi)得分提示
case eState.Over:
curTime = 0;
ShowScore();
break;
}
}
虛函數(shù),繼承的子類來(lái)實(shí)現(xiàn)這里的功能
/// <summary>
/// 做判定操作使用的虛函數(shù)
/// </summary>
public virtual void OnJudgetOperation()
{
}
打開(kāi)的得分提示UI,這里設(shè)置角度的原因是部分Note會(huì)旋轉(zhuǎn)位置,而打開(kāi)的提示UI不能隨著父物體旋轉(zhuǎn)而旋轉(zhuǎn)
public void ShowScore()
{
if (mainShowObj != null)
{
mainShowObj.gameObject.SetActive(false);
}
switch (curScore)
{
case eScore.Good:
statePointArr[0].gameObject.SetActive(true);
statePointArr[0].transform.eulerAngles = Vector3.zero;
break;
case eScore.Perfect:
statePointArr[1].gameObject.SetActive(true);
statePointArr[1].transform.eulerAngles = Vector3.zero;
break;
case eScore.Fail:
statePointArr[2].gameObject.SetActive(true);
statePointArr[2].transform.eulerAngles = Vector3.zero;
break;
}
}
HitCircle和Slider實(shí)現(xiàn)
創(chuàng)建HitCircle和Slider腳本并繼承NoteLogic類,通過(guò)重載OnJudgetOperation函數(shù)來(lái)做各自獨(dú)立的功能處理。
HitCircle腳本只用點(diǎn)擊后判斷出得分,改變當(dāng)前的狀態(tài)為Over就結(jié)束了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
public class HitCircle : NoteLogic
{
public override void OnJudgetOperation()
{
//這里簡(jiǎn)單的通過(guò)時(shí)間的差值來(lái)判斷得分
float dValue = Mathf.Abs(curTime - startTime);
if (dValue < startTime * 0.35f)
{
curScore = eScore.Perfect;
}
else if (dValue < startTime * 0.7f)
{
curScore = eScore.Good;
}
else
{
curScore = eScore.Fail;
}
SetCurState(eState.Over);
}
}
而Slider腳本需要額外擴(kuò)展掛點(diǎn),因此可以直接在子類聲明變量。
[Header("移動(dòng)掛點(diǎn)")]Unity的一個(gè)屬性,它能再Inspector界面的變量上顯示你添加的字符串。
我們直接在功能處理中讓移動(dòng)點(diǎn)移動(dòng)到目標(biāo)位置就好了,當(dāng)移動(dòng)到目標(biāo)點(diǎn)后判定得分,改變狀態(tài)為結(jié)束。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
public class Slider : NoteLogic
{
[Header("移動(dòng)掛點(diǎn)")]
public GameObject movePoint;
[Header("移動(dòng)開(kāi)始掛點(diǎn)")]
public GameObject moveStartPoint;
[Header("移動(dòng)目標(biāo)掛點(diǎn)")]
public GameObject moveTargetPoint;
[Header("滾動(dòng)球物體")]
public GameObject rollBar;
[Header("移動(dòng)提示圈")]
public RectTransform moveTipObj;
public override void OnJudgetOperation()
{
movePoint.transform.position = moveStartPoint.transform.position;
rollBar.gameObject.SetActive(true);
Tweener tween = null;
//Dotween可以很方便各種鏈接Update功能和Complete功能
tween = movePoint.transform.DOMove(moveTargetPoint.transform.position, judgeTime).OnComplete(() =>
{
//當(dāng)球移動(dòng)到目標(biāo)位置后調(diào)用
float dValue = Mathf.Abs(startTime+judgeTime -curTime);
//和點(diǎn)擊圓圈的判定一樣,其實(shí)也可以做不同的處理
if (dValue < startTime * 0.35f)
{
curScore = eScore.Perfect;
}
else if (dValue < startTime * 0.7f)
{
curScore = eScore.Good;
}
else
{
curScore = eScore.Fail;
}
rollBar.gameObject.SetActive(false);
SetCurState(eState.Over);
}).OnUpdate(() =>
{
//在每一幀移動(dòng)中判斷鼠標(biāo)是否超出跟隨小球移動(dòng)的黃色圓圈
if (Vector3.Distance(Input.mousePosition, moveTipObj.position) > moveTipObj.sizeDelta.x * 0.5f)
{
if (tween != null)
{
tween.Kill();
}
curScore = eScore.Fail;
rollBar.gameObject.SetActive(false);
SetCurState(eState.Over);
}
});
}
}
可以繼續(xù)鏈加Update方法來(lái)每幀判斷處理鼠標(biāo)是否移出判定范圍了

將腳本掛在顯示的界面物體上,看下效果
HitCircle:

Slider:

關(guān)卡配置
通用UI組件
之前我們說(shuō)過(guò)了UI做成預(yù)提體的方便之處,這里我們要將這兩個(gè)非界面類型的HitCircle和Slider物體也做成預(yù)制體,這樣通用組件化可以很方便我們動(dòng)態(tài)搭建出界面效果。
將HitCircle和Slider保存成預(yù)制體

然后我們只用按照音樂(lè)播放的時(shí)間,在特定時(shí)刻動(dòng)態(tài)讀取創(chuàng)建出Com_HitCircle和Com_Slider,設(shè)置好位置和角度,再給腳本傳入延遲、等待、判定、銷毀時(shí)間就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單音樂(lè)戰(zhàn)斗了。
配置我們的關(guān)卡數(shù)據(jù)
OSU!的Battle流程其實(shí)就是一個(gè)根據(jù)時(shí)間排序好的Note列表,我們先創(chuàng)建一個(gè)LevelNoteData類來(lái)儲(chǔ)存數(shù)據(jù)。

儲(chǔ)存關(guān)卡信息就先臨時(shí)放在Page_GamePlay界面的UI腳本上吧,其實(shí)正式數(shù)據(jù)是決不能這樣做的,這里是想快速實(shí)現(xiàn)效果避免創(chuàng)建過(guò)多的類導(dǎo)致說(shuō)明混亂。
我們先規(guī)定levelNoteList存儲(chǔ)的信息都是按照時(shí)間從小到大排序的,每幀判定當(dāng)前時(shí)間是否滿足最近的快要?jiǎng)?chuàng)建的Note的創(chuàng)建時(shí)間,若滿足就判斷最近的Note類型,創(chuàng)建出對(duì)應(yīng)的UI通用組件,并對(duì)腳本賦值。
創(chuàng)建完成后就可以將這個(gè)Note移出關(guān)卡信息列表了。

簡(jiǎn)單介紹關(guān)卡編輯
創(chuàng)建一個(gè)Editor腳本添加[CustomEditor(typeof(Page_GamePlay))]屬性便可以修改所有用到掛載Page_GamePlay腳本的Inspector界面信息。

這樣寫(xiě)完之后,我們看到的腳本參數(shù)是這樣的。

這里我就不細(xì)講Editor腳本的用法了,因?yàn)檫@里做腳本數(shù)據(jù)儲(chǔ)存的方式并不常規(guī),而且這種編輯關(guān)卡的方法太過(guò)容易失誤。我這里主要是為了完成教程說(shuō)明,才臨時(shí)用這用方法存儲(chǔ)數(shù)據(jù)。
總結(jié):
OSU!的戰(zhàn)斗功能先簡(jiǎn)單講解了點(diǎn)擊圓圈和跟隨Slider兩種,下一期將會(huì)講缺失的畫(huà)圈和一個(gè)背景視頻插入的效果。這一期重點(diǎn)在于介紹UI系統(tǒng)相關(guān)的九宮格和序列幀動(dòng)畫(huà),以及游戲玩法相關(guān)的腳本功能和信息編輯存儲(chǔ)。有興趣的讀者朋友可以下載Demo了解,最后附上下載地址。
https://github.com/chs71371/OSU_Battle
對(duì)游戲開(kāi)發(fā)感興趣的同學(xué),歡迎圍觀我們:【皮皮關(guān)游戲開(kāi)發(fā)教育】 ,會(huì)定期更新各種教程干貨,更有別具一格的線下小班教育。在你學(xué)習(xí)進(jìn)步的路上,有皮皮關(guān)陪你!~
我們的官網(wǎng)地址:http://levelpp.com/
我們的游戲開(kāi)發(fā)技術(shù)交流群:610475807
我們的微信公眾號(hào):皮皮關(guān)