You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 13:45:00