Unity+MIDIで音ゲーを作るならコレ!(2) の続きです。
音ゲーに必要な、音が出る前にオブジェクトを表示する部分を作ります。
とりあえず結論
以下のスクリプトを空のオブジェクトにアタッチします。
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");
}
}
}
MidiFilePlayer
にはMidiFilePlayerをドラッグ&ドロップします。
MidiFilePlayer > ShowUnityEvents > OnStartPlayMidi
にはOnMIDIStart
を、
MidiFilePlayer > ShowUnityEvents > OnEventNotesMidi
にはOnMIDITick
を関連付けます。
EventFilter
には受け取りたいイベントのパラメータ範囲を入れてください。Prebuild
はMIDI2秒前のイベント(ここではオブジェクト表示)、Midi
は演奏中のイベント(ここではエフェクト表示)になります。
パラメータ数が0の項目は全てのパラメータを通します。例えば、
・chが5の時にオブジェクトを生成
・chが9でnoteが35≦n≦42の時、およびchが2の時にエフェクトを生成
するには下記のようになります。
エフェクトは各自用意し、Effect Prefab
にプレハブを入れてください。
結果
作成したものがこちらになります。
音が出る2秒前に奥の球体から生成されるオブジェクトはUpdate()
内で、音と同時に表示されるエフェクトはイベントが呼ばれた時に呼ばれるイベントMidiFilePlayer > ShowUnityEvents > OnEventNotesMidi
に関連付けた関数OnMIDITick()
で生成されています。
解説
MIDIの再生前にイベントを調べる
前回は、イベントが呼ばれた時の処理を行う関数OnMIDITick()
を作成したうえで、
MidiFilePlayer > ShowUnityEvents > OnEventNotesMidi
にセットしました。
これで、音に合わせてイベントを発動させることはできるようになりましたが、音ゲーとしては音が出る前にあらかじめオブジェクトを出しておきたいところです。
MidiFilePlayerは、
MidiFilePlayer.MPTK_MidiEvents
にすべてのイベント(音を出す、音を消すetc)を持っています。
ですので、例えば音が出る2秒前にあらかじめオブジェクトを出しておきたいのであれば、演奏中のMIDIの2秒前のイベントを調べればよさそうです。
逆に言えば、オブジェクトを出すイベントを調べる処理が動き始めてから、2秒遅れてMIDIの演奏を開始する必要があります。
MIDIを再生しないとイベントが読み込まれない問題
先ほど、MidiFilePlayer.MPTK_MidiEvents
にすべてのイベントを持っています。と書きましたが、実際にはMidiFilePlayer.MPTK_Play();
でMIDIを再生するまでデータは読み込まれません。
MidiFilePlayer.MPTK_Load()というデータを読み込むだけの関数もありますが、読み込み終了時に呼ばれるイベントがないのでここでは使わないようにしています。
再生直前に呼ばれるイベントを使う
MidiFilePlayer > ShowUnityEvents > OnStartPlayMidi
を使用すると、(MIDIデータが読み込まれた後)再生の直前に行う処理を挟むことができます。ループ再生時にも呼ばれるので、遅延再生時に使用するタイマー等もここで初期化させておくと便利です。
ここで再生直前のMIDIをMidiFilePlayer.MPTK_Pause()
で一旦停止させ、2秒遅延させてから再度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の2秒前のイベントを取得することができます。
// 音が出る前にあらかじめイベントを調べる部分
// 前回呼ばれてから今回までの間に起こったイベントを処理する
void Update()
{
if (!m_loaded)
:
// 処理する部分をここに記述
createObj(ev);
:
createObj(ev)
を自分が行いたい処理に置き換えてください。
おわりに
これで音ゲーの基礎部分ができました。いかがでしたでしょうか? お役に立てましたら幸いです。