← 筆記

《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;
           }
        }