使用DryWetMidi Unity包获取MIDI音符起止事件开发节奏游戏
Unity + DryWetMidi 节奏游戏鼓组事件处理实现
1. 导入DryWetMidi到Unity
- 下载DryWetMidi的Unity兼容包直接导入项目;或通过Unity Package Manager选择
Add package from git URL导入官方包,确保Melanchall.DryWetMidi命名空间可正常引用。
2. 准备资源
- 将鼓组MIDI文件导入Unity,在Inspector面板设置其Importer Type为
TextAsset,方便代码读取。 - 将对应音轨音频拖入场景的
AudioSource组件,用于同步播放。
3. 核心代码实现
创建MidiController脚本挂载到场景空物体,负责MIDI解析、音频同步与事件触发:
using UnityEngine; using Melanchall.DryWetMidi.Core; using Melanchall.DryWetMidi.Interaction; using System.Collections.Generic; using System.Linq; public class MidiController : MonoBehaviour { [Header("资源引用")] public AudioSource audioSource; public TextAsset midiFile; [Header("鼓组-动作映射")] public Dictionary<int, int> drumToActionMap = new Dictionary<int, int>() { { 36, 1 }, // 底鼓 → 动作1 { 38, 2 }, // 军鼓 → 动作2 { 42, 3 }, // 闭合踩镲 → 动作3 { 49, 4 } // 叮叮镲 → 动作4 }; private List<MidiNoteEvent> _allNoteEvents = new List<MidiNoteEvent>(); private int _currentEventIndex = 0; private bool[] _actionAllowed = new bool[5]; // 对应动作1-4的允许状态 void Start() { ParseMidiEvents(); audioSource.Play(); } /// <summary> /// 解析MIDI文件,提取鼓组音符的起止事件 /// </summary> void ParseMidiEvents() { if (midiFile == null) { Debug.LogError("未指定MIDI文件"); return; } var midiBytes = midiFile.bytes; var midiFileObj = MidiFile.Read(midiBytes); // 获取鼓组轨道(MIDI标准鼓组位于通道9) var drumTrack = midiFileObj.GetTracks().FirstOrDefault(track => { var firstNote = track.Chunks.OfType<NoteOnEvent>().FirstOrDefault(); return firstNote != null && firstNote.Channel == 9; }); if (drumTrack == null) { Debug.LogError("MIDI文件中未找到鼓组轨道"); return; } var tempoMap = midiFileObj.GetTempoMap(); var allNotes = drumTrack.GetNotes(); // 提取每个音符的开始/结束事件并转换为秒级时间 foreach (var note in allNotes) { var noteOnTime = TimeConverter.ConvertTo<MetricTimeSpan>(note.Time, tempoMap).TotalSeconds; _allNoteEvents.Add(new MidiNoteEvent { Time = noteOnTime, NoteNumber = note.NoteNumber, IsNoteOn = true }); var noteOffTime = TimeConverter.ConvertTo<MetricTimeSpan>(note.Time + note.Length, tempoMap).TotalSeconds; _allNoteEvents.Add(new MidiNoteEvent { Time = noteOffTime, NoteNumber = note.NoteNumber, IsNoteOn = false }); } // 按时间排序事件,保证触发顺序正确 _allNoteEvents = _allNoteEvents.OrderBy(evt => evt.Time).ToList(); } void Update() { if (!audioSource.isPlaying || _currentEventIndex >= _allNoteEvents.Count) return; float currentAudioTime = audioSource.time; // 处理所有已到触发时间的事件 while (_currentEventIndex < _allNoteEvents.Count && currentAudioTime >= _allNoteEvents[_currentEventIndex].Time) { var currentEvent = _allNoteEvents[_currentEventIndex]; HandleNoteEvent(currentEvent); _currentEventIndex++; } } /// <summary> /// 更新对应动作的允许状态 /// </summary> void HandleNoteEvent(MidiNoteEvent evt) { if (drumToActionMap.TryGetValue(evt.NoteNumber, out int actionId)) { _actionAllowed[actionId] = evt.IsNoteOn; Debug.Log(evt.IsNoteOn ? $"动作{actionId}已解锁" : $"动作{actionId}已锁定"); // 可在此添加提示动画、音效等逻辑 } } // 存储MIDI音符事件的关键信息 private class MidiNoteEvent { public float Time; public int NoteNumber; public bool IsNoteOn; } }
4. 关键细节说明
- 鼓组音符编号:不同MIDI文件的鼓音符编号可能有差异(比如部分底鼓用35而非36),可通过MIDI编辑器(如MidiEditor)查看对应鼓的编号,调整
drumToActionMap中的数值。 - 同步精度:若出现音频与MIDI事件轻微不同步,可给
currentAudioTime添加微小偏移(如currentAudioTime += 0.05f)抵消Unity音频播放延迟。 - 动作判断:在玩家输入检测代码中,直接读取
_actionAllowed数组状态即可判断当前是否允许执行对应动作。
内容的提问来源于stack exchange,提问作者SAVELIQ
相关产品推荐
相关产品推荐

