こんにちは、よこてです。Mirrativ のアバターは Unity で動いているという話をします。Mirrativ は iOS/Android の ライブ配信アプリですが、機能の一つとしてエモモ(アバター)があります。
これは Unity で動いているのですが Mirrativ そのものはネイティブのモバイルアプリです。意味がわかりませんね。具体的には
オレンジの部分がネイティブで実装されていて、青い部分がUnityで実装されています。わかりにくいですね。要するに 基本的にはネイティブ実装されていて、アバターの部分だけがUnityで実装されています 。
このように Mirrativ は ネイティブ実装とUnity実装のハイブリッド構成 になっています。これは歴史的経緯があるのですが、Mirrativ はモバイルのライブ配信サービスとして開発され運用されてきました。最初はアバターという機能はなかったため、普通のiOS/Androidのネイティブアプリだったわけです。その後にサービスグロースの過程において Mirrativ にアバターを導入することにしたのですが、 Mirrativ の既存の資産を活かして市場検証を最速で回したいというモチベーションがありました。このような背景があり、 native app + Unity という構成の導入が検討されました。
ネイティブアプリの資産を活かしつつ Unity を利用する方法はいくつか考えられますが、その一つに WebGLを使う方法があります。アバターとしての機能は WebGL という形で出力して、アプリ側からは WebView 経由で表示させるという方法です。Mirrativ のアバター導入検討時の最初期にはこれを実験したのですが、パフォーマンス上の問題があり断念しました。
今 Mirrativ では Unity をフレームワークとして扱い、ネイティブに埋め込むという Embedded Unity を採用しています。Unity 2019.3 では Unity as a library という形でサポートされる予定ですが、実はそれ以前のバージョンでも同じことができるのです。
Embedding Unity
Unity をネイティブアプリに組み込むにあたり考えるべきことは
- Unity の view の切り出し
- フレームワーク化(iOS なら .framework 、 Android なら .aar として出力)
- ネイティブアプリ(Swift/Kotlin)からの利用
まず重要なのは Unity が出力するプロジェクト は single view application である、ということです。Unity は一つの view を生成して全画面表示しているだけで、この view は iOS では単なるUIView
、 Android ではSurfaceView
です。つまり Unity の view はネイティブアプリがもつ他の view と全く同じように扱うことができます。
具体的には、 Unity の iOS ビルド時に export される Xcode プロジェクトは
main()
が Unity のエンジンを起動、またUIApplicationMain()
の呼び出しUnityAppController
がUIApplicationDelegate
を実装UnityGetGLView()
でUIView
を取得
となっていて
- Unity が通常実行する
main()
の中身をネイティブ側のコードから実行(かつUIApplicationMain()
を呼ばない) UnityAppController
をネイティブ側のapplication
に追加UnityGetGLView()
で得られる view をネイティブ側のもつ view にinsertSubview
とすれば Unity の view をネイティブな iOS アプリ上で動作させることができます。
ちなみに
int main(int argc, char* argv[]) { UnityInitStartupTime(); @autoreleasepool { UnityInitTrampoline(); UnityInitRuntime(argc, argv); RegisterMonoModules(); RegisterFeatures(); std::signal(SIGPIPE, SIG_IGN); UIApplicationMain(argc, argv, nil, [NSString stringWithUTF8String: AppControllerClassName]); } return 0; }
Unity のmain()
はこんな感じで実装されています。
iOS
ビルド
iOS のフレームワーク化はjiulongw/swift-unityの実装がほぼすべてなのですが簡単に説明していきます。
Unity が export する Xcode のプロジェクトはこのような構成になります。この中で特に重要なのは次の3つで
- Classes
- Unity のエンジンに相当
Classes/UnityAppController.mm
がUIApplicationDelegate
相当Classes/main.mm
が entry point 相当- これを外部から利用できる形にコードを修正する
- Libraries
- 主に .NET の周辺
- Data
- アプリに embed されるデータ群
基本的には bridging header を追加してこれらを framework としてビルドするだけです。 .framework が得られるため、他の iOS アプリからフレームワークとして利用することができます。
ネイティブからの利用
ネイティブ側からの利用については、特に重要な部分を抜粋すると
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @objc var currentUnityController: UnityAppController! func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { unity_init(CommandLine.argc, CommandLine.unsafeArgv) currentUnityController = UnityAppController() currentUnityController.application(application, didFinishLaunchingWithOptions: launchOptions) currentUnityController.applicationDidBecomeActive(application!) currentUnityController.applicationWillResignActive(application!) return true } func applicationWillResignActive(_ application: UIApplication) { currentUnityController.applicationWillResignActive(application) } func applicationDidBecomeActive(_ application: UIApplication) { currentUnityController.applicationDidBecomeActive(application) } }
unity_init()
はmain()
の置き換えになります。unity_init()
でエンジンを起動して、UnityAppController
をUIApplicationDelegate
として登録しています。applicationDidBecomeActive
を一度呼び出しているのは、UnityAppController
はapplicationDidBecomeActive
時点でグラフィックの初期化(= view の初期化)を行っているためで、明示的にこれを実行しています。
また、view の利用は
class ViewController: UIViewController { func showUnitySubView() { if let unityView = UnityGetGLView() { view?.insertSubview(unityView, at: 0) unityView.translatesAutoresizingMaskIntoConstraints = false let views = ["view": unityView] let w = NSLayoutConstraint.constraints(withVisualFormat: "|-0-[view]-0-|", options: [], metrics: nil, views: views) let h = NSLayoutConstraint.constraints(withVisualFormat: "V:|-75-[view]-0-|", options: [], metrics: nil, views: views) view.addConstraints(w + h) } } }
適当なUIViewController
にinsertSubview
することを考えます。UnityGetGLView()
でもらえるUIView
をinsertSubview
するだけです。実際にはapplicationDidBecomeActive
から呼び出されるグラフィックの初期化を待った上でshowUnitySubView
を呼び出す必要があります。
メッセージング
ネイティブから Unity に対してはSendMessage
を利用できます。
void UnitySendMessage(const char* obj, const char* method, const char* msg);
Unity からネイティブに対しては native plugin を利用できます。文字列を渡すだけなら
#ifndef UnityPlayerToIOS_h #define UnityPlayerToIOS_h @protocol UnityCallback <NSObject> @optional @required - (void)receiveMessage: (NSString *)msg; @end @interface UnityPlayerToIOS : NSObject @property (class, nonatomic) id <UnityCallback> receiver; + (void)sendMessage: (NSString *)msg; @property (class, nonatomic) id <UnityCallback> callbackReceiver; + (void)sendCallbackMessage: (NSString *)msg; @end #endif
#import "UnityPlayerToIOS.h" static id <UnityCallback> _receiver = nil; @implementation UnityPlayerToIOS + (id <UnityCallback>) receiver { return _receiver; } + (void)setReceiver: (id <UnityCallback>)receiver { _receiver = receiver; } + (void)sendMessage: (NSString *)msg { if (_receiver) { [_receiver receiveMessage:msg]; } } @end extern "C" { void sendMessage(const char *msg) { [UnityPlayerToIOS sendMessage:[NSString stringWithCString: msg encoding:NSUTF8StringEncoding]]; } }
Unity からは文字列を流し込み、ネイティブ側はコールバックを受け取るようにします。
これら2つで双方向にテキストを流せるため、ネイティブからの呼び出しという形で Unity 側のAPIを構築することができます。
Android
ビルド
こちらの記事がほぼすべてになります。Android のライブラリ作成は iOS と比べて簡単で、 AndroidManifest.xml と build.gradle を書き換えるだけでライブラリを作ることができます。
AndroidManifest.xml から application
タグを消します。
<!-- <application android:icon="@mipmap/app_icon" android:label="@string/app_name" android:isGame="true" android:banner="@drawable/app_banner" android:theme="@style/UnityThemeSelector.Translucent"> ... </application> -->
build.gradle をlibrary
に対応します。
apply plugin: 'com.android.library'
これだけです。あとはメニューからビルドをポチっとするだけで .aar が作成されます。
ネイティブからの利用
Unity のライブラリにはエンジンをラップする形でUnityPlayer
という view があり、UnityPlayerActivity
がこれをもっています。UnityPlayer
を使う最も手っ取り早い方法はUnityPlayerActivity
を継承してしまうことで
class UnityActivity : UnityPlayerActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_unity) frame.addView(mUnityPlayer) } }
これだけで Unity を埋め込んだ形で使うことができます。ただし、 iOS では view を生成する部分のコードが直接出力されるため view の不透明/半透明を切り替えるのはそれほど難しくありませんが、Android で同じことをやろうとすると多少の工夫が必要になります。UnityPlayer
の実体はSurfaceView
ですがこれに直接触る手段がないため
var view = mUnityPlayer.view var field = view.javaClass.getDeclaredField("s") field.isAccessible = true var sf = field.get(view) as SurfaceView sf.setZOrderOnTop(true) sf.holder.setFormat(PixelFormat.TRANSPARENT)
このようにリフレクションでSurfaceView
を引っ張ってくる必要があります(フィールド名はUnityのバージョンによって変わります)。
メッセージング
iOS と同様にネイティブから Unity に渡すときはSendMessage
UnityPlayer.UnitySendMessage(gameObject, method, msg)
Unity からネイティブにわたすときはコールバックを使います。
public class UnityPlayerToAndroid { public interface ICallback { public void receiveMessage(String message); } public static ICallback receiver = null; public void sendMessage(String message) { if (receiver != null) { receiver.receiveMessage(message); } } public static ICallback callbackReceiver = null; public void sendCallbackMessage(String message) { if (callbackReceiver != null) { callbackReceiver.receiveMessage(message); } } }
おわりに
だいたいこんな感じで iOS/Android のネイティブアプリに Unity を組み込むことができます。
現実の運用ではCDが必要になりますが、 Cloud Buildのような便利ツールはないので自前で作ります。 正直、ビルドする方法探すよりこっちのほうが骨が折れます。 またフレームワーク単体だとUnityエディタ上で動作確認が一切行えないため、開発用のシーンを別途作ってあるのですが、フレームワーク自体に操作するためのUIがないためこちらもそれなりに作り込む必要があります。
Unity をネイティブアプリのフレームワークとして扱うことが有用なケースとしては、アプリのコアな機能がネイティブ側にある場合で 3D を扱う必要がある場合には検討しても良いのではないでしょうか。 3D である程度リッチな機能を高速につくろうとすると Unity が圧倒的に優秀な一方、 Unity はあくまでゲームエンジンなのでネイティブアプリのような構成でつくるのは必ずしも得意というわけではないからです。 Mirrativ はこれに相当します。ただし、開発・運用は複雑になり細かいハマりポイントも多くあるためあまり安易に導入するのは現状としてはオススメできません。
今後の展望としては 2019.3 でサポートされる形への乗り換えを考えています。単純に Unity のバージョンを最新に保ちたいというのが理由ですが、現状 Unity のバージョンを更新するたびに 微妙な不具合が毎回出る (普通にビルドした場合は起きない)というのが観測されていてつらいなーと思っています。そういうわけで、公式がサポートしてくれるのは 仕事がなくなって嬉しい 心強いなーと思っています。
We are hiring!
ミラティブでは Unity を hack しながらエモモを一緒に作ってくれるエンジニアを募集中です! 体験入社という制度もあるのでお気軽にどうぞ!