概要
KLab Engineer Advent Calendar 2021 12月9日の記事です。
開発推進部の庄司です。お仕事では、スマートフォンゲーム開発案件のサポートをしています。
「MIDI Plugin for Mobile Devices」というUnityアセットを作成して、アセットストアにリリースしてみましたので、その経緯や手順を共有します。
MIDIについて
いきなりの余談ですが、「MIDI」ってご存知でしょうか? とってもレガシーな規格(1983年)です。Music Instruments Digital Interfaceの略で、要は楽器同士や、楽器とコンピュータとを接続するための規格です。時代がかった規格ではありますが、現在でもいろんなところで使われています。ゲームサウンドの制作過程(DTM環境)でも使われていますし、ゲーム開発で使われるサウンドフレームワークの制御の仕組みなども、MIDIイベントメッセージの設計思想がベースになっているものが多いです。 参考:audiokinetic WwiseのAPI統合事例
経緯
プライベートで「MIDIを扱うためのAndroidライブラリ」というのを開発しています。
業務でUnityを扱うようになり、「このライブラリってもしかしてUnityでも動かせるのでは?」と思ったところから、プライベートプロジェクトが久々に進捗しはじめました。
実際やってみたところUnityでも動かせました。「電子ピアノの鍵盤を叩くとUnityアプリのログに音程や音量の数値が出た」 …興奮しませんか? そこから音を鳴らす実装を入れるだけでシンセサイザーが出来るんですよ!
ところで、Unityアプリと楽器との通信ができるということは「VRアプリにも使える」ということになります。VR世界からリアル世界の楽器にアクセスできたら、(もしくはその逆でも、)面白い体験が得られるのでは?というワクワク感がありますね!そこでOculus Quest2を購入してVRアプリを適当に書いてみました。実際、電子ピアノを演奏したらVR空間の空から球が降ってくる、みたいなサンプルVRアプリを書いてみたのですが、ただそれだけのシンプルなアプリですが割と楽しめました。
ここまで来たら、折角なのでUnity Asset Storeにアセットを並べてみたいという欲求が出てきたので、挑戦してみることにしました。
市場調査
Asset Storeで公開するのはいいのですが、どうせなら値段を付けてみたい。既存の有料アセットがどのくらいの値段が妥当なのか調べよう。…ということでAsset Storeを検索してみたところ、PCなど多プラットフォームに対応しているものが見つかりました。70ドルでもそこそこ売れるのか… モバイル向け(iOS/Android)しか対応しない分、安くすれば対抗プロダクトになるんじゃないだろうか? ということで25ドルに決めました。
Unityアセット開発
ネイティブプラグインを含むUnityのアセットパッケージを開発してゆきます。
Unityビルドの環境について
UnityのLTSとなっているバージョン(現状では2018, 2019, 2020)の最新版を使ってビルドします。Unity Hubを使うと、複数バージョンのインストールや最新版への追従が楽で便利です。
対応予定の各プラットフォーム向けにビルドして、それぞれ機能を一通り使用できることを確認できていれば良いでしょう。今回は2プラットフォーム(iOS, Android) × Unityバージョン3種類、計6バイナリを作成して動作検証しました。
アセットストアに送信するアセット作成は、LTSの最新年版(現状ではUnity 2020)で行うと良いでしょう。ちなみに、アセット審査1回目ではUnity 2018(LTSの下限)でビルドしたバイナリだったのですが、なぜかサブミット後に即リジェクトされました。Unity 2020でビルドし直すとOKでした。
Unityバージョン間の差異を解消
バージョンアップで挙動や仕様が変わっている箇所があったりするので、 #if UNITY_2019_4_OR_NEWER
~ #endif
のようなシンボルで区切って実装を切り分けています。
#if UNITY_2019_4_OR_NEWER
project.AddFrameworkToProject(project.GetUnityFrameworkTargetGuid(), "CoreMIDI.framework", true);
project.AddFrameworkToProject(project.GetUnityFrameworkTargetGuid(), "CoreAudioKit.framework", true);
#else
project.AddFrameworkToProject(project.TargetGuidByName("Unity-iPhone"), "CoreMIDI.framework", true);
project.AddFrameworkToProject(project.TargetGuidByName("Unity-iPhone"), "CoreAudioKit.framework", true);
#endif
今回はUnity 2019.4にてiOSのPostProcessBuild周りでPBXProject.GetUnityFrameworkTargetGuid()を使うようになったので、以降のバージョンでそちらを呼び出すように対応しました。
Unityでのイベント処理
↓Android側プラグイン実装から、メッセージ送信部分の抜粋です。
public class UsbMidiUnityPlugin {
private static final String GAME_OBJECT_NAME = "MidiManager";
private UsbMidiDriver usbMidiDriver;
public void initialize(Context context) {
usbMidiDriver = new UsbMidiDriver(context) {
...中略...
@Override
public void onMidiNoteOn(@NonNull final MidiInputDevice sender, int cable, int channel, int note, int velocity) {
UnityPlayer.UnitySendMessage(GAME_OBJECT_NAME, "OnMidiNoteOn", serializeMidiMessage(sender.getDeviceAddress(), new int[] {cable, channel, note, velocity}));
}
}
}
}
Unity内の「MidiManager」という名前のGameObjectに対して、ネイティブプラグインから「UnitySendMessage」メソッドを使ってメッセージを送ることによって、メッセージイベントをネイティブ側からUnity側へと通知しています。
扱えるメッセージはテキスト(文字列)です。上記コードの serializeMidiMessage
メソッド(自作)で文字列化し、Unity側で再度数値などに変換しています。
↓Unity C#側のイベント処理実装の抜粋です。
private void OnMidiNoteOn(string midiMessage)
{
var eventData = new MidiEventData(midiMessage, EventSystem.current);
foreach (var midiDeviceEventHandler in midiDeviceEventHandlers)
{
if (!ExecuteEvents.CanHandleEvent<IMidiNoteOnEventHandler>(midiDeviceEventHandler))
{
continue;
}
ExecuteEvents.Execute<IMidiNoteOnEventHandler>(midiDeviceEventHandler,
eventData, (eventHandler, baseEventData) =>
{
if (baseEventData is MidiEventData midiEventData)
{
var parsed = DeserializeMidiMessage(midiEventData.MidiMessage);
eventHandler.OnMidiNoteOn(parsed.DeviceId, (int) parsed.Messages[0],
(int) parsed.Messages[1], (int) parsed.Messages[2], (int) parsed.Messages[3]);
}
});
}
}
Unity側でメッセージを受信したら DeserializeMidiMessage
メソッド(自作)で文字列→数値の変換を行っています。
UnityEngine.EventSystems 名前空間のクラスを利用して、MidiManagerに登録した全てのGameObjectに対して同時にイベント情報を伝達しています。
Unityからネイティブへのメッセージ送信は、単にネイティブのメソッドを呼ぶだけです。
Androidネイティブプラグイン開発
USB MIDIとBluetooth MIDIのライブラリに機能追加する、という形で実装しました。
既存のライブラリとして全機能が完成していたので、Unityへの情報伝達部分だけを追加するという作業になりました。
成果物: USB MIDIのUnity連動を行うコード, Bluetooth MIDIのUnity連動を行うコード
ネイティブプラグイン側から、UnityのJava側実装(com.unity3d.player.UnityPlayer クラスの UnitySendMessage メソッド)を呼び出さないといけないのですが、これはUnity Editorのclasses.jarファイルに含まれています。おそらくプロプライエタリなライセンスのパッケージで、OSSプロジェクトに含めるわけにはいかず悩み所でした。
色々と試した結果、
- 同一パッケージに同名のMockクラス(空実装のメソッドが定義されている)を用意して
-
compileOnly
なライブラリモジュールとしてビルド - これをネイティブプラグインのモジュール側から呼ぶようにする
とすることで、Unity製の実クラスを含めない状態でUnityプラグインをビルドすることができました。ビルド時はMockクラスを参照させてコンパイルを通し、実行時はUnityの本物のクラスを呼び出す、というからくりです。
参考までに、Mockクラスは UnityPlayer.java、Gradle設定の記述箇所は build.gradle#L14です。
このほか、権限 BLUETOOTH
, BLUETOOTH_ADMIN
, ACCESS_FINE_LOCATION
、機能 bluetooth_le
を追加する必要があるため、PostProcessBuild で System.Xml.XmlDocument クラスを使って AndroidManifest.xml
ファイルを書き換えています。
PostProcessの実装にあたっては、↓このあたりの情報が参考になります。
#if UNITY_ANDROID
public class ModifyAndroidManifest : IPostGenerateGradleAndroidProject
{
public void OnPostGenerateGradleAndroidProject(string basePath)
{
var androidManifest = new AndroidManifest(GetManifestPath(basePath));
androidManifest.SetPermission("android.permission.BLUETOOTH");
androidManifest.SetPermission("android.permission.BLUETOOTH_ADMIN");
androidManifest.SetPermission("android.permission.ACCESS_FINE_LOCATION");
androidManifest.SetFeature("android.hardware.bluetooth_le");
androidManifest.Save();
}
}
#endif
iOSネイティブプラグイン開発
iOSのプラグインはCore MIDIのAPIを調べつつフルスクラッチで実装しました。Bluetooth MIDIとApple Network MIDIに対応してみました。調べていると既にCore MIDIはMIDI 2.0に対応していたりして、流石Appleだなぁと感心しました。
iOSプラグインを開発するためMacの環境を用意しました。手元には古いOSしかなかったのですが、古いXcodeで最新のiOSデバイスにアプリをインストールするツール iOS-DeviceSupport を入れることで開発できました。
(最新のSDKのAPIを使いたい場合は、MacのOSとXcodeを更新する必要があります。古いMacに新しいOSを入れるツールは「OS名 patcher」でググると出てきますが、ミスをした場合は最悪OSがbootしなくなるので、自己責任でお願いします)
今回はCoreMIDI.frameworkとCoreAudioKit.frameworkをバンドルする必要があるため、PostProcessBuild を使ってXcodeプロジェクトを書き換えています。Bluetoothデバイスの利用確認UIに表示するための文言 NSBluetoothAlwaysUsageDescription
も追加します。
Unityのドキュメントが参考になります。
#if UNITY_IOS
public class PostProcessBuild
{
[PostProcessBuild(99)]
private static void OnPostProcess(BuildTarget target, string pathToBuildProject)
{
if (target == BuildTarget.iOS)
{
var project = new PBXProject();
var pbxProjectPath = PBXProject.GetPBXProjectPath(pathToBuildProject);
project.ReadFromFile(pbxProjectPath);
project.AddFrameworkToProject(project.GetUnityFrameworkTargetGuid(), "CoreMIDI.framework", true);
project.AddFrameworkToProject(project.GetUnityFrameworkTargetGuid(), "CoreAudioKit.framework", true);
project.WriteToFile(pbxProjectPath);
var infoPlist = new PlistDocument();
var infoPlistPath = pathToBuildProject + "/Info.plist";
infoPlist.ReadFromFile(infoPlistPath);
if (infoPlist.root["NSBluetoothAlwaysUsageDescription"] == null)
{
infoPlist.root.SetString("NSBluetoothAlwaysUsageDescription", "Uses for connecting BLE MIDI devices");
infoPlist.WriteToFile(infoPlistPath);
}
}
}
}
#endif
実装のコードを隠す必要は特にないのですが、ライブラリ形式にして.aとしてPlugins/iOSに配備しています。プラグインの実装の全コードはgithubの Unity-MIDI-Plugin-iOS リポジトリにあります。
ドキュメント作成
PDFもしくはRTFで記述する必要があるようです。Googleドキュメントで作成してPDFにエクスポートしました。
ドキュメントの構成としては以下のような感じです。
- 目次
- アセットの概要
- インストール方法の説明
- ビルド後処理(PostProcessing)の説明
- アセットの使い方の詳しい説明
- 初期化の処理
- 終了時の処理
- イベント処理の実装
- MIDIメッセージ送信の実装
- MIDIメッセージ受信の実装
- 検証したデバイス・環境の一覧
- バージョン履歴
- 連絡先(メールアドレス、githubのリンク、関連OSSの情報)
ディレクトリ構成
プロジェクトのディレクトリ構成は下記の通りです。ネイティブプラグインを伴うアセットを提供する場合、類似した構成になると思います。「MIDI」ディレクトリのところをアセット名で読み替える形になるかと。
- Assets/AssetStoreTools
- アセットストアにアップロードするためのツール。アセット成果物には含めません。
- Assets/MIDI
- アセット成果物に含める全てのファイルをこのディレクトリ以下の階層に置きます。(今回はMIDIというアセット名にしました)
- ドキュメントPDFをここに置きます。
- Assets/MIDI/Plugins/Android
- Androidネイティブプラグイン
- Assets/MIDI/Plugins/iOS
- iOSネイティブプラグイン
- Assets/MIDI/Samples
- サンプルアプリ実装
- Assets/MIDI/Scripts
- プラグインC#側実装
- Assets/MIDI/Scripts/Editor
- iOS向けPostProcessBuild実装
Unity Asset Storeへの登録
Unity Asset Storeにアセットを登録したときにメモしていたものです。
参考:Unity公式のガイドライン
アセット情報の登録
- 「新規アセット」を登録します
- アセットの情報を埋めていきます
- 各種画像を準備します
- What makes a great key image and are there any size restrictions? という記事があったので参考にしました。
- 各画像での、共通の禁止事項
- Unityロゴを入れるのはNG
- 「セール」のバナーを入れるのはNG
- デフォルトのUnity Skyboxを使っているものはNG
- ボケていたり、縮尺が違ったり、切り取られた画像はNG
- Unity EditorのスクリーンショットはNG
- 共通の必須事項
- それぞれの画像サイズに合わせたレイアウト
- 必要な画像
- アイコン:160x160
- 80x80で表示されたときに、デザインが潰れたり視認性が損なわれないこと
- カバー:1950x1300
- 商品詳細の画面で表示される
- 含んでも良いもの
- アセットのタイトル
- ロゴ、配布者の名前
- タグライン
- カード:420x280
- Asset Storeのリストに表示されるサムネイル画像
- カバー画像とアスペクト比は同じ
- アイコン:160x160
- テキストを準備します
- アセットの紹介文
- Englishがベースになり、日本語、中国語、韓国語の翻訳が追加できる
- いくつかのHTMLタグが利用できる
- 紹介文に含めたもの(※参考)
- アセットの概要
- アセットの提供する機能の一覧を箇条書き
- アセットの紹介文
- 価格を設定します
- 有料か無料を選択
- 有料の場合、4.99ドル以上である必要があります
- 価格変更はバージョン更新時に行えます
- 対応Unityバージョン、プラットフォームの選択
- バージョン:2018, 2020など
- プラットフォーム:iOS/Androidなど
アセットのアップロード
- アセットのプロジェクトに、Asset Store Toolsアセットを導入します
- インストール後、同パッケージの更新があれば更新しておきます
- アップロードするディレクトリを選択し、Validationツールに掛けます
- 問題があればメッセージを確認して解消します
- Validationに問題なければ、アップロード先のアセットを選んで、アップロードします
- アップロード後、Asset Store Publisher(Webサイト側)で「Review」ボタンを押します
- チェックボックスが2つ出てくるので選びます
- レビュー通過後、ストアに自動で反映させるか、手動でやるかを選択するためのチェックボックス
- 「全てが自分の著作物である」ことを確認するためのチェックボックス(チェックしないと先に進めません)
- Reviewボタンを押した後、自動応答メールが来るので確認します
- 静的チェックで即NGになる場合があるので要確認です
- NGでもOKでも自動応答メールがきます
- レビューが終わるまで気長に待ちます
レビューの推移
Publisherページにレビュー待ちの数が表示されているので、日々メモっていたものをまとめてみます。
日付 | レビュー待ち数 | 備考 |
---|---|---|
07/02 | レビュー提出 | |
07/09 | 1195 | 1週後 |
07/16 | 903 | 2週後 |
07/23 | 297 | 3週後 |
07/30 | 118 | 4週後 レビューステータスが「Assigned」に変更 |
08/05 | - | レビューステータスが「Rejected」に変更 |
08/06 | - | レビューステータスが「Review Requested」に変更 |
08/07 | - | レビューステータスが「Resubmission is in the queue」に変更 |
08/11 | レビュー通過 |
レビュー期間、1ヶ月と少しで通過しました! 意外と長かったです。レビュー待ちが一気に減る週があったり、なぜか増える日もあったりして、どれくらい掛かるかは読みづらいです。
途中で一度リジェクトされていますが、これはドキュメントがテキストファイルだったのと、Webサイトのリンクが切れていたためでした。PDFのドキュメントを作成してアセット内に配備して、サイトURLを修正し、再サブミットして通過しました。
結果
リリースから3ヶ月ほど経過して、3つ売れ、2つWishlistに入り、1件問い合わせがありました。
プロモーションも何もしていないのでこんなもんかな、という感じです。ポツポツと月に1個くらいのペースで売れてくれたらいいですね。
今後の展望
現在はC#でMIDIファイルを再生・記録する機能を実装しているところです。来年の頭あたりにアップデート対応できたらと考えています。
あとはMIDI2.0なデバイスを入手して相互接続テストを実施したいです。