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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 13:55:22