自作音ゲーをiphone向けにビルド
こんにちは。過去何度かにわたって「UnityでMIDIを使ったリズムゲームを作り、電子ドラムで操作する」ことを試みてきました。この記事は以下の記事の続きです。
上の記事では、iOSアプリとして実際にビルドする部分は特に何も書きませんでした。今回はこれまで作ってきたプロジェクトを実際にビルドする部分について書きます。一つ一つは小さな問題なのでまとめて記事にしました。
なお、この記事で書くのは特殊なケースだったり常識的なことだったりするので、ほぼ備忘録のつもりで書いてます。
iOS向けにビルド
今回の目標はこれまで作ってきたゲームをiOSデバイスで動くようすることである。そのためにiOS用にビルドして手元のiphone8で実機テストする。 iOS向けにビルドするときは、まずUnityでビルドが行われて、Xcodeプロジェクトが作成される。次にXcodeでのビルドがうまくいくと実機に転送できるという二段構えである。
今回はどちらのステップでも問題が起きた。
Unityでビルドできない
基本的にUnity Editor上では正しく動作していたのでビルドを試みたが、ターゲットプラットフォームに関係なくUnity上でビルドが通らなかった。これは以下で説明するようにUnity Assembly Definitionに関する問題が生じていた。
Unity Assembly Definitionの問題
今回MIDI周りのプログラムの一部はGitHubで公開されているプロジェクトを改造して利用している。そこでは、これらのプログラムはUnityの拡張Editor用として用意されていた。そのため、これらをゲームそのものに利用しているとビルド時にエラーが起きるよふうだ。
そもそも全くこの辺りの制御をどうやっているのか知らなかったので、とりあえずファイルをパラパラみてみた。すると.asmdef
ファイルというものがあった。これが原因だと考えて、Unity Assembly Definitionについて調べた。
基本的にUnity Assembly Definitionは、「C#のビルドファイル(アセンブリ)を分割して出力する」ことができる機能である。こうすることでコンパイルの高速化や、マルチプラットフォーム向けの開発など様々な利点があるようだ。
確かに.asmdef
をインスペクターで確認すると、Editorで動かす時にしか機能しないようになっていた。
よって、Unityでビルドをするためには利用したファイルの参照関係を正しく理解して整理する必要がある。しかし今回はサボって以下のような手順をとった。
デフォルトでは全て、Assembly-CSharp.dll
に紐づく。今回は、MIDIを読み込んでノートを生成する部分に関与する部分のスクリプトを持ってきて、それらをまとめてあたらしいディレクトリの中においてAssembly-CSharp.dll
にビルドさせる。それ以外の今回使っていないスクリプトは消去した。
対処療法的だがとりあえずこれでUnityでビルドが通り、Xcodeプロジェクトが作成された。
Xcodeでビルドできない
次にXcodeでビルドが開始される。まず、iOSのネイティブプログラムを使うために書いたプラグイン(Objective-C++)のコンパイルはこの段階で行われる。今回は、プラグインのコードは正しいかったが以下のようにCoreMIDIに関してのエラーが出た。
CoreMIDI周りのエラー
ビルドをする際に、「CoreMIDI周りのクラスが未定義である」と言った旨のエラーが出ることがあった。これは、CoreMIDIフレームワークがプロジェクトに追加されていないことが原因である。(なぜ手動で設定しないといけない時とそうでない時があるのか筆者は理解していない。)
これは次の図のようにBundele Settingからフレームワークを手動で追加すれば解決する。
Xcodeでビルドが通ったのにアプリが開けない
以下の記事に対処法が書かれているが、これを読む限り無料でできる範囲の開発だと、3つまでしかアプリを入れることはできないらしい。確かに、ビルドした時点で4つ目だったのでこれに引っかかったようだ。
iOSアプリBuild時に「Unable to install "アプリ名"」と表示されるがXcodeの設定で解決 - Qiita
結局、自分は以下のようにした。Window > Devices and Simulators
から接続しているデバイスの情報を取得。するとそこに入っている開発中のアプリ一覧があるので不要なものをここから削除した。
この上でビルドすると今度は実機にインストールされた。
インストール後に起きた問題
アプリが起動したのにゲームが始まらない
無事、実機に転送されたのでめでたいと思って早速確認したら、アプリが起動するのにゲームが始まらないと言う問題が起こった。
より正確に述べると、スコアなどは正しく表示されているのでゲーム自体は開始され、少なくともStart()
関数くらいは読まれているらしい。つまり問題はノートの生成(note generator)がうまくいっていないことになる。
そこで、Xcodeでログを確認すると、「midiファイルが見つからない」旨のエラーが出ていた。今note generator
は、読みこむMIDIファイルはpathの直打ちで指定していた。このパスがiphoneと違うのは当然のことである。そこで正しいパスを設定する。
実際Unityでは様々なプラットフォームに対してパスを指定する方法が用意されており、以下の公式ドキュメントが参考になる。
Unity - Scripting API: Application.dataPath
なので早速その場所を指定してやろうと思ってxcodeのプロジェクト内で検索をかけてみたがMIDIファイルが見当たらなかった。 実は、通常Unityではビルドの際様々なファイルをエンコードしてしまい、今回のように楽譜のデータをその都度読み込みたいと言うように明示的にファイルを指定したい時は都合が悪い。
そこで、StreamingAssetという特別なディレクトリを用いる。これを使うと、基本的にこの中のファイルはそのままアプリに持ち込まれる。この時注意が必要なのは基本的にこの中のファイルはRead Onlyと言うことである。しかし今回はMIDIファイルを読みたいだけなので特に心配しなくてよい。
今回はAsset/
以下にStreamingAsset
というディレクトリを作成し、そこにMIDI形式の楽曲データを入れた(この時整理のためMIDI
というディレクトリにまとめてから入れた)。
そうすると以下のようなコードでフルパスを取得できる。
fullPath = Application.streamingAssetsPath + "/MIDI/" + file;
ただしfile
の部分は、今の場合***.mid
である。このようにしてStreamingAssetの場所を指定するとUnity Editorで実行するか、iphoneで実行するかなどのプラットフォームの違いに依らずに正しい場所を参照することができる。
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; } }
UnityとMIDIで音ゲー:見た目の改良
こんにちは。たつぷりです。最近趣味に費やす時間があまりなったため、少し時間が空いてしまいましたが、一ヶ月ほど前に書いていたUnityでMIDIを使ってリズムゲームを作る企画の続きです。
前回までで、MIDI音源に合わせてノートのオブジェクトを生成して曲に合わせて動かすことをやりました。
今回は、見た目を変更しようと思います。これ自体は本質的にリズムゲームのシステムに関係するわけではないのですが、いつまでもしょうもない箱でテストしてるとこちらも気分がのらないので...
というわけで、今回はあまり手間をかけずに見た目を少しよくすることをやります。
目標
最初に作ったプロトタイプは下図のようなあまりにも味気ないので、この見た目を改良することを目標にする。
今回の改良の結果下の図のようなものを作成したので、今回は下図のような見た目にする方法に限ってまとめることにする。
このように絵を改良するために以下を行った。
ノートのオブジェクトの形状を変更
Post ProcessingでBloom効果を追加
ノートの消滅時(この段階ではまだ実装していないがバーの上でタップした時)にパーティクルを生成
前提として、このプロジェクトはURPで作成している。
オブジェクトの作成
当初Unityで作成される立方体をノートオブジェクトに使っていたが、あまり見栄えが良くないので少し丸みを帯びた形にする。今回は簡単にBlenderで丸みを帯びた立方体を作成し、FBXで出力した上でUnityに取り込んだ。
Blenderで立方体を作成
Bevel Modifier
Blenderで丸みを帯びた立方体を作成するのは非常に簡単である。まずBlenderを起動するとデフォルトで立方体が表示されているので、これを変形していく。丸みを持たせるにはBevel Modifierを利用すればよい。今回の目的においていじるパラメータは
Offset:どれほど深く切り落とすか
Segments:切り落とした部分をどれほど密に補完するか
の二つである。Bevel Modifierは切基本的には各辺を斜めに切り落として新しい面を作成する。実際Bevelは動詞としては「斜めに切る」という意味であることを考えるとこのModifierの意味するところは分かりやすい。今回用いるパラメーターのOffsetは、どれほど深いところまで切り落とすかを指定する。つまり辺を切り落とすことでどれほど大きな面を作成するかを制御するのである。一方のSegmentsは切り落とされて新しくできた面をどれほど密に補完するかを指定している。つまり切り落としただけではただ平坦な面ができるだけだが、そこをSegmentsの分だけ丸みを持たせるイメージである。
マテリアルの設定
BlenderのオブジェクトをUnityでインポートするときマテリアル名やマテリアルをどこに割り当てたかなどの情報を引き継ぐことができる。詳細な情報はUnity側でも改めて設定できる。逆に、Blenderで詳細に作りこんでもそのまま出力されない。表面などの詳細な情報を反映させるには、法線マップにベイクしてから出力するなどの方法があるが、今回は触れない。今後別の記事で紹介する。
マテリアル名や、大まかな設定はUnityにも引き継がれるのでBlenderで編集する段階である程度作っておくのが良い。ある程度作れたら、これをFBX形式で出力する。
出力する際、カメラやライトも出力してしまうとUnityにもそれらが持ち込まれるので、必要なオブジェクトのみ出力するようにオプションを設定する。
個人的に簡単だと思うのは、必要なものだけ選択状態にしておいてSelected Objects にチェックを入れる方法である。 他にもObject Typesから必要なものを選ぶなどの方法がある。
これでオブジェクトがFBX形式で保存されたのでUnityにインポートする準備ができた。
Unityにインポート
先ほど作成したFBXファイルをUnityにインポートする。インポートすると、以下のようにオブジェクトのメッシュとマテリアルがパックされている。下の図ではマテリアル名やメッシュの名前は、Blenderで設定したものが引き継がれる(ここではデフォルトのまま)。マテリアルを複数設定していれば、ここで複数のマテリアルが格納されている。
このオブジェクトのインスペクターを見ると以下のようになっている。ここでExtract Materials を選択すると、オブジェクトにパックされているマテリアルを抽出することができる。実際に選択すると、抽出したマテリアルの保存先を聞かれるので、適当に他のマテリアルがあるディレクトリなどに保存する。
マテリアルを抽出すると以下のように元のオブジェクトとマテリアルが分離する。この時自動的に、元のオブジェクトには抽出したマテリアルが割り当てられる。 複数マテリアルを設定していれば、ここで複数マテリアルが設定される。
抽出したマテリアルは、当然インスペクターからいつも通り設定することができる。
Post Processing
今回は、ポストプロセスを用いて見た目を改善する。といっても、今回はBloom効果を使うだけにとどめる。
Hierarchy ViewからシーンにGlobal Volumeを追加する。Global Volumeオブジェクトに対してインスペクターからAdd Override
でPost Processing 効果を追加できる。今回はBloom効果を用いる。見栄えが良くなるように適当にパラメータを調節する。
ここでゲームビューにPost Processing効果を適用するためには、メインカメラの設定で、Rendering>Post Processing
をオンにしておく必要がある。
Visual Effect Graph
グラフの構成
今回は以下のように非常にシンプルなグラフで十分である。
やりたいことは、
OnPlay
メッセージを受け取ったら適当な量の粒子を生成し、
その後適当に収束させ、最終的に消滅する
というだけのエフェクトである。
Awake時に自動で再生しないようにする方法
デフォルトのままではVFXオブジェクトは生成時に自動で再生されてしまう。これを避けるために以下のように設定を行う。VFXオブジェクトのインスペクターから、Visual Effect > GeneralのInitial Event Name
のチェックボックスをオンにして、かつその右側のフィールドを空白にする。
こうすることでオブジェクト生成時にOnPlay
が送られることがなくなる。
同じことは一応ここにも書いてた。
スクリプトから制御
基本的にVFXグラフで作成したエフェクトは、UnityEngine.Experimental.VFX
に含まれるVisualEffect
クラス(公式レファレンス)で制御することができる。
VisualEffectクラスにはSendEvent()
メソッドが用意されている。
このメソッドの引数としてVFXグラフのEventブロックで定義したEvent名を渡すことで、VFXグラフ内でのそのEventからのフローを呼び出すことができる。
using UnityEngine; using UnityEngine.VFX; using UnityEngine.Experimental.VFX; public class Note : MonoBehaviour { private Transform VFXs; private VisualEffect VFX; void Start() { this.gameObject.GetComponent<MeshRenderer>().material = defaultMat; player = GameObject.FindGameObjectWithTag("Player"); VFXs = GameObject.FindGameObjectWithTag("VFX").transform; VFX = VFXs.GetChild(lineNumber - 1).GetComponent<VisualEffect>(); } public void Success() { VFX.SendEvent("OnPlay"); Destroy(this.gameObject); } }