Unityで電子ドラムを音ゲー入力装置にする
こんにちは、たつぷりです。今回も、UnityでMIDIを用いたリズムゲームを作るシリーズです。
前回までは、MIDIの楽曲データをからノートオブジェクトを生成して曲に合わせて再生することをやってきました。今回はこのゲームに入力システムを作成します。しかし、普通のスマホのリズムゲームみたいに指でタップするだけではあまり面白くないので、電子ドラムをiOS向けリズムゲームの入力する方法を考えます。
今回の範囲でできたのは以下のような感じです。
ちなみに私のドラム経験は0なので、スティックの握り方さえあってるか分かりません...
関連記事は以下の記事です
目標
以前Unityで作成したMIDIデータからノートを生成して音ゲー風に再生することを行った。 今回目標とするのは、本物の電子ドラムをゲームの入力装置として使うことである。
より具体的には以下の状況を想定している。
やること
以上の目標を実現するために必要なことを考える。 まず電子ドラムからのMIDI信号をiPhoneで処理するには、CoreMIDIフレームワークを用いる。 ここでCoreMIDIはiOSのネイティブプログラムであるので、Unityから直接扱うことができない。UnityからiOSネイティブプログラムを呼び出すにはUnity側でプラグインを用意しておく必要がある。
つまり今回行うことは、
XcodeでCoreMIDIを用いて、ドラムからのMIDI信号を取得し、信号に応じてUnityにメッセージを送る機能を実装する
Unity側でドラム入力に対応した信号を受け取りそれをゲームに反映させる
iOS向けにビルドする
である。今回iOS側からUnityへの信号送信はUnitySendMessage
関数を用いる。これはObjective-Cの関数である。また
プラグインはObjective-C++で書かれる。そのため今回はCoreMIDI周りのプログラムはObjective-Cで書くことにする(Swiftは今回使わない)。
Objective-C版のCoreMIDI
公式レファレンス
developer.apple.com
なお最後の項目、「iOS向けにビルドする」に関しては、個人的につまづくポイントがいくつかあったので、それらはまた改めて別のノートにまとめることにする。
電子ドラム
今回用いる電子ドラムはMEDELI DD401Jである。見た目は以下の図。
出力は以下の図のようになっている。今回は、MIDI出力のUSBポートと、iPadを接続します。


UnityでiOSプラグインを作成する
UnityでiOSのネイティブ機能を使うためには、プラグインを作成する必要がある。公式のレファレンスはここ。
まず、iOSプラグインをおく場所はAssets > Plugin > iOS
である。ここにObjective-C++ファイルを作成する。拡張子は.mm
である。なおUnityから直接作成することはできないので適当なエディターを用いて作成する。
CoreMIDIを使った処理
このノートの範囲では大まかな流れと重要な処理に対してのみ記述する。その他の細かいことは、また別のノートにまとめる。
なお、このセクションの内容を書くにあたって公式レファレンスはもとより、以下の資料も参考になった。
- Core MIDI その1 MIDIObject | objective-audio
- Core MIDI その2 MIDIPacketの受信 | objective-audio
- SwiftでCore MIDI - Qiita
メインの関数は、以下のGetMIDI
である。これは、Bool
型の返り値を返す。まずはこのコードの構造をまとめておく。
基本的な流れは、以下の通りである。
GetMIDI
は実際のプロジェクトはゲームのオープニング画面でその処理が行われる。この関数ではMIDIデバイスと通信するために必要な以下の一連の流れを実行する。
これらの準備が整い、電子ドラムがコントローラーとして使えるようになったらGetMIDI
がtrue
を返し、オープニング画面のスタートボタンが押せるようにしている。「Start」を押すとシーンが遷移し、ゲームが始まるという流れである。
具体的にGetMIDI
をUnityから呼び出した時、大まかには以下の流れで処理が行われる。
MIDIGetNumberOfSources
でiOSデバイスに接続されているMIDIデバイスの数を取得する。ここでは複数のデバイスが接続されることを想定していないので、結果が1
の時のみ先に進み、それ以外ではfalse
を返す。MIDIGetSource(0)
で上で検出したデバイスのMIDIエンドポイントを取得する。MIDIObjectGetStringProperty
で正しくエンドポイントの情報が取得できたら先に進む。MIDIClientCreateWithBlock
でMIDIクライアントを作成する。正しく作成できたら先に進む。MIDIInputPortCreateWithBlock
でMIDIポートを作成する。コールバックとして、onMIDIMessageReceived
というブロックを指定している。このブロックに受け取ったMIDIデータに対応じた処理を書く。つまりゲームの制御に関する処理はここに書かれる。ここも正しくポートを作成できたら先に進む。MIDIPortConnectSource
で上で取得したエンドポイントと作成したMIDIポートを接続する。正しく接続できたらtrue
を返す。
bool GetMIDI() { if (MIDIGetNumberOfSources() == 1) { MIDIEndpointRef source = MIDIGetSource(0); OSStatus err; CFStringRef strEndPointRef = NULL; err = MIDIObjectGetStringProperty(source, kMIDIPropertyName, &strEndPointRef); if (err == noErr) { MIDIClientRef client = MIDIClientRef(); err = MIDIClientCreateWithBlock(strEndPointRef, &client,onMIDIStatusChanged); if(err == noErr) { NSLog(@"MIDIClient created"); CFStringRef portName = (CFStringRef)@"inputPort"; MIDIPortRef port = MIDIPortRef(); err = MIDIInputPortCreateWithBlock(client, portName, &port, onMIDIMessageReceived); if(err == noErr) { NSLog(@"MIDIInputPort created"); err = MIDIPortConnectSource(port, source, nil); if(err == noErr) { NSLog(@"MIDIEndpoint connected to InputPort"); return true; } } } } } return false; }
onMIDIMessageRecieved
ブロックに受け取ったMIDI信号に対しての処理を書く。
MIDI信号はMIDIパケットリストの形で渡されるので、まずはここから先頭のMIDIパケットを取得する。基本的に各パケットに対してのループを回して、パケット毎に処理を行う。
各MIDIパケットは(ステータスバイト,ノート,ベロシティ)
の情報で構成されているので、パケットをこの状態に分解する。それぞれの情報はdata[0]
,data[1]
,data[2]
に格納されている。ここでやりたいのは以下の処理である。
受け取ったMIDIパケットのステータスバイトがドラムの発音情報、すなわち0x99
である時(かつベロシティが0でない時)以下の処理をする
MIDIパケットのノート情報を文字列に変換する
UnitySendMessage 関数でUnityにノート情報を送信する
受け取ったMIDIパケットのステータスバイトがドラムの発音情報、すなわち0x99
である時(かつベロシティが0でない時)、”ドラムを叩いた”ことになるのでUnity側にメッセージを送る。
この時、UnityへはUnitySendMessage
関数で情報を送ることができる。UnitySendMessage
は、あるゲームオブジェクトがもつメソッドに対して文字列のみを送ることができる。かなり制限のある関数であるが、今回の目的の上ではこれで十分である。ここではbar
オブジェクトのNoteJudge
メソッドにMIDIパケットから取得したノート情報を文字列として送る。この場合は、”ドラムのどこ(スネアなど...)を叩いたのか”という情報が送られることになる。
MIDIPacketNext
で次のMIDIパケットの処理に移る。
MIDIReadBlock onMIDIMessageReceived = ^(const MIDIPacketList *pktlist, void *srcConnRefCon) { //MIDIパケットリストの先頭のMIDIPacketのポインタを取得 MIDIPacket *packet = (MIDIPacket *)&(pktlist->packet[0]); //パケットリストからパケットの数を取得 UInt32 packetCount = pktlist->numPackets; for (NSInteger i = 0; i < packetCount; i++) { Byte status = packet->data[0]; Byte note = packet->data[1]; NSString *str = [NSString stringWithFormat:@"%d", note]; if((status == 0x99) && (packet->data[2] != 0)) { UnitySendMessage("bar", "NoteJudge", [str UTF8String]); } //次のパケットへ進む packet = MIDIPacketNext(packet); } };
Unity側の処理
GetMIDI
をUnityから使う
GetMIDI
はプラグインで定義された関数であるので、これをUnityから呼び出すには、以下のようにしておく必要がある。
下の名前空間を導入する。
using System.Runtime.InteropServices;
メンバー変数の定義と一緒に下を宣言する。
[DllImport("__Internal")] private static extern bool GetMIDI();
これで、Unityからプラグインで定義した GetMIDI
関数が使えるようになる。
UnitySendMessage
をUnityで受け取る
今回は、UnitySendMessage("bar", "NoteJudge", [str UTF8String]);
でノート情報を送信していた。これを受け取るのは、bar
オブジェクトに割り当てられたNoteJudge
メソッドである。今回はこのメソッドを以下のようにJudge
クラスに定義した。
NoteJudge
はstr
型の引数を受け取るように定義しておく。こうすることで、UnitySendMessage
が呼ばれるとこのNoteJudge
関数がノート情報(の文字列)を引数にして実行される。
具体的にこれが呼ばれると、今叩くべきノートオブジェクトを取得して、それに対応したところが実際に電子ドラムで叩かれたかどうかを判定している。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class Judge : MonoBehaviour { public Text text; private void NoteJudge(string str) { text.text = str; foreach (GameObject justNote in GameObject.FindGameObjectsWithTag("now")) { if(justNote.GetComponent<Note>().lineNumber == Line(str))//MIDIからのトーン情報から参照 { justNote.GetComponent<Note>().Success(); } } } private int Line(string tone) { if(tone == "36")//バスドラム { return 4; } else if(tone == "38")//スネア { return 3; } else if(tone == "42")//クローズドハイハット { return 2; } return 0; } }