複雑化するAssetBundleの配信からロードまでを基盤化した話【CEDEC 2017】
こんにちは、QualiArts 技術開発室 エンジニアリーダーの石黒です。
8月末のCEDEC 2017にて、「複雑化するAssetBundleの配信からロードまでを基盤化した話」というセッションを講演させていただきました。講演自体は弊社の福永と二人で発表しました。当日は満席に近い状態まで埋まり、貴重な時間を割いて本講演にご参加いただけた皆様には御礼申し上げるとともに、自分自身初のCEDEC発表として大変嬉しい限りです。
本エントリーは当日の講演内容をフォローする形なので、当日のスライド↓とあわせてご覧いただけると幸いです。
ゴール
QualiArtsでは2013年頃よりUnityを用いたゲームを多くリリースしてきましたが、当時よりAssetBundleと呼ばれる、Unity用のアセット動的配信機能を使ってきました。そこで本公演では、
- 今までQualiArtsがAssetBundleでどんな苦労をしてきたか
- どうしてAssetBundle配信基盤を作ったのか
- 設計・実装の背景と詳細
をご紹介しました。
あくまで弊社での解決の方向性ということを、留意していただければと思います。
経緯
元々2015年頃まで開発していたゲームでは、AssetBundleに関する機能は個別に開発していました。もちろん各ゲームで完全に個別開発していたわけではなく、ある程度のコード資産の共有は行っていましたが、AssetBundleに対する各種ノウハウなどが分散していたのが実情でした。
そこで基盤を作ることで、以下を目指しました。
- ノウハウの集中
- 開発リソースの集中
- 配信にかかるインフラコストの適正化
- 実装品質の担保
2015年秋頃より基盤化プロジェクト「OCTO」が始動し、2016年夏にリリースされた「オルタナティブガールズ」に初めて採用されました。以後、全てのアプリにて「OCTO」が利用されています。
サーバー設計・実装
主にセッション中には触れなかった内容について紹介します。
言語
まず、APIサーバー、管理画面、そしてCLIの全てがGolangを採用しています。
採用理由は、
- シンプルな言語仕様
- コンパイルが速い
- デプロイが容易:単一ファイルを配布するだけなので、コンテナと相性が良い
- クロスコンパイル:様々なプラットフォーム用のCLIを簡単にビルドできる
癖はありますが、基盤に適した良い言語だと思います。実際に他の基盤にもGolangは採用されています。
データベース
データベースに関しては、実績とノウハウのあるMySQLを採用しています。更新するのが開発者側のみで、ユーザーからは参照しかないため、弱めのマスターと強めのスレーブを並べるような形になっています。またコンテナなどは使わずに、普通にインスタンスを立てて使っています。
通信プロトコル
クライアントとの通信プロトコルは、Protocol Buffersを採用しています。これはシリアライズ・デシリアライズともに高速であることが大きな理由です。スキーマを事前に定義する必要はありますが、基盤の性質上大きな変更があることはほとんどなく、現にリリース以降でスキーマは変更していません。また既に他のGolangを採用した基盤でもProtocol Buffersの採用実績があったことも理由の1つです。
CLI
時間の都合上詳しくは紹介できなかったのですが、CLIは以下の機能があります。
- アップロード
- タグ追加
- 削除
- Copy
- Sync
- 存在確認
- リスト取得
(一部機能は管理画面やゲームサーバーからも使えるようにAPIでも提供しています。)
クライアント設計・実装
こちらも主にセッション中に触れなかった内容について紹介します。
C言語?
講演中では説明しませんでしたが、過去に他の基盤を作る際にC/C++実装のライブラリを採用することで、パフォーマンスとマルチプラットフォーム対応の両方を実現したことがありました。
確かに目的は達成することができたのですが、実際には各プラットフォーム用にビルドする環境を維持するのが大変難しく、またC/C++を扱える人間も少ないため、メンテナンスコストが非常に高かったです。そのためOCTOを作る際にも当初C/C++でなるべく実装するという話もありましたが、オールC#を目指して開発することにしました。
UnityWebRequestの秘話
また、講演当日は、UnityWebRequestでスムーズに検証を進めたかの如く喋っていましたが、実際にはリリースまでにいろいろなトライ&エラーを繰り返しました。それは当時使っていた(今でも使っている)Unity 5.3系ではまだiOS/Androidサポートがexperimentalな位置づけだったためです。普通にGETで通信するだけでも、クラッシュしてしまったり、挙動がおかしいことが多々ありました。
そのため、Unity 5.3系の初期の方では、ひたすらUnityWebRequestのバグをUnity側にレポートしていました。そのかいあってか、後半の方にはおおよそ致命的なバグは直っていたため、なんとかサービスのリリースに間に合わせることができました。
ただ、UnityWebRequest自体のバグは2017年9月現在でも散見するような状況ですので、バグが一切ないわけではないということに注意する必要があります。
独自キャッシュ管理周りのバグ祭り
バグ関連でもう一つ、独自キャッシュ管理においてキャッシュ走査を別スレッドで行っていることを説明しましたが、実はこれに関連したバグがリリース以降も散発してしまいました。(これに関してはユーザーさんからのお問い合わせで発覚することが多く、大変申し訳無い気持ちでいっぱいでした。)それはマルチスレッドが故に、なかなか発生しないレアなバグが潜在的に多かったからです。
もちろん排他制御は行っているのですが、大きい単位で排他制御をすると最悪ゲームが動いているメインスレッドが止まってしまう可能性があるため、必要以上に大きい単位でロックしないようにしたり、ロックに使うmutexを細かく使い分けて、パフォーマンスに配慮していましたが、逆にこれがバグの温床になってしまいました。
結果的には、リリースして半年以上経ってもマルチスレッド起因のバグが潰しきれていない状態でしたが、ここ最近は不具合を聞いていないので一年越しでようやく仕上がったと思います。
反省としては、キャッシュモジュール周りのエイジングテストをしっかり行う、実機を想定して低速なI/O環境を擬似的に再現できるようにsleepを入れるなどが考えられます。
ストイックなAssetBundleのメモリ管理
講演中の説明では、AssetBundleを参照しているGameObjectがある限りはアンロードしないと説明しましたが、実はこの方式の場合はAssetBundleを同時に使う数が増えれば増えるほどメモリを圧迫してしまう原因になってしまいます。AssetBundleが必要なのはAssetのロードが完了するまでで、Assetが完了したらそのAssetBundleはアンロードしても良いはずです。
そこでOCTOではデフォルトメモリリーク回避のために、AssetBundle.UnloadのunloadAllLoadedObjects引き数をtrueにしているところを、ストイックな管理もできるように、オプションの切り替えでAssetBundleをアンロードしてもロードしたアセットをアンロードしないようにもできるようになっています。
まとめ
AssetBundle配信基盤の目的としては、基盤化による工数削減と品質担保がメインでした。そのためスケールメリットを活かせるような規模でないと基盤化は難しいかもしれません。ですがAssetBundle周りはユーザ体験に直結するため、AssetBundleを扱う全ての人に何かしらのヒントになれば嬉しい限りです。そして最終的に、全世界のユーザーの方の不満が少しでも減らせることができれば、エンジニア冥利に尽きます。