書名:《Unityで作るリズムゲーム》
作者:長崎大学マルチメディア研究会
播放曲目
遊戲的效果音可以通過AudioSource.PlayClipAtPoint來播放,在NoteBaseController中添加一個AudioClip,並在OnKeyDown和OnKeyUp的時候進行調用
public abstract class NoteControllerBase : MonoBehaviour
{
//...
[SerializeField] protected AudioClip clipHit = null; //效果音
}//SingleNoteController
public override void OnKeyDown(JudgementType judgementType)
{
//...
if(judgementType != JudgementType.MISS)
{
AudioSource.PlayClipAtPoint(clipHit, transform.position);
//...
}
}
//LongNoteController
public override void OnKeyDown(JudgementType judgementType)
{
//...
if(judgementType != JudgementType.MISS)
{
AudioSource.PlayClipAtPoint(clipHit, transform.position);
//...
}
}
public override void OnKeyUp(JudgementType judgementType)
{
//...
AudioSource.PlayClipAtPoint(clipHit, transform.position);
//...
}
曲目具體的樂曲播放
在bms文件中,有專門播放音源的命令,通過把音源加到BGM通道上,從而把播放音源的命令添加到bms文件中。在把音源和對應的音源物件分配好後,bms文檔會多出一段指向音源文件的代碼
在本案例中,編號01是音源文件的通道,只要把音樂文件分配到#WAV01號中,然後把01對象放置在BGM通道中,音樂即可播放
音源添加流程
把音源文件放到Beatmap的文件夾中
創建一個Bms文件,並在#WAV區域內,把音源文件放到編號01的格子中
把編號01的物體設置在BPM軌道的第0小節開始位置上
該Bms文件的表述為:
*---------------------- HEADER FIELD
#PLAYER 1
#GENRE Electric
#TITLE Vitality t+pazolite Remix
#ARTIST t+pazolite & Mittsies
#BPM 175
#PLAYLEVEL 9
#RANK 1
#LNTYPE 1
#WAV01 Vitality.wav
*---------------------- MAIN DATA FIELD
#00001:01
音源文件名
#WAV01 Vitality.wav
具體數據信息
#00001:01
000小節
BGM通道(通道編號01)
物體本身(物體編號01)
- 讀取音源信息及文件
BmsLoader
Header正則部分添加一個讀取音源信息的正則表達式
//Header數據格式的正則表達式
static List<string> headerPattern = new List<string>
{
//...
@"#(WAV[0-9A-Z]{2}) (.*)" //對應WAV00-ZZ
};
新增一個BGM的數據類型以及對應的處理分支,在分支中,得到音源播放的偏移量
(「音源開始播放的拍子與第0拍的差」這個信息轉換成對應的秒數 = 音源開始播放的具體時間偏移量)
//數據(Note)類型
public enum DataType
{
UNSUPPORTED,
SINGLE_NOTE, //1L, L = Lane
LONG_NOTE, //5L, L = Lane
DIRECT_TEMPO_CHANGE, //直接指定型BPM變化:03
INDEXED_TEMPO_CHANGE, //索引指定型BPM變化:08
MEASURE_CHANGE, //拍子變化:02
BGM //音源:01
}
//通過通道編號來得到數據類型
DataType GetDataType(string channel)
{
if (channel[0] == '1') { return DataType.SINGLE_NOTE; }
else if (channel[0] == '5') { return DataType.LONG_NOTE; }
else if (channel == "02") { return DataType.MEASURE_CHANGE; }
else if (channel == "03") { return DataType.DIRECT_TEMPO_CHANGE; }
else if(channel == "08") { return DataType.INDEXED_TEMPO_CHANGE; }
else if(channel == "01") { return DataType.BGM; }
else { throw new Exception($"DataType Not Found With Channel: {channel}"); }
}
void LoadMainDataLine(string line)
{
//...
//Note類型/BPM變化類型/音源播放類型
else if(dataType == DataType.SINGLE_NOTE || dataType == DataType.LONG_NOTE ||
dataType == DataType.DIRECT_TEMPO_CHANGE || dataType == DataType.INDEXED_TEMPO_CHANGE ||
dataType == DataType.BGM)
{
//...
//數據為BGM類型
else if(dataType == DataType.BGM)
{
//「音源開始播放的拍子與第0拍的差」這個信息轉換成對應的秒數 = 音源開始播放的具體時間偏移量
audioOffset = BeatMap.BeatToSecWithVarTempo(beat, tempoChanges);
}
}
}
Beatmap
現在的音源文件名保存了在headerData[“WAV01”]中,我們可以通過該音源文件名,加上之前保留下來的bms文件夾絕對路徑,得出音源的絕對路徑。
public string audioFilePath = ""; //樂曲文件絕對路徑
public float audioOffset; //音源播放偏移
// 實例化BeatMap時讓BmsLoader讀取並解析bms文件
public BeatMap(string filePath)
{
//bms文件夾的絕對路徑
string bmsDirectory = new FileInfo(filePath).DirectoryName;
//...
//從bms文件中的HeaderData中得到音源文件名,讀取音源文件路徑
if(bmsLoader.headerData.ContainsKey("WAV01"))
{
//音源文件絕對路徑
audioFilePath = bmsDirectory + "\\" + bmsLoader.headerData["WAV01"];
}
//...
}
PlayController
添加AudioSource用以播放音源,並根據”file:///” + 音源文件路徑,並通過UnityWebRequestMultimedia.GetAudioClip得到本地的音源文件
注意!只可以加載.wav/.ogg格式的文件,且文件名不能有特殊字符
IEnumerator LoadAudioFile(string filePath)
{
if (!File.Exists(filePath)) { yield break; }
var audioType = GetAudioType(filePath);
//讀取音頻文件(注意!只能讀取wav/ogg,且文件名不能有任何特殊字符,如空格、下划線等等)
using (var request = UnityWebRequestMultimedia.GetAudioClip(
"file:///" + filePath, audioType))
{
yield return request.SendWebRequest();
if (!request.isNetworkError)
{
//得到音頻文件
var audioClip = DownloadHandlerAudioClip.GetContent(request);
audioSource.clip = audioClip;
}
}
}
//只能處理Ogg或Wav
AudioType GetAudioType(string filePath)
{
string ext = Path.GetExtension(filePath).ToLower();
Debug.Log(ext);
switch (ext)
{
case ".ogg":
return AudioType.OGGVORBIS;
case ".wav":
return AudioType.WAV;
default:
return AudioType.UNKNOWN;
}
}
添加一個startSecond記錄遊戲開始時的時間,以及一個標志位表示遊戲是否暫停。通過這兩個字段實現遊戲的暫停與重啟:
遊戲暫停(AudioSettings.dspTime也會停止計算)時,會把開始秒數等同於當前Time.time;再開時,會使音頻在AudioSettings.dspTime的基礎上,加上譜面的偏移和音源的偏移後繼續,使音樂在正確的位置再開。
同樣地,由於當前遊戲經過時間 = Time.time – 譜面偏移 – 開始時間,暫停時,開始時間 = Time.time,因此,當前遊戲經過會被設置為「譜面偏移」,間接使遊戲時間停止,同時確保基於當前拍子數(由當前經過秒數轉換)notes的位置正確
//...
float startSecond = 0f; //譜面播放開始後秒數(用於暫停播放)
bool isPlaying = false; //譜面暫停標志符
private void Update()
{
//開始播放譜面和曲目
if(!isPlaying && Input.GetKeyDown(KeyCode.Space))
{
isPlaying = true;
//等待指定秒數(譜面開始時間 + 音源播放的偏移)後播放音源
//暫停時也要利用AudioSettings來暫停,它代表了音樂系統的時間
audioSource.PlayScheduled(
AudioSettings.dspTime + startOffset + beatMap.audioOffset
);
}
//暫停中,譜面開始播放秒數與Time.time保持一致
//也就是說等同於譜面沒有開始
if (!isPlaying)
{
startSecond = Time.time;
}
//當前已經過秒數等於 = 當前秒數 - 譜面下落的偏移 - 譜面開始時間
currentSec = Time.time - startOffset - startSecond;
//把當前秒數轉換成拍子
currentBeat = BeatMap.SecToBeatWithVarTempo(currentSec, beatMap.tempoChangeDatas);
}