《Unityで作るリズムゲーム》學習筆記(三):「Notes的功能實現」
書名:《Unityで作るリズムゲーム》
作者:長崎大学マルチメディア研究会
主要Class類與結構
- NoteProperty:存儲Note的基本屬性
- TempoChange:存儲速度變化的時機和BPM
- Beatmap:譜面數據,存儲了NoteProperty列表以及BPM變化段的列表、秒數與拍子的相互變換也是在這裡完成
- PlayerController:繼承MonoBehaviour、管理譜面的播放、曲目開始後的時間和拍子單位,並持有對播放中的譜面數據類(Beatmap)的引用
- NoteControllerBase:繼承MonoBehaviour,控制notes的流動,持有對NoteProperty的引用
- SingleNoteController:派生自NoteControllerBase,控制單點Note的邏輯
- LongNoteController:派生自NoteControllerBase,控制長押Note的邏輯
類列表
-
NoteProperty
-
public enum NoteType { Single, Long } /// /// note屬性 /// public class NoteProperty { public float beatBegin; //Note進入判定線,與其重疊的一拍 public float beatEnd; //Note離開判定線,離開前與其重疊的最後一拍 public int lane; public NoteType noteType; public NoteProperty(float beatBegin, float beatEnd, int lane, NoteType noteType) { this.beatBegin = beatBegin; this.beatEnd = beatEnd; this.lane = lane; this.noteType = noteType; } }
-
-
TempoChange
-
/// /// 保存BPM變化的數據 /// public class TempoChange { public float beat; //BPM發生變化的一拍 public float tempoAfterChanged; //變化後的具體BPM值 public TempoChange(float beat, float tempoAfterChanged) { this.beat = beat; this.tempoAfterChanged = tempoAfterChanged; } }
-
-
Beatmap
-
using System.Collections.Generic; using System.Linq; /// /// 譜面數據 /// public class BeatMap { //保存所有note數據 public List<NoteProperty> noteDatas = new List<NoteProperty>(); //保存譜面中的所有速度段數據 public List<TempoChange> tempoChangeDatas = new List<TempoChange>(); //在指定的一個BPM段落裡,將拍子轉成秒 public static float BeatToSecWithFixedTempo(float beat, float tempo) { return beat / (tempo / 60); } //在指定的一個BPM段落裡,將秒轉成拍子 public static float SecToBeatWithFixedTempo(float sec, float tempo) { return sec * (tempo / 60); } //考慮曲目中的所有變速段後,計算到達某一拍的經過時間 public static float BeatToSecWithVarTempo(float beat, List<TempoChange> tempoChanges) { float accumulatedSec = 0f; //經過時間 int tempoPartIdx = 0; //變速段索引 //在當前指定的拍子之前有多少個變速段 var changeCnt = tempoChanges.Count(x => x.beat < beat); //對指定拍子前的全部變速段進行遍歷,將不同BGM段落中的拍子轉成秒數並累積起來 while (tempoPartIdx < changeCnt - 1) { accumulatedSec += BeatToSecWithFixedTempo( tempoChanges[tempoPartIdx + 1].beat - tempoChanges[tempoPartIdx].beat, tempoChanges[tempoPartIdx].tempoAfterChanged); tempoPartIdx++; } //經過上面的運算後,變速後索引現在會位於beat的前一個變速段索引上 //如果這時該索引中變速段的beat,仍然沒有到達當前beat,代表當前beat並不位於變速段的起點 //而beat將會因此大於其前一個變速段的起點beat,因此需要考慮當前指定的beat以及其前一個變速段起點的beat這裡所經過的秒數 accumulatedSec += BeatToSecWithFixedTempo(beat - tempoChanges[tempoPartIdx].beat, tempoChanges[tempoPartIdx].tempoAfterChanged); return accumulatedSec; } //考慮曲目中的所有變速段後,計算到達某一秒的經過拍子 public static float SecToBeatWithVarTempo(float sec, List<TempoChange> tempoChanges) { float accumulatedSec = 0f; int tempoPartIdx = 0; //因為無法得到TempoChange的時間信息,只能得到從哪一個拍開始變速 //因此,無法判斷sec具體是在哪一拍上,並像BeatToSec一樣篩選掉不適用的tempoChange元素 //只能直接從頭遍歷到尾,直到發現累積時間超過sec的時候中止遍歷 var changeCnt = tempoChanges.Count; while(tempoPartIdx < changeCnt - 1) { //計算累積時間 float tmpSec = accumulatedSec; accumulatedSec += BeatToSecWithFixedTempo( tempoChanges[tempoPartIdx + 1].beat - tempoChanges[tempoPartIdx].beat, tempoChanges[tempoPartIdx].tempoAfterChanged); //累積時間超過了sec,代表sec位於兩個BPM變換的起點拍子的時間之間 if(accumulatedSec >= sec) { //返回(在當前sec前的BPM變換起點拍子) + //(sec - 到上一個BPM轉換起點拍子的累積時間中,每一秒的拍子數) return tempoChanges[tempoPartIdx].beat + SecToBeatWithFixedTempo(sec - tmpSec, tempoChanges[tempoPartIdx].tempoAfterChanged); } tempoPartIdx++; } //遍歷到最後 //就返回最後一個變速段的累計拍子數 + (sec - 前面累積的秒數下的每秒拍子數) return tempoChanges[changeCnt - 1].beat + SecToBeatWithFixedTempo(sec - accumulatedSec, tempoChanges[changeCnt - 1].tempoAfterChanged); } }
-
-
PlayerController
-
using UnityEngine; public class PlayerController : MonoBehaviour { public static float scrollSpeed = 1.0f; //譜面流動速度 public static float currentSec = 0f; //當前已經過秒數 public static float currentBeat = 0f; //當前已經過拍子數 public static BeatMap beatMap; //譜面數據管理 float startOffset = 1.0f; //譜面offset(秒)=>譜面在多少秒後正式開始 private void Awake() { currentSec = 0f; currentBeat = 0f; } private void Update() { currentSec = Time.time - startOffset; //當前已經過秒數 = 當前秒數 - 譜面offset //把當前秒數轉換成拍子 currentBeat = BeatMap.SecToBeatWithVarTempo(currentSec, beatMap.tempoChangeDatas); } }
-
-
NoteControllerBase
-
using UnityEngine; public abstract class NoteControllerBase : MonoBehaviour { public NoteProperty noteProperty; }
-
-
SingleNoteController
-
using UnityEngine; public class SingleNoteController : NoteControllerBase { private void Update() { //位置更新 Vector2 pos = new Vector2(); pos.x = noteProperty.lane - 2; //選擇軌道 //高度更新 //一個note的y坐標由「自曲目開始後,note與判定線重合的時間」-「曲目開始後的已經過時間」決定 //原始坐標 * 流速則可得到note在不同流速下的y坐標 pos.y = (noteProperty.beatBegin - PlayerController.currentBeat) * PlayerController.scrollSpeed; transform.localPosition = pos; } }
-
-
LongNoteController
-
using UnityEngine; /// /// 長押 /// public class LongNoteController : NoteControllerBase { [SerializeField] Transform begin = null; [SerializeField] Transform mid = null; [SerializeField] Transform end = null; private void Update() { //長押起點和終點拍子的坐標設置 Vector2 beginPos = new Vector2(); beginPos.x = noteProperty.laneXPos; beginPos.y = (noteProperty.beatBegin - PlayerController.currentBeat) * PlayerController.scrollSpeed; begin.transform.localPosition = beginPos; Vector2 endPos = new Vector2(); endPos.x = noteProperty.laneXPos; endPos.y = (noteProperty.beatEnd - PlayerController.currentBeat) * PlayerController.scrollSpeed; end.transform.localPosition = endPos; //起終點之間中間路徑的中心點及Scale設置 Vector2 midPos = (beginPos + endPos) / 2f; mid.transform.localPosition = midPos; Vector2 midScale = mid.transform.localScale; midScale.y = endPos.y - beginPos.y; mid.transform.localScale = midScale; } }
-