書名:《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;
}
}