《Unityで作るリズムゲーム》學習筆記(八):「曲目播放」
書名:《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"]; } //... }
-
- 現在的音源文件名保存了在headerData[“WAV01”]中,我們可以通過該音源文件名,加上之前保留下來的bms文件夾絕對路徑,得出音源的絕對路徑。
-
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); }
-
-