こんにちは、ミラティブ菅谷です。MirrativのWebGL製ライブゲームにて、アセットのロード方法をインスペクタでの直接参照からAddressable Asset System(以下Addressable)に変更したことで、クラッシュ率が大幅に改善し、クラッシュせずにプレイが終了した「正常プレイ率」は98%まで改善しました。本記事では、その具体的な手順と効果について紹介します。
課題背景
MirrativのライブゲームはWebGLで動いていますが、特にモバイルのWebGLではパフォーマンスに気をつける必要があります。今回改善を行ったプロジェクトではもともとAddressableを使用していませんでした。プロジェクトの初期リリース時点では大きな問題もなく運用できていましたが、アセットやリソースを追加していくにつれてクラッシュが多く見られるようになりました。ただし、WebGLではクラッシュしてもログが正確にキャッチできず、メモリの問題なのか特定の処理やアセットが原因なのかは不明のままとなっていました。
クラッシュ率の測定のため、以前のブログで紹介した方法を導入することで、より具体的な数値として取得できるようになりました。 その結果、iOSでのクラッシュ率は運用を続けていくことで大きくなってしまっていました。
原因の調査
UnityのProfilerを使用して確認したところ、ゲーム内で使用していないにも関わらず、インスペクタで参照しているすべてのデータがメモリにロードされ、これがメモリを圧迫していました。3Dモデルやステージはゲームの特性上、同時に生成する数や種類が限られているため、無駄が多い設計となっていました。特にiOSでは使用可能なメモリが少ないため、この問題が顕著に現れていました。
ステージオブジェクトそれぞれがnew_slot_testというMeshを持っており、それらが全てメモリに乗ってしまっている。 実際にゲーム内で使われるのはそのうちのどれか1種類なので本来なら1個だけがメモリに乗るようにしたい。
これまでのデータの追加方法
これまではキャラクターやステージの3DモデルをPrefab化し、インスペクタで直接参照していました。この方法では使用時にInstantiateで生成して利用するという最も簡単な手法を取っていました。
[SerializeField] private GameObject prefab; public void Load() { var instance = Instantiate(prefab); }
Addressable化
問題の対策として、Addressableを導入しました。Asset Referenceを使用することで、既存の構成や追加のフローを大きく変えずに移行を進めることができました。非同期処理を適切に行うため、async/awaitを利用し、UniTaskも併せて導入しました。
Asset Referenceでのデータロード
以下のようにしてAddressableを利用してデータをロードします。
// GameObjectはAssetReferenceGameObjectを利用する [SerializeField] private AssetReferenceGameObject prefabRef; private AsyncOperationHandle<GameObject> prefabHandle; public async UniTask LoadPrefab() { prefabHandle = prefabRef.LoadAssetAsync(); var result = await prefabHandle.Task; var instance = Instantiate(result); } // MaterialやAudioClipはAssetReferenceTを使用する [SerializeField] private AssetReferenceT<Material> materialRef; private AsyncOperationHandle<Material> materialHandle; public async UniTask LoadMaterial() { materialHandle = materialRef.LoadAssetAsync(); var result = await materialHandle.Task; // 例えばskyboxの変更など RenderSettings.skybox = result; }
[SerializeField]で参照していた箇所をAssetReferenceにしておきます。あとは同様にInstantiateで生成して使用します。AssetReferenceにしておくことでインスペクタ上で参照できるようになります。
また、AssetReferenceは使用せずに、Addressables.LoadAssetAsync<GameObject>(assetName)
で直接ロードすることも可能です。
使用し終わったら以下のようにReleaseを行います。
public void ReleaseRef() { Addressables.Release(prefabHandle); Addressables.Release(materialHandle); }
Releaseタイミングはオブジェクトの寿命と紐付けることで、処理をシンプルに保つことができます。オブジェクトの使用中にReleaseしてしまうと生成したオブジェクトが消えてしまうことがあります。
結果
Texture2D、Mesh、CubeMap、AudioClipなど、Addressableでロードするようにしたアセットは使用していないデータがメモリに乗らなくなりました。その結果、これまで起動すらできなかったiPhone8などの低スペック端末でも、クラッシュせずにプレイできるようになりました。
Addressable化前。Texture2DやMeshが多い。
Addressable化後。アセットがメモリに乗らなくなっていることがわかる。
Addressable化の注意点や計測の課題
Addressable化を行うことでロード処理は必ず非同期処理になります。AddressableにはWaitForCompletion()による同期処理化のメソッドがありますがWebGLでは使用できません。そのため同期処理のみで構成していたプロジェクトでも非同期処理を扱うことになります。また、WebGLのAddressableではアセットのダウンロードも同時に行われるため、ユーザー環境によってはダウンロードが遅かったり、そもそもダウンロードに失敗したりします。それによりロードに失敗した場合も常に考えておく必要が出てきます。開発の効率化を考えると、なんでもかんでもAddressableを利用するのではなく、運用していても増えないデータや小さいデータはこれまで同様インスペクタへの直接参照を利用したほうが良いです。
WebGLビルドでもUnity Profilerを使用して計測することは可能ですが、計測時にエラーが起こりやすく、計測の難易度は高いです。Mirrativの場合、WebGLゲームはMirrativアプリ内で動作するため、Unity Profilerが使用できません。そのため、Xcodeも使用して調査を行っています。また、同じWebGLでも、PCとモバイルでは動作が異なる場合があるため注意が必要です。
その他のWebGL開発での注意点やノウハウは過去の記事でも紹介しています。
まとめ
Addressableを導入することでWebGLゲームであっても安定した動作を提供できるようになりました。他にも最適化の手段や方法はありますが、データ数が増えたことによるクラッシュ率の増加という点では、まず不要なデータがメモリに乗っていないかの調査をおすすめします。
We are hiring!
ミラティブでは一緒に開発してくれるエンジニアを募集しています!
また、ライブゲーム開発に興味のあるインターンも積極募集中です!