节奏游戏读取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(); } }
问题分析与修复方案
核心问题
执行顺序错误:
NewLane的Start方法中先调用了SetPositionsY(),但此时timeStamps数组还未被NewSongManager填充。Unity中不同脚本的Start方法执行顺序不确定,只有少数车道刚好在SetTimeStamps之后执行了SetPositionsY,所以能正常生成Y坐标数组。音符匹配可能失效:部分车道的
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
相关产品推荐
相关产品推荐

