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

节奏游戏读取MIDI文件仅部分车道生成音符问题求助

问题描述

我正在开发一款大学项目节奏游戏,游戏读取MIDI文件中的关卡谱面,将特定类型的音符分配至不同车道。各车道会根据音符速度计算每个音符的生成Y坐标并生成音符,但6条车道中仅第2条和第5条可正常工作。

每个车道拥有两个核心数组:

  • timeStamps:存储音符出现时间,从读取MIDI文件的SongManager获取数据
  • notesPositionsY:存储音符生成Y坐标,每个时间戳对应一个坐标,用于在准确时间将音符生成至车道控制器位置

运行时发现仅第2、5车道的notesPositionsY数组被填充,其余车道该数组为空。


相关代码

NewLane 脚本

public class NewLane : MonoBehaviour
{
    public Melanchall.DryWetMidi.MusicTheory.NoteName noteRestriction; // 限制车道仅接收特定音符
    public KeyCode input; // 对应按键

    public GameObject notePrefab; // 车道对应的音符预制体
    public GameObject laneButton; // 车道对应的彩色控制器按钮

    public float positionX; // 车道的X轴位置

    public List<double> timeStamps = new List<double>(); // 存储音符出现时间的列表
    public List<float> notesPositionsY = new List<float>(); // 存储音符生成Y坐标的列表

    Vector3 spawnPoint;

    void Start()
    {
        SetPositionsY(); // 填充notesPositionsY
        Instantiate(laneButton); // 实例化车道按钮
        Vector3 newPos = new Vector3(positionX, 0, 0); // 创建车道位置
        transform.position = newPos; // 设置车道位置
        int i = 0;

        foreach (double timestamp in timeStamps) 
        {
            spawnPoint = new Vector3(transform.position.x, notesPositionsY[i], -1); // 创建音符生成位置
            Instantiate(notePrefab, spawnPoint, transform.rotation); // 在对应位置实例化音符
            i++;
        }
    }

    public void SetTimeStamps(Melanchall.DryWetMidi.Interaction.Note[] array) // 读取所有音符,保存当前车道对应的音符时间戳
    {
        foreach (var note in array)
        {
            if (note.NoteName == noteRestriction) 
            {
                var metricTimeSpan = TimeConverter.ConvertTo<MetricTimeSpan>(note.Time, NewSongManager.midiFile.GetTempoMap()); // 转换为标准时间单位
                timeStamps.Add((double)metricTimeSpan.Minutes * 60f + metricTimeSpan.Seconds + (double)metricTimeSpan.Milliseconds / 1000f); // 保存时间戳
            }
        }
    }

    public void SetPositionsY() 
    {
        foreach (double timestamp in timeStamps) 
        {
            notesPositionsY.Add(((float)timestamp * NewSongManager.Instance.noteSpeed) + NewSongManager.Instance.noteSpeed * NewSongManager.Instance.songDelayInSeconds); // 计算Y坐标:(出现时间*速度) + 偏移量
        }
    }
}

NewSongManager 脚本

public class NewSongManager : MonoBehaviour
{
    public static NewSongManager Instance; // 单例实例
    public AudioSource audioSource; 
    public NewLane[] lanes; // 所有车道数组

    public float noteSpeed = 0;

    public string midiLocation; // MIDI文件路径
    public static MidiFile midiFile; // 读取的MIDI文件

    public int songDelayInSeconds = 0;

    void Start()
    {
        Instance = this; 
        ReadFromFile();
    }

    public void ReadFromFile()  // 读取MIDI文件
    {
        midiFile = MidiFile.Read(Application.streamingAssetsPath + "/" + midiLocation); 
        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;
    }

    public void StartSong()
    {
        audioSource.Play();
    }
}

问题分析与修复方案

核心问题

  1. 执行顺序错误:NewLane的Start方法中先调用了SetPositionsY(),但此时timeStamps数组还未被NewSongManager填充。Unity中不同脚本的Start方法执行顺序不确定,只有少数车道刚好在SetTimeStamps之后执行了SetPositionsY,所以能正常生成Y坐标数组。

  2. 音符匹配可能失效:部分车道的noteRestriction参数配置错误,导致MIDI文件中没有匹配的音符,timeStamps为空,进而notesPositionsY也为空。

修复步骤

步骤1:调整执行顺序,确保Y坐标在时间戳填充后计算

修改NewSongManager的GetDataFromMidi方法,在给所有车道设置完时间戳后,主动触发Y坐标计算:

public void GetDataFromMidi()
{
    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);
        lane.SetPositionsY(); // 新增:设置完时间戳后立即计算Y坐标
    }

    Invoke(nameof(StartSong), songDelayInSeconds);
}

同时修改NewLane的Start方法,移除原本的SetPositionsY()调用:

void Start()
{
    // 移除SetPositionsY()调用,改由SongManager统一触发
    Instantiate(laneButton);
    Vector3 newPos = new Vector3(positionX, 0, 0);
    transform.position = newPos;
    int i = 0;

    foreach (double timestamp in timeStamps)
    {
        spawnPoint = new Vector3(transform.position.x, notesPositionsY[i], -1);
        Instantiate(notePrefab, spawnPoint, transform.rotation);
        i++;
    }
}

步骤2:验证车道音符配置

  • 在Unity编辑器中检查每个NewLane组件的noteRestriction参数,确保每个车道对应的音符在目标MIDI文件中存在。
  • 可以在SetTimeStamps方法中添加日志,确认是否有音符被匹配:
public void SetTimeStamps(Melanchall.DryWetMidi.Interaction.Note[] array)
{
    int matchedCount = 0;
    foreach (var note in array)
    {
        if (note.NoteName == noteRestriction)
        {
            var metricTimeSpan = TimeConverter.ConvertTo<MetricTimeSpan>(note.Time, NewSongManager.midiFile.GetTempoMap());
            timeStamps.Add((double)metricTimeSpan.Minutes * 60f + metricTimeSpan.Seconds + (double)metricTimeSpan.Milliseconds / 1000f);
            matchedCount++;
        }
    }
    Debug.Log($"车道{gameObject.name}匹配到{matchedCount}个音符");
}

内容的提问来源于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 00:33:14