Unityエンジニアのいも(@adarapata)です。 みなさんはUnityでWebGLプラットフォーム開発していますか?
ミラティブでは先日「あてっこ!ぷにまるず」というライブゲームをリリースしました。 これはモバイルブラウザで動くWebGLです。 PCブラウザ想定のWebGLは多くありますが、モバイルブラウザを想定したゲームはあまり多くはありません。 いざ開発しようとしてもそもそも動くのか?このライブラリは対応しているのか?要求水準は満たせるのか?といったさまざまな課題が待ち受けているでしょう。
本エントリーでは、あてっこ!ぷにまるず(以下ぷにまるず)を実際にリリースするにあたって利用したライブラリの簡単な紹介や、ちょっと気をつけるポイントなどを紹介していきます。
個別の技術スタックに関する話は別途エントリを書いていくので、ここでは全体のサマリーと簡単な選定理由などに留めておきます。
※UnityではWebGLビルドのモバイル対応は公式にはサポートされていません
あてっこ!ぷにまるずの概要
あてっこ!ぷにまるずはミラティブアプリ上で動作するライブゲームです。
かわいい「ぷにまる」たちがボールをあてっこして勝負するターン制GvGアクションです。 相手のボールをぬりかえて盤面をコントロールする戦略性と、ひっさつで一気に爆発させる爽快さがあります。
開発スタッフがプレイした動画をご紹介📺✨
— 【公式】あてっこ!ぷにまるず (@punimals_jp) 2023年7月18日
バウンド数に注目👀⁉
こんなに連鎖できたら気持ちよさそう…🐰💕
動画を参考にレッツ❗チャレンジ💁
SNSスタッフは2回バウンドできました🙌
まだまだ道のりは険しい…🏔 pic.twitter.com/YdZMRKxbgv
技術的用件としては、特徴的なものは以下になります。
- ミラティブアプリのWebView上で動作するWebGL
- 2on2のGvG対戦アクションのため、二段階のマッチングが必要
- チームを作成するマッチング、チーム同士のマッチング
- 配信を見ている視聴者をゲームに招待して、すぐにチームを組むことができる
- 視聴者の招待、ギフト、コメントなどミラティブの機能と連携する
- ゲームからミラティブに伝える、ミラティブ側からゲームに伝える
- ランキングでシーズンごとに全国のプレイヤーと競い合う
やるべきことが多く、全ての機能を1から実装するとゲームの面白さ部分に取り組む時間が少なくなってしまうことが想定されます。 ということで、外部ライブラリを利用して開発速度を上げていくことにしました。
ライブラリ紹介
UniTask
UniTaskはUnityに特化したゼロアロケーションのasync/awaitのライブラリです。
ぷにまるずでは、インゲームのシーケンスだったりアウトゲームの通信や画面遷移だったり実装の8割くらいがUniTaskに依存しているので、これは欠かせないというくらいには必要なライブラリになりました。
ぷにまるずではキャンセル時に OperationCanceledException
を出させないために大元の呼び出しで SuppressCancellationThrow
をつけるのが基本実装になっています。
SuppressCancellationThrow
はOperationCanceledException
が発生したかどうかをboolで返します。
var (isCanceled, result) = await _battleReadyTeamMakeModel .ConnectAsync(_parameter.SessionName, cancellationToken) .SuppressCancellationThrow(); if (isCanceled) { /*キャンセル時の何か*/ }
これは例外が発生しても処理は続行したいが、try-catchは小さくない処理負荷がかかるのでこの対応を取っています。
UniRx
UniRxはUnityで利用できるReactive Extensionsです。
ぷにまるずのアウトゲームはMVPアーキテクチャを採用しており、主にViewのUIイベントのためにUniRxを導入していましたが、利用頻度は少なめとなりました。
画面が基本的に1つUIをタップしたら次の画面に遷移という構成が多かったので、単発でUniTaskの OnClickAsync
を行うだけでよかったり、複数回押せるUIであっても押すと通信が挟まったりして連打をしてほしくないといった状況が多く、それであればIUniTaskAsyncEnumerable
でpull型の方式に切り替えた方が適切、といったことが多かったです。
ゲームの用件として、「1度だけ押せるようにしたい」「任意のタイミング以外は押せないようにしたい」というシチュエーションが多かったためUniTaskでの実装が多くなった印象でした。
VContainer
VContainerはUnityで使えるDIフレームワークです。
今回は以下の理由から導入を検討しました。
- 複雑な依存関係を知ることなくオブジェクトを生成したい
- 寿命の不安定なMonoBehaviourを起点とした実装を避けて寿命の安定したPure Classから制御フローが流れるようにしたい
- 寿命管理をお任せしたい
ぷにまるずでは、インゲームもアウトゲームも全てLifetimeScopeに登録したIAsyncStartable
を実装したEntryPointを起点として実行しています。
基本的にはLifetimeScopeを生成してそこにオブジェクトを登録していく以外のことはしておらず、システムの根っこ以外のところではVContainerの存在を意識させないようにしています。実装者は本命のコードを実装する、LifetimeScopeに登録するの2ステップで実行できるようにしました。
その一方で、後述のPhotonFusionを用いたネットワーク周りが関わると、DIコンテナの管理外でランタイムにオブジェクトが生成されてしまいます。その管理外のオブジェクトからも依存関係を参照できるように、限定的ですがServiceLocatorを用意しています。
/// <summary> /// InGameシーン上のみで取り扱うシングルトン /// </summary> public class InGameServiceLocator { private static InGameServiceLocator _instance; public static InGameServiceLocator Instance => _instance ??= new InGameServiceLocator(); public T Resolve<T>(Scene onScene) { var scope = LifetimeScope.Find<InGameLifetimeScope>(onScene); if (scope != null) { return scope.Container.Resolve<T>(); } return default; } }
依存関係の整理もそうですが寿命管理をLifetimeScopeにお任せできたのは非常に大きいです。
基本的にはIDisposableを実装させる癖をつけておけば、LifetimeScopeを削除するだけで後始末は全てお任せできるので、Dispose忘れでうっかりリークしちゃうなどのミスを減らせます。
ちなみに、Lifetime.Transiante
は自分で破棄する必要があるので気をつけましょう。
レアケースですが、ReflectionによるInjectionは特定のiOS Safariのランタイムエラーが発生してしまったので、特に理由がないならモバイルWebGLではSource Generatorを採用することをお勧めします。
Photon Fusion
Photon Fusionはリアルタイムなマルチプレイゲームを簡単に作れるサービスです。
ぷにまるずではメインの対戦部分、ならびにマッチングをPhoton Fusionで行っています。 マルチプレイのサービスはいくつかありますが、今回は以下の理由でPhoton Fusionを採用しました。
- 2023年段階でPhotonを使う際に、PUN2はメンテナンスモードで今後更新されることはなかった
- モバイルWebGLでも動作を確認できた
- Hosted Modeは不整合を未然に防ぎやすそう
特にHosted Modeは更新権限がホストに集約するため、プレイヤー間の状況のずれに対してホストを正として進行できるのが致命的な不具合を抑えることに繋がり開発を楽にできそうだなと考えました。
結果として、Photon Fusion起因の不具合というのはほとんどなく、アクセスが集中して遅延が…といった現象も発生しませんでした。 しかし、Hostedモード自体が旧来のPUN2と作り方自体変わってくるので、ある程度の慣れが必要です。ゲーム性次第ですが、PUN2を使っていた人はSharedモードの方が楽かもしれません。 また、モバイルWebGLというプラットフォームにおいてはメモリヒープの設定見直しなど、パフォーマンスチューニングは必要になります。
導入する場合は、技術資料が少なかったり公式ドキュメントも完全ではなかったりと調査少し大変ではありますが、DiscordのPhoton Engineコミュニティなどは活発なので、ここで質問していくのもよいでしょう。
https://discord.com/channels/87465474098483200
国内ではo8queさんやニム式さんのPhoton Fusionの記事が非常に参考になるので、まずは目を通しておくことをお勧めします。
余談ですが、ぷにまるずではGvGという特性上、チーム作成のためのマッチングと対戦チームとのマッチングで2回Photonのセッションを作成しています。適切な使い方かどうかはわかりません。
MessagePipe
MessagePipeはDIを前提とした高速なメッセージングライブラリです。
基本的にはVContainerを使って依存関係を構築した上でエントリポイントから真っ直ぐに制御しているので、Pub/Subでメッセージを受け取る機会はあまりないです。ただ、Photon Fusionを利用するとNetworkBehaviourなど、ホストのタイミングでランタイムに生成され、ホストのタイミングで削除されるので自分で寿命の管理ができないオブジェクトが発生します。それらはコンテナに登録しにくい上に、参照を持っていても気づいたらnullになっている…といった状況が発生しがちです。 そのような参照関係を持つのが難しいオブジェクトとのやりとりにMessagePipeを挟んで、イベントだけを発行及び購読できるような設計にしています。
NetworkBehaviourなどの、寿命管理できない側はServiceLocatorを経由して取得しています。
var publisher = InGameServiceLocator.Resolve<IPublisher<FooMessage>>(); publisher.Publish(new FooMessage());
Containerに登録されている進行管理側のスクリプトはイベントを待つのみです。
await _fooSubscriber.FirstAsync(tokan); // イベントを待つ
利用頻度こそ多くはないですが、ここぞというところで効いてくるので導入した効果は大きかったです。 最初は購読の破棄忘れによるエラーが若干発生しましたが、MessagePipe Diagnostics WindowでSubscriptionの可視化もされているので、慣れていけばすぐ対応できるでしょう。
Visual Scripting
VisualScriptingはUnity公式が提供しているノードベースのノーコーディングツールです。
インゲームのUIや演出周りは全てVisualScriptingで実装されています。 ミラティブにはVisualScriptingで挙動を実装できるデザイナーがいるためぷにまるずでも採用となりました。 チュートリアルステージのガイドなども全てVisualScriptingです。
基本的にはVisualScriptingの内容に制限はかけていません、とはいえPhoton Fusionのネットワーク同期に関わる部分はエンジニアのメンテナンスが必要だったので、どこまでを触って良いのかという境界を設けるようにしました。 スクリプトから呼び出すためのカスタムイベントの定義や、VisualScriptingからスクリプトに干渉したい場合のコンポーネントの指定など、スクリプト <-> VisualScriptingの間を取り持つ方法だけルール決めしてドキュメント化を行っています。
ここはどのようなチームであるかといった部分も大きいですが、ぷにまるずにおいてはエンジニアが不在でもインゲームのブラッシュアップができるというのは非常に効率がよく、これなしではリリースが間に合わなかったと言っていいくらい重要なツールでした。
その一方で、モバイルWebGLにおいてVisualScriptingのパフォーマンスは無視できるものではありませんでした。Nodeがjsonで構成されており、ランタイムにデシリアライズを行うのでそれなりのGC Allocが発生します。このため、ノード構成の見直しやマクロ化など「VisualScriptingの最適化」が発生しました。 また、不具合が発生した際にC#スクリプトとVisualScripting側のどちらが原因かなどの切り分けは慣れが必要です。そのためエンジニアもやはりVisualScripting側の流れを理解することになりそうです。
結論としては、非エンジニアが動かせるツールは今後も採用したいが、もう少しメモリに優しいツールがないものかと考えています。
例えば「uNode」というアセットはVisualScriptingに近しい機能を持っており、組んだノードを最終的にC#コードとして出力してくれるそうです。
Unity Screen Navigator
Unity Screen Navigator はuGUIでの画面遷移を行うためのライブラリです。
アニメーションの設定や画面のスタック、ライフサイクルに対応したコールバックなど、基本的な画面遷移に欲しい挙動が概ね備わっているのでとても便利です。 今回はuGUIベースの画面遷移をサクッと実装したいので採用しました。
ぷにまるずではUnityScreenNavigatorの仕組みを下地として、以下の機能を追加したPageManagerを実装しています。
- VContainerと組み合わせて、ページ遷移時にLifetimeScopeをBuildする仕組み
- UniTaskを利用したasync/await対応
実装方針としては、手前味噌ですが過去にお話しさせていただいたPageManagerに近いものなので、合わせてこちらも一読していただけると理解が深まります。
アウトゲームの画面遷移やライフサイクルは毎回自前で実装を行っていましたが、今回UnityScreenNavigatorを使うことでかなり開発時間を抑えることができました。ドキュメントも揃っているので、トランジションの設定をデザイナーに学習してもらったところ、気づいたらエンジニアが何も作業することなくTimelineが実装されているという嬉しい誤算も起きています。 uGUIベースでスタンダードな画面遷移を行う場合は今後も採用を検討できそうでした。
Firestore
FirestoreはFirebaseが提供する機能の一つで、NoSQLのサーバーレスデータベースです。
ぷにまるずでは、サーバーから任意のタイミングでクライアントにメッセージを送る際に使用しています。
- ギフトやコメント、入室といったミラティブからのイベント情報
- マッチングの際に視聴者にセッション情報を教える
ライブゲームではMirrativ上でのコメントを受信したり、配信に入室したりといったMirrativ側で発生したイベントをゲーム内に反映させる必要があります。 しかし、アプリからWebViewのWebGLに直接イベントを伝えるのは難しいため、ぷにまるずではゲームサーバーを経由してイベントを受け取っています。サーバーからゲーム側への送信の手段としてFirestoreを採用しました。
Firestore自体は非常に優秀で、モバイルWebGLでも特に問題なく動作します。コメントが集中した場合に時系列がずれてしまうという懸念がありましたが、そもそもソートして取得も可能です。取りこぼしというのも発生していないので非常に安定しています。 Mirrativのライブゲームではゲーム外との連携が多くなるので、今後も活用を検討しています。
ちなみに、WebGLビルドの場合jslibを使うためC#側でブリッジスクリプトを書かなくてはいけないのでそこだけ注意です。
所感
モバイルブラウザでのWebGLも、基本的にはPCブラウザでのWebGLと同様に動作します。 有名どころのライブラリもほとんどが問題なく動作確認できました。 しかし、純粋にブラウザに割り当てられるメモリのサイズなどが大きく違うので、普段の開発よりもパフォーマンスには気を使わないとすぐにメモリリークを起こしてしまいます。 もし、みなさんのゲームがモバイルブラウザに対応しなければならなくなった時に、この記事が参考になれば幸いです。
We are hiring!
ミラティブでは面白いゲームを共に作っていくエンジニアを募集しています!!