Unity节奏游戏MIDI音符生成不准、首音符提前问题求助
问题梳理
我正在做大学节奏游戏项目,用MIDI文件生成音符:提前解析MIDI里的音符并分配「触发时间」(玩家需按键同步的精确时间),在触发时间前X秒生成音符,让音符在X秒内移动到按键区域。但实际运行有两个核心问题:
- 几乎所有音符都有随机微延迟,导致手感拖沓
- 首个音符生成时间远早于触发时间,和歌曲完全不同步
我想找不依赖Update/FixedUpdate、基于精确时间而非帧率的生成方案,但这是第一个Unity项目,没找到可行办法。
相关代码
SongManager 关键方法
public void ReadFromFile() //读取文件 { midiFile = MidiFile.Read(Application.streamingAssetsPath + "/" + midiLocation); //获取MIDI文件路径 GetDataFromMidi(); } public void GetDataFromMidi() //提取MIDI数据 { var notes = midiFile.GetNotes(); var array = new Melanchall.DryWetMidi.Interaction.Note[notes.Count]; notes.CopyTo(array, 0); foreach (var lane in lanes) lane.SetTimeStamps(array); //给每个轨道分配对应音符 Invoke(nameof(StartSong), songDelayInSeconds); } public static double GetAudioSourceTime() { return (double)Instance.audioSource.timeSamples / Instance.audioSource.clip.frequency; }
Lane 关键方法
void FixedUpdate() { timePassing = SongManager.GetAudioSourceTime(); if (spawnIndex < timeStamps.Count) //还有待生成的音符时 { timeSpawn = timeStamps[spawnIndex] - SongManager.Instance.noteSpeed / 2; if (timePassing >= timeSpawn) //当前音频时间达到生成时间 { Instantiate(notePrefab); //生成音符 spawnIndex++; } } } public void SetTimeStamps(Melanchall.DryWetMidi.Interaction.Note[] array) //读取歌曲音符,保存当前轨道对应的音符时间戳 { foreach (var note in array) { if (note.NoteName == noteRestriction) { var metricTimeSpan = TimeConverter.ConvertTo<MetricTimeSpan>(note.Time, SongManager.midiFile.GetTempoMap()); timeStamps.Add((double)metricTimeSpan.Minutes * 60f + metricTimeSpan.Seconds + (double)metricTimeSpan.Milliseconds / 1000f); } } }
解决方案
1. 替换FixedUpdate为协程+音频时间精确调度
FixedUpdate的执行间隔受帧率波动影响,是随机微延迟的核心原因。直接用协程基于音频时间触发生成,彻底摆脱帧率依赖:
修改Lane脚本,去掉FixedUpdate,新增协程:
private IEnumerator SpawnNotesCoroutine() { foreach (var triggerTime in timeStamps) { // 计算生成时间:触发时间提前X秒(这里X对应你原来的noteSpeed/2,需确保noteSpeed是音符移动到按键区的总时长) double spawnTime = triggerTime - SongManager.Instance.noteSpeed / 2; // 精准等待直到音频时间达标 while (SongManager.GetAudioSourceTime() < spawnTime) { yield return null; // 每帧检查一次,避免空转 } // 生成音符 Instantiate(notePrefab); } }
然后在SongManager的StartSong方法里启动协程(替换原来的FixedUpdate触发逻辑):
public void StartSong() { audioSource.Play(); foreach(var lane in lanes) { lane.StartCoroutine(lane.SpawnNotesCoroutine()); } }
2. 修复首个音符不同步问题
问题根源是MIDI时间与音频起始时间未对齐,按以下步骤排查:
- 确保
StartSong执行时audioSource.time被重置为0,避免音频已播放一段时间才开始生成音符 - 检查
Invoke(nameof(StartSong), songDelayInSeconds)的延迟时间,若设置过大,会导致音频和音符生成的时间差被放大 - 确认MIDI时间转换时,
TimeConverter.ConvertTo使用的TempoMap完全匹配MIDI文件,避免时间偏移
另外,建议给生成的音符直接传递触发时间,让音符自身基于音频时间计算移动进度,进一步提升同步精度:
// 生成音符时传递触发时间 var note = Instantiate(notePrefab).GetComponent<Note>(); note.SetTriggerTime(triggerTime); // Note脚本内的移动逻辑 void Update() { double currentAudioTime = SongManager.GetAudioSourceTime(); double timeRemaining = triggerTime - currentAudioTime; // 按剩余时间计算位置(比如剩余X秒时在轨道顶部,0秒时到达按键区) float normalizedPos = (float)(timeRemaining / SongManager.Instance.noteSpeed); transform.position = new Vector3(transform.position.x, normalizedPos * trackHeight, transform.position.z); }
3. 额外优化
- 预生成音符对象池:提前实例化一批音符,避免Instantiate的性能开销导致的突发延迟
- 增加手动校准选项:让玩家微调音频与音符的时间偏移量,弥补不同设备的硬件延迟
内容的提问来源于stack exchange,提问作者Raffaele Naccarello
相关产品推荐
相关产品推荐

