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