Unity+MIDIで音ゲーを作るならコレ！(3)

[Unity+MIDIで音ゲーを作るならコレ！(2)](https://qiita.com/ELIXIR/items/a20ba6aeb1b0964b638c) の続きです。
音ゲーに必要な、音が出る前にオブジェクトを表示する部分を作ります。

### とりあえず結論

以下のスクリプトを空のオブジェクトにアタッチします。
```MidiDelayCall.cs
using MidiPlayerTK;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.Rendering.DebugUI;

public class MidiDelayCall : MonoBehaviour
{
    [System.Serializable]
    public class MidiMinMaxFilter
    {
        public int min = 0;
        public int max = 0;
    }
    [System.Serializable]
    public class  MidiEvFi
    {
        public MidiMinMaxFilter[] trackFilter;
        public MidiMinMaxFilter[] channelFilter;
        public MidiMinMaxFilter[] velocityFilter;
        public MidiMinMaxFilter[] noteFilter;

        private bool contains(int value, MidiMinMaxFilter[] filters)
        {
            if ((filters == null)||(filters.Length == 0))
                return true;
            foreach (MidiMinMaxFilter filter in filters)
            {
                if (value >= filter.min && value <= filter.max)
                    return true;
            }
            return false;
        }
        public bool Contains(int track, int channel, int velocity, int note)
        {
            if (!contains(track, trackFilter))        return false;
            if (!contains(channel, channelFilter))    return false;
            if (!contains(velocity, velocityFilter))  return false;
            if (!contains(note, noteFilter))          return false;
            return true;
        }
    }
    [System.Serializable]
    public class MidiEvFiArr
    {
        public MidiEvFi[] filters;
        public bool Contains(int track, int channel, int velocity, int note)
        {
            if ((filters == null) || (filters.Length == 0))
                return true;
            foreach (MidiEvFi evfi in filters)
            {
                if (evfi.Contains(track, channel, velocity, note))
                    return true;
            }
            return false;
        }
    }

    [System.Serializable]
    public class MidiEventFilter
    {
        public MidiEvFiArr prebuild;
        public MidiEvFiArr midi;
    }

    [SerializeField] private MidiFilePlayer m_midiFilePlayer;
    [SerializeField] private float m_delaySec = 2.0f;
    [SerializeField] private MidiEventFilter m_eventFilter;
    [SerializeField] GameObject m_effectPrefab;

    float m_currentTime = 0.0f;
    bool m_loaded = false;
    int m_eventIdx = 0;

    private void Awake()
    {
        m_midiFilePlayer.MPTK_PlayOnStart = false; // Start時に再生しない
        m_midiFilePlayer.MPTK_LogEvents = false; // イベントログを出力しない


    }
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        m_midiFilePlayer.MPTK_Play(); // Load and Start
    }

    // 音が出る前にあらかじめイベントを調べる部分
    // 前回呼ばれてから今回までの間に起こったイベントを処理する
    void Update()
    {
        if (!m_loaded)
            return;

        m_currentTime += Time.deltaTime;
        List<MPTKEvent> events = m_midiFilePlayer.MPTK_MidiEvents;
        for (int i = m_eventIdx; i < events.Count; i++)
        {
            MPTKEvent ev = events[i];
            float evTimeSec = ev.RealTime * 0.001f;
            if (m_currentTime < evTimeSec)
            {
                m_eventIdx = i;
                break;
            }

            if (ev.Command == MPTKCommand.NoteOn)
            {
                // フィルターに含まれるイベントのみ処理する
                if (m_eventFilter.prebuild.Contains((int)ev.Track, ev.Channel, ev.Velocity, ev.Value))
                {
                    // 処理する部分をここに記述
                    createObj(ev);
                }

                string msg = $"tr:{ev.Track} ch:{ev.Channel} note:{ev.Value} vol:{ev.Velocity}";
                Debug.LogWarning(msg);
            }
        }

    }

    // Loadが終わり再生が始まる前に呼ばれる
    public void OnMIDIStart(string str)
    {
        m_loaded = true;
        m_currentTime = 0f;
        m_eventIdx = 0;
        m_midiFilePlayer.MPTK_Pause();　// 一旦停止
        StartCoroutine(waitStartCo(m_delaySec));
        Debug.Log("MIDI Start:" + Time.time);
    }

    // 再生時に定期的に呼ばれる処理
    // 再生中、前回呼ばれてから今回までの間に起こったイベントを処理する
    public void OnMIDITick(List<MPTKEvent> midievents)
    {
        foreach (MPTKEvent ev in midievents)
        {
            if (ev.Command == MPTKCommand.NoteOn)
            {
                // フィルターに含まれるイベントのみ処理する
                if (m_eventFilter.midi.Contains((int)ev.Track, ev.Channel, ev.Velocity, ev.Value))
                {
                    // 処理する部分をここに記述
                    createEfc(ev);
                }

                string msg = $"tr:{ev.Track} ch:{ev.Channel} note:{ev.Value} vol:{ev.Velocity}";
                Debug.Log(msg);
            }
        }
    }

    // 再生終了時に呼ばれる処理
    public void OnMIDIEnd(string str, EventEndMidiEnum endEnum)
    {
        Debug.Log("MIDI End");
    }

    // MIDIの再生を遅延させる
    IEnumerator waitStartCo(float _delaySec)
    {
        yield return new WaitForSeconds(_delaySec);
        m_midiFilePlayer.MPTK_Play();
    }

    void createObj(MPTKEvent ev)
    {
        PrimitiveType type = ((ev.Value & 1) == 0) ? PrimitiveType.Sphere : PrimitiveType.Cube;
        GameObject go = GameObject.CreatePrimitive(type);
        go.transform.position = transform.position;
        go.transform.rotation = transform.rotation;
        go.transform.localScale = Vector3.one * ev.Velocity * 0.001f;

        Rigidbody rb = go.AddComponent<Rigidbody>();
        rb.useGravity = false;
        rb.AddForce(go.transform.forward * 4.0f, ForceMode.Impulse);
        Destroy(go, 10.0f); // 10秒後に自動消滅
    }

    void createEfc(MPTKEvent ev)
    {
        if (m_effectPrefab != null)
        {
            GameObject efcGo = Instantiate(m_effectPrefab);
            efcGo.transform.position = transform.position + Random.onUnitSphere * 3f;
            efcGo.transform.localScale = Vector3.one * ev.Velocity * 0.01f;
            if (ev.Channel == 2)
            {
                efcGo.transform.localScale *= 0.1f;
            }
            Destroy(efcGo, 2.0f); // 2秒後に自動消滅
        }
        else
        {
            Debug.LogWarning("Effect Prefab is not set");
        }
    }
}
```
<br>

```MidiFilePlayer```にはMidiFilePlayerをドラッグ＆ドロップします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/9bef4bb3-cd18-089c-56cf-f033c7498fb8.png)
<br>
```MidiFilePlayer > ShowUnityEvents > OnStartPlayMidi```には```OnMIDIStart```を、
```MidiFilePlayer > ShowUnityEvents > OnEventNotesMidi```には```OnMIDITick```を関連付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/3411cdc4-fe3e-225f-a5c2-237da3332a8a.png)

<br>

```EventFilter```には受け取りたいイベントのパラメータ範囲を入れてください。```Prebuild```はMIDI２秒前のイベント（ここではオブジェクト表示）、```Midi```は演奏中のイベント（ここではエフェクト表示）になります。

パラメータ数が０の項目は全てのパラメータを通します。例えば、
　・chが5の時にオブジェクトを生成
　・chが9でnoteが35≦n≦42の時、およびchが2の時にエフェクトを生成
するには下記のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/0e757ce1-ca71-f070-72e4-6fb6731d85ef.png)

<br>

オブジェクトの先行時間はDelaySecで変更できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/3c71f14c-4b78-0039-ae6f-a35db780f1ec.png)

<br>

エフェクトは各自用意し、```Effect Prefab```にプレハブを入れてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/365bf3a4-e6f9-39ed-e513-29cceba48146.png)

### 結果
作成したものがこちらになります。
<iframe width="560" height="315" src="https://www.youtube.com/embed/4VYsBAEgo-U?si=ZF2naRwHG5Ho6rgm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

音が出る２秒前に奥の球体から生成されるオブジェクトは```Update()```内で、音と同時に表示されるエフェクトはイベントが呼ばれた時に呼ばれるイベント```MidiFilePlayer > ShowUnityEvents > OnEventNotesMidi```に関連付けた関数```OnMIDITick()```で生成されています。

### 解説
#### MIDIの再生前にイベントを調べる

前回は、イベントが呼ばれた時の処理を行う関数```OnMIDITick()```を作成したうえで、
```MidiFilePlayer > ShowUnityEvents > OnEventNotesMidi```
にセットしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/f9195352-2f2b-ad36-eddf-7e5696e7eba7.png)

これで、音に合わせてイベントを発動させることはできるようになりましたが、音ゲーとしては音が出る前にあらかじめオブジェクトを出しておきたいところです。

MidiFilePlayerは、
```MidiFilePlayer.MPTK_MidiEvents```にすべてのイベント（音を出す、音を消すetc）を持っています。
ですので、例えば音が出る２秒前にあらかじめオブジェクトを出しておきたいのであれば、演奏中のMIDIの２秒前のイベントを調べればよさそうです。
逆に言えば、オブジェクトを出すイベントを調べる処理が動き始めてから、２秒遅れてMIDIの演奏を開始する必要があります。

#### MIDIを再生しないとイベントが読み込まれない問題
先ほど、```MidiFilePlayer.MPTK_MidiEvents```にすべてのイベントを持っています。と書きましたが、実際には```MidiFilePlayer.MPTK_Play();```でMIDIを再生するまでデータは読み込まれません。

:::note info
MidiFilePlayer.MPTK_Load()というデータを読み込むだけの関数もありますが、読み込み終了時に呼ばれるイベントがないのでここでは使わないようにしています。
:::

#### 再生直前に呼ばれるイベントを使う
```MidiFilePlayer > ShowUnityEvents > OnStartPlayMidi```を使用すると、(MIDIデータが読み込まれた後)再生の直前に行う処理を挟むことができます。ループ再生時にも呼ばれるので、遅延再生時に使用するタイマー等もここで初期化させておくと便利です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/3411cdc4-fe3e-225f-a5c2-237da3332a8a.png)

ここで再生直前のMIDIを```MidiFilePlayer.MPTK_Pause()```で一旦停止させ、２秒遅延させてから再度```MidiFilePlayer.MPTK_Play()```で再生させる処理を入れます。
```
    // MIDIの再生を遅延させる
    IEnumerator waitStartCo(float _delaySec)
    {
        yield return new WaitForSeconds(_delaySec);
        m_midiFilePlayer.MPTK_Play();
    }
```

#### MIDI再生中に呼ばれる関数を作成する
```MidiFilePlayer > ShowUnityEvents > OnEventNotesMidi```を使用すると、MIDI再生中に定期的に呼ばれる処理を挟むことができます。
```
    // 再生時に定期的に呼ばれる処理
    // 再生中、前回呼ばれてから今回までの間に起こったイベントを処理する
    public void OnMIDITick(List<MPTKEvent> midievents)
    {
        foreach (MPTKEvent ev in midievents)
　　　　　　　：
            // 処理する部分をここに記述
            createEfc(ev);
　　　　　　　：
```
```OnMIDITick()```の部分では前回と同じ、演奏中のイベントを取得することができます。
```createEfc(ev)```を自分が行いたい処理に置き換えてください。

#### MIDI再生に先行してイベントを調べる処理をUpdate()内に作成する
MIDI再生時、一定タイミングで```MidiFilePlayer > ShowUnityEvents > OnStartPlayMidi```
のイベントが呼ばれますが、音が出る前にオブジェクトを出すためのイベントを調べる部分には自分で作成する必要があります。

```Update()```の部分では演奏中MIDIの２秒前のイベントを取得することができます。
```
    // 音が出る前にあらかじめイベントを調べる部分
    // 前回呼ばれてから今回までの間に起こったイベントを処理する
    void Update()
    {
        if (!m_loaded)
　　　　　　　：
            // 処理する部分をここに記述
            createObj(ev);
　　　　　　　：
      
```
```createObj(ev)```を自分が行いたい処理に置き換えてください。

### おわりに
~~これで音ゲーの基礎部分ができました。いかがでしたでしょうか？ お役に立てましたら幸いです。~~

<a href = "https://qiita.com/ELIXIR/items/ea0d387b2a26da5b4eba"><img width="120" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/db1b8d96-64d7-28b9-794c-c4fa46646255.png"></a>