この記事は DeNA Advent Calendar 2016 2日目の記事です。
初めまして。DeNAでUnity基盤開発をしている、大竹(a.k.a. trapezoid)と申します。UniRxおじさんも兼業していたりします。 今回は、UnityでのNative Plugin開発の流れや勘所について紹介していこうと思います。
まえおき
Unityは非常に多彩なプラットフォームにほぼワンソースで対応出来る優秀なマルチプラットフォームゲームエンジンです。 Unityでゲームを作っていく場合、基本的にはC#でそのコードを記述していくわけですが、プラットフォーム固有の込み入った処理を書きたい場合や、高い性能が求められるような場合、Native Pluginとしてその処理を記述して、Unity本体とインテグレーションするような手法を取る必要があります。
DeNAでは、オーディオエンジンや同期通信用の内製ミドルウェアのSDKなど、ゲームエンジンに関わらず使われる独立したミドルウェアは、原則的にネイティブ開発したものをUnityにインテグレーションする手法をとっているものが多いため、積極的にNativePluginを開発/利用していっています。
Native Pluginをつくる
Native Pluginの形態
モバイルゲームにおいてであれば、やはりまずサポートされるべきターゲットはiOS/Androidとなります。 プラットフォームとC#実行エンジンの選択に従って、Native Pluginをどのようにゲーム本体とリンクするべきかは、以下のように変わります。
OS | 実行エンジン | リンク形態 |
---|---|---|
iOS | Mono | Static Library |
iOS | IL2CPP | Static Library |
Android | Mono | Shared Library, jar(JNI経由の呼び出し) |
Android | IL2CPP |
iOSは基本的にStatic Libraryを作ってリンクします。これはiOS上では外部ライブラリの利用はスタティックリンクしか規約上許可されていないため、このような制約がかかっています。
Androidの場合は、Shared Libraryによるリンクと、JNI経由でJVM上のコードを呼び出す事が可能です。
IL2CPPを利用する場合は、iOSと同様にStatic Libraryを利用することができます。
(訂正:2016/12/02)
Unityフォーラムの投稿
によれば、Android IL2CPPの場合、Static Libraryはサポートされてないようです。
Native Pluginとしてビルドする
上記の前提にのっとって、まずはNative側の実装を作って、Unityから繋ぎこむべき先のライブラリをビルドします。 Androidの場合はAndroid NDKを、iOSの場合はXcodeを使っていくのが手軽です。cmakeを使うことで一元化することも出来ますが、これはこれで話が長くなるので今回は割愛します。
Native PluginとC#をつなぎこむ
https://docs.unity3d.com/ja/current/Manual/NativePlugins.html 基本的なことや具体的なコードは上記のUnity公式ドキュメントに記載されていますので割愛しますが、UnityのNative Pluginとのインターフェースは、.NETのP/Invokeの仕組みにのっとって行われます。 P/Invokeではマーシャリング手法を引数や返り値, 型毎に細かく指定する事が出来るので、コピー頻度やメモリアロケーション頻度を下げられるように気を使いながら、C#上からP/Invoke宣言を記述していきます。 StructLayout等を利用することも可能ですが、特にiOSの場合64bitアーキテクチャと32bitアーキテクチャに両対応する必要があるため、アライメント境界には強く気を使う必要があります。
EditorでもNative Pluginを使う
Unityを使った開発の強みは、Unity Editorによって動作確認が出来ることによる、開発サイクルの加速にあります。 そのために、Native Pluginによってゲーム中の機能を実現する場合、Unity Editorの内部でも、可能な範囲で動作確認が可能であるべきです。 つまり、Android/iOSに加えて、WindowsやmacOSに対してもマルチプラットフォームサポートを行う事が、実質的にはスタートラインになります。
UnityではmacOSはLoadable Bundle, WindowsはDLLの形で、それぞれネイティブプラグインを利用可能です。 Windowsは単純にDLLを作るだけなので一旦省くとして、Loadable Bundleは馴染みがない方も多いとは思います。Loadable Bundleを作るには、まず以下のようなInfo.plistを任意のパスに保存しましょう。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>CompanyName.$(PRODUCT_NAME:rfc1034identifier)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016年 Your Name. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
次に、cmakeでコンパイル対象や必要なライブラリを指定した上で、 先程のInfo.plistを相対パスで指定する形で、ターゲットに対して(BuildTargetNameは適宜読み替えてください)cmake上の以下のようなプロパティ設定を追加します。
set_target_properties(BuildTargetName PROPERTIES
BUNDLE TRUE
MACOSX_BUNDLE TRUE
BUNDLE_EXTENSION bundle
MACOSX_BUNDLE_INFO_PLIST ./Path/To/LoadableBundleResource/Info.plist
)
こうした上で
cmake . -G"Xcode"
でcmakeを走らせれば、Loadable Bundleを生成できるxcodeprojを吐き出せます。(もちろん、手でXcodeからxcodeprojを生成してもかまいません) Windows/OSX共に、利用するUnity Editorのアーキテクチャに合わせて適宜64bitにするのを忘れないようにしましょう。
Native Pluginを配置する
ビルドした各Platform向けのNative PluginをUnityプロジェクト上のAssets/Plugins以下に配置します。 Inspectorから、それぞれのバイナリがどのプラットフォーム向けのものなのかを指定しておけば、完了です。
プラクティス
複雑な構造を手軽にマーシャリングする
非Bittableな値を多く転送するのであれば、どのみちマーシャリングコストはかかるので、マルチプラットフォームで高速なIDL定義型のシリアライゼーションライブラリを利用していくのも手です。代表例としてはProtocolBuffersやFlatbuffersが上がります。 この手のIDL定義型のシリアライゼーションライブラリは、IDLから各言語のパーサコードを生成してアクセスする形式のため、マーシャリングに関わる実装コストをざっくりと押しつけつつ、比較的安全にManaged-Native間のやりとりを行う事ができます。 連続的なメモリアドレスにパラメータを積み込めるので、バッチ的に複数の処理を一気に転送する、というパターンを行いたい場合にも有効です。
Native Pluginでの諸注意
Native Pluginの利用に関しては、いくつか注意するべき点がありますが、今回は
- Native側からManagedのメソッドをコールバックとして呼び出させる場合、必ずUnityのメインスレッド内から呼び出すようにする
- Editor上でのPreview再生の停止をしても、Native Pluginはアンロードされない
この二点について説明します。
コールバックは必ずUnityのメインループ内から呼び出す
Unityは多くの処理がメインスレッド、メインループ(Update, LateUpdateなど、Unityがエントリポイントとしてメインスレッド上から呼んでくれるメソッドから連なるコールスタック)から行われることが前提にされています。 uGUIなどは、CanvasRendererとの都合でこの外側からメッシュが編集される処理が走った場合、容易にクラッシュに至ります。 また、Editor上でNativePluginを使う場合には、別スレッドからコールバックが一度でも呼ばれてしまうと、以後MonoDevelop/VisualStudioからのデバッガーアタッチを行ったときにUnityがフリーズしてしまいます。(UnityのMonoのDebuggerのバグに起因しているようです) これらの事情から、コールバックは必ずUnityのメインループの内部から発火されるようにするのがベターです
Editor上でのPreview再生の停止をしても、Native Pluginはアンロードされない
これも非常に厄介なポイントなのですが、Editor上のNative Pluginは、一度ロードされるとPreview再生/停止をするだけでは初期化されません。 再生後に最初の呼び出しで初期化し、再生終了時には終端処理を必ず行うようにする必要があります。さらに、再生中にC#コードを編集してリコンパイルとアセンブリリロードが走った場合、終端処理がきちんと呼び出される保証もなくなるので、初期化処理は冪等性を保っておき、何度呼び出しても支障はない状態にしておく必要があります。
さいごに
長くなりましたが、UnityのNative Pluginを作る方法やその注意点について紹介していきました。
明日は hirashunshun さんです。
- 2016/12/02 Android IL2CPPでのサポートされるリンク手法について追記と修正をしました。
最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。