たつぷりの調査報告書

博士後期課程(理学)の学生が趣味でUnityやBlenderで遊ぶブログです。素人が独学で勉強した際の忘備録です。

自作音ゲーをiphone向けにビルド

こんにちは。過去何度かにわたって「UnityでMIDIを使ったリズムゲームを作り、電子ドラムで操作する」ことを試みてきました。この記事は以下の記事の続きです。

tatsupuri.hatenablog.com

上の記事では、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からフレームワークを手動で追加すれば解決する。 f:id:Tatsupuri:20201129165621p:plain

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ファイルを読みたいだけなので特に心配しなくてよい。

ストリーミングアセット - Unity マニュアル

今回はAsset/以下にStreamingAssetというディレクトリを作成し、そこにMIDI形式の楽曲データを入れた(この時整理のためMIDIというディレクトリにまとめてから入れた)。

そうすると以下のようなコードでフルパスを取得できる。

fullPath = Application.streamingAssetsPath + "/MIDI/" + file;

ただしfileの部分は、今の場合***.midである。このようにしてStreamingAssetの場所を指定するとUnity Editorで実行するか、iphoneで実行するかなどのプラットフォームの違いに依らずに正しい場所を参照することができる。

Unityで電子ドラムを音ゲー入力装置にする

こんにちは、たつぷりです。今回も、UnityでMIDIを用いたリズムゲームを作るシリーズです。

前回までは、MIDIの楽曲データをからノートオブジェクトを生成して曲に合わせて再生することをやってきました。今回はこのゲームに入力システムを作成します。しかし、普通のスマホリズムゲームみたいに指でタップするだけではあまり面白くないので、電子ドラムをiOS向けリズムゲームの入力する方法を考えます。

今回の範囲でできたのは以下のような感じです。

www.youtube.com

ちなみに私のドラム経験は0なので、スティックの握り方さえあってるか分かりません...

関連記事は以下の記事です

tatsupuri.hatenablog.com

目標

以前Unityで作成したMIDIデータからノートを生成して音ゲー風に再生することを行った。 今回目標とするのは、本物の電子ドラムをゲームの入力装置として使うことである。

より具体的には以下の状況を想定している。

  • iPhone(またはiPad)のライトニングポートと電子ドラムのMIDI出力ポートを接続する

  • iPhineではリズムゲームが始まる

  • 電子ドラムを叩くと、iPhoneのゲームに対応する入力を送信する

やること

以上の目標を実現するために必要なことを考える。 まず電子ドラムからのMIDI信号をiPhoneで処理するには、CoreMIDIフレームワークを用いる。 ここでCoreMIDIはiOSのネイティブプログラムであるので、Unityから直接扱うことができない。UnityからiOSネイティブプログラムを呼び出すにはUnity側でプラグインを用意しておく必要がある。

つまり今回行うことは、

  • UnityからCoreMIDIフレームワークを呼び出すためのプラグインを作成する

  • 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である。見た目は以下の図。

f:id:Tatsupuri:20201129190532j:plain:w500

出力は以下の図のようになっている。今回は、MIDI出力のUSBポートと、iPadを接続します。

f:id:Tatsupuri:20201129190509j:plainf:id:Tatsupuri:20201129190504j:plain

UnityでiOSプラグインを作成する

UnityでiOSのネイティブ機能を使うためには、プラグインを作成する必要がある。公式のレファレンスはここ

まず、iOSプラグインをおく場所はAssets > Plugin > iOSである。ここにObjective-C++ファイルを作成する。拡張子は.mm である。なおUnityから直接作成することはできないので適当なエディターを用いて作成する。

f:id:Tatsupuri:20201129165404p:plain

CoreMIDIを使った処理

このノートの範囲では大まかな流れと重要な処理に対してのみ記述する。その他の細かいことは、また別のノートにまとめる。

なお、このセクションの内容を書くにあたって公式レファレンスはもとより、以下の資料も参考になった。

メインの関数は、以下のGetMIDIである。これは、Bool型の返り値を返す。まずはこのコードの構造をまとめておく。 基本的な流れは、以下の通りである。

GetMIDIは実際のプロジェクトはゲームのオープニング画面でその処理が行われる。この関数ではMIDIバイスと通信するために必要な以下の一連の流れを実行する。

これらの準備が整い、電子ドラムがコントローラーとして使えるようになったらGetMIDItrueを返し、オープニング画面のスタートボタンが押せるようにしている。「Start」を押すとシーンが遷移し、ゲームが始まるという流れである。

f:id:Tatsupuri:20201129165413j:plain:w300f:id:Tatsupuri:20201129165417p:plain:w300

具体的にGetMIDIをUnityから呼び出した時、大まかには以下の流れで処理が行われる。

  1. MIDIGetNumberOfSourcesiOSバイスに接続されているMIDIバイスの数を取得する。ここでは複数のデバイスが接続されることを想定していないので、結果が1の時のみ先に進み、それ以外ではfalseを返す。

  2. MIDIGetSource(0)で上で検出したデバイスMIDIエンドポイントを取得する。MIDIObjectGetStringPropertyで正しくエンドポイントの情報が取得できたら先に進む。

  3. MIDIClientCreateWithBlockMIDIクライアントを作成する。正しく作成できたら先に進む。

  4. MIDIInputPortCreateWithBlockMIDIポートを作成する。コールバックとして、onMIDIMessageReceivedというブロックを指定している。このブロックに受け取ったMIDIデータに対応じた処理を書く。つまりゲームの制御に関する処理はここに書かれる。ここも正しくポートを作成できたら先に進む。

  5. 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クラスに定義した。

NoteJudgestr型の引数を受け取るように定義しておく。こうすることで、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音源に合わせてノートのオブジェクトを生成して曲に合わせて動かすことをやりました。

tatsupuri.hatenablog.com

tatsupuri.hatenablog.com

今回は、見た目を変更しようと思います。これ自体は本質的にリズムゲームのシステムに関係するわけではないのですが、いつまでもしょうもない箱でテストしてるとこちらも気分がのらないので...

というわけで、今回はあまり手間をかけずに見た目を少しよくすることをやります。

目標

最初に作ったプロトタイプは下図のようなあまりにも味気ないので、この見た目を改良することを目標にする。

f:id:Tatsupuri:20201129164849p:plain:w500

今回の改良の結果下の図のようなものを作成したので、今回は下図のような見た目にする方法に限ってまとめることにする。

f:id:Tatsupuri:20201129164854p:plain:w500

このように絵を改良するために以下を行った。

  • ノートのオブジェクトの形状を変更

  • Post ProcessingでBloom効果を追加

  • ノートの消滅時(この段階ではまだ実装していないがバーの上でタップした時)にパーティクルを生成

前提として、このプロジェクトはURPで作成している。

オブジェクトの作成

当初Unityで作成される立方体をノートオブジェクトに使っていたが、あまり見栄えが良くないので少し丸みを帯びた形にする。今回は簡単にBlenderで丸みを帯びた立方体を作成し、FBXで出力した上でUnityに取り込んだ。

Blenderで立方体を作成

Bevel Modifier

Blenderで丸みを帯びた立方体を作成するのは非常に簡単である。まずBlenderを起動するとデフォルトで立方体が表示されているので、これを変形していく。丸みを持たせるにはBevel Modifierを利用すればよい。今回の目的においていじるパラメータは

  • Offset:どれほど深く切り落とすか

  • Segments:切り落とした部分をどれほど密に補完するか

の二つである。Bevel Modifierは切基本的には各辺を斜めに切り落として新しい面を作成する。実際Bevelは動詞としては「斜めに切る」という意味であることを考えるとこのModifierの意味するところは分かりやすい。今回用いるパラメーターのOffsetは、どれほど深いところまで切り落とすかを指定する。つまり辺を切り落とすことでどれほど大きな面を作成するかを制御するのである。一方のSegmentsは切り落とされて新しくできた面をどれほど密に補完するかを指定している。つまり切り落としただけではただ平坦な面ができるだけだが、そこをSegmentsの分だけ丸みを持たせるイメージである。

f:id:Tatsupuri:20201129164900p:plain

マテリアルの設定

BlenderのオブジェクトをUnityでインポートするときマテリアル名やマテリアルをどこに割り当てたかなどの情報を引き継ぐことができる。詳細な情報はUnity側でも改めて設定できる。逆に、Blenderで詳細に作りこんでもそのまま出力されない。表面などの詳細な情報を反映させるには、法線マップにベイクしてから出力するなどの方法があるが、今回は触れない。今後別の記事で紹介する。

マテリアル名や、大まかな設定はUnityにも引き継がれるのでBlenderで編集する段階である程度作っておくのが良い。ある程度作れたら、これをFBX形式で出力する。

f:id:Tatsupuri:20201129164915p:plain:w500

出力する際、カメラやライトも出力してしまうとUnityにもそれらが持ち込まれるので、必要なオブジェクトのみ出力するようにオプションを設定する。

個人的に簡単だと思うのは、必要なものだけ選択状態にしておいてSelected Objects にチェックを入れる方法である。 他にもObject Typesから必要なものを選ぶなどの方法がある。

f:id:Tatsupuri:20201129164921p:plain:w500

これでオブジェクトがFBX形式で保存されたのでUnityにインポートする準備ができた。

Unityにインポート

先ほど作成したFBXファイルをUnityにインポートする。インポートすると、以下のようにオブジェクトのメッシュとマテリアルがパックされている。下の図ではマテリアル名やメッシュの名前は、Blenderで設定したものが引き継がれる(ここではデフォルトのまま)。マテリアルを複数設定していれば、ここで複数のマテリアルが格納されている。

f:id:Tatsupuri:20201129164927p:plain:w500

このオブジェクトのインスペクターを見ると以下のようになっている。ここでExtract Materials を選択すると、オブジェクトにパックされているマテリアルを抽出することができる。実際に選択すると、抽出したマテリアルの保存先を聞かれるので、適当に他のマテリアルがあるディレクトリなどに保存する。

f:id:Tatsupuri:20201129164933p:plain:w500

マテリアルを抽出すると以下のように元のオブジェクトとマテリアルが分離する。この時自動的に、元のオブジェクトには抽出したマテリアルが割り当てられる。 複数マテリアルを設定していれば、ここで複数マテリアルが設定される。

f:id:Tatsupuri:20201129164938p:plain:w500

抽出したマテリアルは、当然インスペクターからいつも通り設定することができる。

f:id:Tatsupuri:20201129164943p:plain:w500

Post Processing

今回は、ポストプロセスを用いて見た目を改善する。といっても、今回はBloom効果を使うだけにとどめる。

Hierarchy ViewからシーンにGlobal Volumeを追加する。Global Volumeオブジェクトに対してインスペクターからAdd Override でPost Processing 効果を追加できる。今回はBloom効果を用いる。見栄えが良くなるように適当にパラメータを調節する。

f:id:Tatsupuri:20201129164948p:plain:w500

ここでゲームビューにPost Processing効果を適用するためには、メインカメラの設定で、Rendering>Post Processingをオンにしておく必要がある。

f:id:Tatsupuri:20201129164953p:plain:w500

Visual Effect Graph

グラフの構成

今回は以下のように非常にシンプルなグラフで十分である。 やりたいことは、 OnPlayメッセージを受け取ったら適当な量の粒子を生成し、 その後適当に収束させ、最終的に消滅する というだけのエフェクトである。

f:id:Tatsupuri:20201129164907p:plain:w500

Awake時に自動で再生しないようにする方法

デフォルトのままではVFXオブジェクトは生成時に自動で再生されてしまう。これを避けるために以下のように設定を行う。VFXオブジェクトのインスペクターから、Visual Effect > GeneralのInitial Event Nameチェックボックスをオンにして、かつその右側のフィールドを空白にする。

こうすることでオブジェクト生成時にOnPlayが送られることがなくなる。

f:id:Tatsupuri:20201129164910p:plain:w500

同じことは一応ここにも書いてた。

スクリプトから制御

基本的に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);
    }

}