Mirrativ tech blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc...)によるブログです

Mirrativ×Unity as a Library 活用事例と開発テクニック

こんにちは、Unityエンジニアの菅谷です。今回はUnity as a Libraryの活用事例と開発テクニックとして、以前のLTで紹介した内容をまとめました。Mirrativでの活用事例を通してUnity as a Libraryの強みや課題を共有し、他のプロダクトでの導入の参考になれば幸いです。2021/6〜2021/7にMirrativ×Unity as a Libraryのテーマで行った3本のLTをまとめた内容となっています。

mirrativ.connpass.com

meetup.unity3d.jp

mirrativ.connpass.com

MirrativにおけるUnity

Mirrativにはエモモという配信をサポートする3Dアバターが存在します。ユーザーは衣装やアクセサリーなどのアイテムを組み合わせてエモモを着飾ることができます。MirrativはiOSはSwift、AndroidはKotlinのネイティブアプリとして開発していますが、エモモはUnityで開発しています。つまりMirrativはネイティブとUnityが融合したアプリとなっています。iOS/AndroidのネイテイブアプリでありながらUnityの機能も使えるため、Unityが得意とする3Dモデルの扱いやリッチなグラフィックが実現できます。この技術のバックグラウンドとしてはUnity as a Libraryを活用しています。

Unity as a Library(UaaL)とは

Unity as a Library(UaaL)はUnityをネイティブアプリのライブラリとして利用する技術で、アプリで作ったViewとUnityで作ったViewを共存させられます。Unity2019.3で正式サポートとなりましたが、MirrativではUnity2018.2から独自で使用していました。Unity2018.2からUnity2019.3へのアップデートの話は過去の記事にて紹介しています。

tech.mirrativ.stream

UaaLで作るメリット

UaaLで作ることでネイティブ、Unityそれぞれの得意領域を1つのアプリ内に共存させられます。ネイティブの強みは配信のための安定したライブラリが使えます。Unityの強みは3Dを簡単に使える仕組みが整っていることや、Unityエディタでのデザイナー向けの開発機能が充実していることがあげられます。

UaaLにおいてUnityとネイティブの役割分担には一長一短ありますが、Mirrativではエモモなど3Dの描画部分とミニゲームの機能のみにUnityを利用しています。サーバーとの通信やミニゲーム以外でのUIの表示などネイティブで実装できるものはできるだけネイティブで行うようにしています。Unityの実装部分に機能をもたせすぎるとネイティブとUnityとで複雑な連携が必要になったり、内部で持つ衣装アイテムなどのパラメータが重複し管理しにくくなったりするため、できるだけUnityはViewの役割に制限することでネイティブからコントールしやすくしています。

Mirrativは歴史的経緯もあり、iOS/Androidでのネイティブアプリの上にあとからUnityで実装したエモモの機能を乗せました。UaaLでは既存のネイティブアプリを作り直さずにUnityの機能を追加できるのも特徴の一つです。3Dを使ったエモモのカスタマイズや豪華なギフトの演出はUnityなしでは実現できませんでした。また、Unityを用いることでアプリのビルドなしにデザイナーのみで演出を作ることができ、更にはUnityのCinemachineやTimelineなど演出に特化した機能を使った開発もできます。豪華なギフト演出については別の記事にて解説しています。

tech.mirrativ.stream

MirrativにおけるUaaLの例とViewの構成

MirrativでのUaaL利用の例を紹介します。 配信画面では、Unityでレンダリングされたエモモの表示は画面全体を覆う1つのViewになっており、その上にネイティブで実装している配信のコメントやUIが存在しています。

配信画面では画面全体にエモモを写すUnityViewがあり、それ以外のコメントやUI(赤枠)がUnityViewの上に乗っています。

エモモの着せ替え画面では、エモモの表示は画面上部のみとなおっており、画面下部の着せ替えUIはネイティブで表示しています。着せ替え画面のようにUnityのViewとネイティブのViewを独立して設置することもできます。

着せ替え画面では画面上部のUnityView(黄枠)と画面下部の着せ替えUI(赤枠)がそれぞれ独立しています。

また、ユーザーのタッチやスワイプなどの入力はUnity側でも検知することができます。ただし、配信画面などUnityのViewの上にネイティブでUIを表示する場合は、干渉を避けるためにネイティブ側で入力を検知しUnityにそのイベントを送っています。また、Mirrativ内のミニゲームなどネイティブのUIがUnityのView上に乗っておらず、Unityの入力の仕組みを活用したほうがよい場合はUnity側で直接入力を検知するようにしています。

Mirrativ内でのオリジナルミニゲームの一つです。ミニゲームはエモモの着せ替え画面と同様にUnityのView(黄枠)とコメント(赤枠)が独立しているため、ミニゲーム内のUIはUnityで作成しタップなどの入力検知もUnity側で行っています。実際にはユーザーの入力ログはネイティブでも使用しているため、ネイティブ側にもイベントとして入力内容を送っています。

MirrativでのUaaLの処理の流れ

エモモを表示するまでのフローを例としてUaaLの動きを紹介します。

  1. ネイティブでの実装部分がエモモの衣装リストをサーバーから取得します。
  2. ネイティブ部分が取得した衣装リストを元にUnityでの実装部分にパラメータとして渡します。
  3. Unity側が受け取ったパラメータを元にアセットバンドルをロードし、3Dモデルの衣装データの生成を行います。
  4. Unity側がネイティブプラグインを通してネイティブ側に衣装ロード完了の通知を送り、衣装ロードの処理を完了させます。

f:id:t_sugaya:20210824174708p:plain

このようにMirrativのUaaLはネイティブを仲介して動く仕組みになっています。

ネイティブからUnityへのメッセージング

ネイティブからUnityへのパラメータを渡す処理はSendMessageを用いて行われます。

  1. ネイティブから呼び出したいUnity(C#)側のメソッドを作成し公開する
  2. シーン上のオブジェクトにスクリプトをアタッチする
  3. ネイティブではsendMessageToGO(Swift)またはUnityPlayer.UnitySendMessage(Kotlin)によりUnityへメッセージングを行う

Unity側の実装はUnityでSendMessageを利用する方法と同じで、Mirrativでは専用のSendMessageアトリビュートを作成して外部から呼ばれるメソッドであることを明示しています。

以下は実際のエモモのロード処理です。Swift側でApiHandlerというUnityのオブジェクトを指定し、LoadAvatarModelメソッドを呼び出しています。その際に衣装リストなどをパラメータとして付与しています。

public class ApiHandler : MonoBehaviour {
    [SendMessage]
    public void LoadAvatarModel(string command) {
        実際のロード処理
    }

Unity(C#)側のコードです。上記のスクリプトはシーン上のApiHanderというGameObjectにアタッチしておきます。

UnityFramework.getInstance()?.sendMessageToGO(
    withName:       “ApiHandler”,
    functionName:   “LoadAvatarModel”,
    message:        “パラメータ”)

iOS(swift)側のコードです。シーン上のオブジェクト名、Unity側のメソッド名、付与するパラメータを指定します。

MirrativではUnityで機能を作るたびにメソッドを作成しネイティブからそれぞれ呼び出していましたが、パラメータの形式が統一されておらず混乱の元になっていました。最近はパラメータのフォーマット統一化を進めており、その中でネイティブからは1つのメソッドのみを呼び出すようにし、パラメータ内で指定したメソッド名を元にそれぞれの機能を実行するよう仕組みを整えています。ネイティブから呼ばれる入り口を1つに絞ることでエラー処理やログ出力など、共通の処理が入れやすくなりました。これらの設計については一長一短があるため日々議論と改善を行っています。

Unityからネイティブへのメッセージング

Unityからネイティブへのメッセージングはネイティブプラグイン経由で行われます。Mirrativでは衣装ロード完了のコールバックなどUnity側で処理が完了した場合や、ミニゲーム中のUnity側のボタン入力イベントなどのイベント発行のタイミングで必要に応じてネイティブ側にメッセージングを行っています。

[DllImport("__Internal")]
static extern void sendMessage(string msg);

public void SendMessage(string msg)
{
    sendMessage(msg);
}

Unity(C#)側のコードです。ネイティブプラグインでsendMessage関数を用意しておきます。また、実際のネイティブプラグイン内ではsendMessageがreceiveMessageというメソッドを呼び出します。

extension Unity: UnityCallback {
    public func receiveMessage(message: String!) {
       messageの中身に応じて処理を行う 
    }

iOS(Swift)側のコードです。Unityから送られたパラメータをmessageとして受け取り、その内容によって処理を分岐させるようにしています。

Unityとネイティブとのメッセージングに関して、より詳しい内容は以前の記事で解説しています。

tech.mirrativ.stream

UaaLでの開発の難しさとミラティブでの取り組み

UaaLはネイティブとUnity両方のいいとこ取りができますが、特有の開発の難しさも多数存在します。Mirrativの開発で感じた難しさとその解決に向けた取り組みについて紹介します。

UaaL特有の課題や問題

UaaLを利用した際の特有の開発の難しさや複雑さがあります。

UaaLではUnityのライブラリとしてのビルドとネイティブアプリのビルドの両方のビルドが必要になります。更にはUnityのライブラリをネイティブアプリが取り込むため、アプリとして動かすためのフローが多くなります。対策としてミラティブではUnityとネイティブアプリそれぞれのCI/CDを整えてビルドを簡単に行えるようにしています。

また、Unityとネイティブアプリの連携部分の開発も必要になります。2つが独立したシステムになってしまうためUnity側に問題があるのか、ネイティブアプリ側に問題があるのかの調査が難しくなります。ミラティブではネイティブエンジニアとUnityエンジニアはそれぞれ別メンバーであるため、お互いのコミュニケーションも重要です。安定した開発の仕組みづくりのために以下のような対策をしています。

  • Unity上でのシミュレーションを充実させてネイティブアプリを使用しなくても動作が確認できるチェッカーの作成(後述)
  • プロトタイプの作成時などはUnity単体でアプリとしてビルドし実機で動かす
  • FirebaseやUnity Cloud Diagnosticsなどのパフォーマンス計測ツールの活用
  • Flipperなどのデバッグ用ツールの導入
  • 日々の定例やSlack・口頭でのコミュニケーションを密に行い連携を図る

Flipperの導入は以前の記事にて紹介しています。

【Android】デバッグツールを変更して開発体験を向上する - Mirrativ tech blog

UaaLを使用した開発での具体的な課題

UaaLでの開発において遭遇した具体的な課題について紹介します。

Unityはネイティブアプリ内で起動するとUnityのViewが写っていなくてもUnityの処理が行われてしまい、無駄にアプリの負荷が高くなります。MirrativではUnityのViewが非表示の画面ではUnityを一時停止させ、UnityのViewを表示する際にUnityを再開させることでアプリの負荷を軽減させています。

また、1つのアプリの中でネイティブアプリとUnityの両方が動くため簡単に負荷が高くなってしまうという課題もあります。Unityは起動するだけで数十MBのメモリを使用してしまいますが、その上エモモはハイクオリティな3Dモデルを用いているためGPU/CPU/メモリどれも負荷が高くなります。特に豪華なギフト演出では様々な3Dモデルや表現方法を用いるため負荷には十分に気をつける必要があります。Unity単体では問題なくともネイティブと組み合わせることで負荷がかなり高くなってしまうこともあり気づきにくい点となっています。デザイナーだけで演出を作れるようにしていますが、最終的にはエンジニアもプロファイラーを活用して負荷に問題がないかを確認しています。ギフトの演出は豪華さと負荷のバランスが大事なので、許容ラインの判定や負荷の軽減にはエンジニアの知識や経験が問われます。

ギフトの演出は登場するオブジェクトやカメラ、エフェクトなどをTimelineで制御することで自由度高く作れるようにしています。この自由度の高さのため負荷も高くなりやすく注意が必要です。演出作成時はできるだけデザイナーが見せたいデザイン要望を叶えられるよう日々課題に向き合っています。

他にも、Unityの動作状況によらずアプリ側では別の処理が行えてしまうため、Unityが止まっているままメッセージングが行われ期待した処理にならないことがあったり、ネイティブアプリがマルチスレッドを利用している場合、Unityをメインスレッド以外で動かそうとしてUnity側がクラッシュしてしまったりとUaaL特有の難しさがありました。

エモモの動作確認ツール(チェッカー)の紹介

UaaLを利用したアプリはネイティブと連動することが前提となるため、Unity単体での動作確認が難しくなりやすいです。そのためチェッカーでのデバッグ機能やアセットのテストを充実させることで開発効率を向上させています。

ミラティブでは新しい機能を作成する際にUnity上でのチェック用にシミュレーション機能も合わせて作成しています。着せ替えやカメラの動き、ギフト演出などのエモモのデザインや機能を確認したり、ミニゲームの開発に利用しています。チェッカーではUnityとアプリとが連携する機能をUnity上だけで完結できるようにしています。

チェッカーの画面構成です。エモモのデザインを確認したりギフトをシミュレーションしたりといった機能が入っています。

チェッカーの構成

MirrativアプリのUnityの機能は1つのシーン(Mainシーン)で作られています。機能開発用のチェッカーでは専用のチェッカーシーン(AvatarCheckerシーン)がMainシーンを動かすことでシミュレーションを実現しています。AvatarCheckerシーンがSendMessageを利用してMainシーンを動かしているため、アプリと同じ仕組みでシミュレーションさせることができます。そのためMainシーンには開発専用の機能は入っていません。

ビルドにはMainシーンのみが含まれ、AvatarCheckerシーンは含まれません。チェッカーの動作中のみAvatarCheckerシーンが使われます。

f:id:t_sugaya:20210824174954p:plain

チェッカーは主にAvatarCheckerシーン内に作成したチェッカーUIから動かします。チェッカーがネイティブ実装の代わりとなることでMainシーン以降はアプリと同じ動作になります。また、アセットバンドルのロードでは実際のアセットバンドルを使わず、Unityエディタ内にあるアセットを直接ロードできるようにしています。これはUnityのAddressableにおけるFastモードに相当しています。

チェッカーの機能

チェッカーではエモモの機能確認の他にデザイナー向けの開発ツールやテスト機能も入れています。主に機能の以下があります。

  • アセットバンドルの依存性チェック
  • 3Dモデルのボーン名と構造のチェック
  • ファイルサイズや圧縮設定のチェック
  • アセットバンドルをビルドせずにUnity上でロードする機能

チェッカーによりUnity上でのシミュレーションやテストができるようになっても実機での検証は必要です。例えばアセットバンドルでの不具合やシェーダーのOS間差分など、実機でのみ発生する不具合や、Unityの一時停止/再開により正常な処理が行われない不具合が発生します。実機でのみ発生する不具合は原因がUnity本体にあることも多く、調査や対処が難しくなりがちです。チェッカーの機能を充実させることは実装におけるミスを減らすことができるため、実機での不具合への向き合いやすさに繋がります。エモモの機能開発と並行してチェッカーやツールも開発する必要があり、チェッカー自体にもまだまだ改善点は多く残っています。全体の開発効率に直結するため向き合い続けている課題の一つとなっています。

We are hiring!

Unity as a Libraryはアプリ開発における一つの可能性であり、まだまだ発展していける技術です。Unityとネイティブの両方の機能を使ってみたい方、新しい技術の先端を追いかけたい方はミラティブがオススメです。ミラティブではUnityエンジニア、ネイティブアプリエンジニアともに積極的に採用しています。是非分かり合っていきたいですね。

www.mirrativ.co.jp

最近はエンジニア向けの資料も公開しました!

speakerdeck.com