More Related Content
Similar to Unity 2018-2019を見据えたDeNAのUnity開発のこれから [DeNA TechCon 2019] (20)
Unity 2018-2019を見据えたDeNAのUnity開発のこれから [DeNA TechCon 2019]
- 2. #denatechcon
自己紹介
• 大竹 悠人(Haruto Otake)
• 開発基盤部 第五グループ 所属
• 来歴
• 2009/4 ドワンゴに新卒入社
• 幾つかのサービスや、家電/ゲーム機向けのプロダクトを担当
• 2013/5 DeNAに中途入社
• Webソーシャルゲームタイトルの運用ののち、新規ネイティブゲーム開発へ参加
• 基盤整備を得意としていたら、いつのまにかそちらが本業に
• 現在はUnityに関連した技術サポートと様々な内製ライブラリの制作に従事
- 6. #denatechcon
Unityの機能提供手法の変化
• Unity Package Manager (UPM)
• Unity 2018から実装された、npmベースのパッケージ管理システム
• manifest.jsonの記述に従って、EditorがUnityのnpmレジストリから
パッケージをダウンロードして、プロジェクトに取り込む
• UPMパッケージの内容はプロジェクトディレクトリと分離されて管理される
• 簡単, 安全にパッケージの導入/削除/更新が可能になった
• バージョン毎に動作保証する
Unityバージョンが決まっている
- 7. #denatechcon
これらの変化によって起きたこと
• Unity Editorの開発 != Unityの開発
• UPMのそれぞれのモジュールはUnity Editorの更新とほぼ独立して開
発が進む
• Unity Editorが更新されなくても、UPMのモジュールの中で完結する
バグフィックスや機能追加はUPMでモジュールを更新することで恩恵
を受けることができるようになった
• Unityの従来の機能のうちの多くがUPMのモジュールに分割された
- 20. #denatechcon
DeNAの今までのパッケージ管理
• Upt (Unity advanced-Packaging Tool)
• パッケージ管理システムを内製
• 自治的な運用が可能であること
• 必要に応じてタイトルが自由に派生版やパッチを作り、導入できる
• 無くてもなんとかなる
• Uptを使わなくてもモジュールの導入自体は可能
• パッケージ管理システム自体の実装に時間を注がない
• 複雑で豪華なものを作ってもリスクが上がる
• 必要最低限のものをクイックに提供する
- 21. #denatechcon
DeNAの今までのパッケージ管理 – 構成
• .unitypackage生成時にmanifestファイルを同時に生成
• GUIDをmanifestに含んでおく
• 削除/更新時にこのGUIDを元に、プロジェクト内の旧アセットを一括削除すること
で、残すことがないようにする
• unitypackageをインポートすればモジュールは導入できる
.unitypackage manifest.xml
unitypackageの
相対パス
含まれるアセット
GUID一覧
パッケージ名 バージョン
依存パッケージの
リポジトリ,タグ
- 22. #denatechcon
DeNAの今までのパッケージ管理 – 配布
• 中央サーバを置かない
• Github上のブランチをインポート時に指定する形式
• メジャーバージョンごとに開発ブランチを切ってモジュールを開発
• develop/v1, develop/v2など
• 同じメジャーバージョンのブランチを常にimportすると最新の安全な変
更を取得できる
• manifestファイルに依存ライブラリが提供されているリポジトリを指
定する形で依存関係を設定する
- 30. #denatechcon
Assembly Definition Files(asmdef)
• Scriptのコンパイル単位を分割する機能
• .asmdefファイルを置いたディレクトリ以下のスクリプトを1つの単位
にしてコンパイルする
• Asmdefからは、他のasmdefを参照関係に設定可能
• 参照関係を設定した場合、他のasmdef内のコードを参照可能
• asmdefの依存関係は、UPMの依存とは別
• Asmdefの依存と別に、UPM上の依存関係も組む必要がある
• 1つのUPMパッケージの中に複数のasmdefを含める事が可能
- 42. #denatechcon
SBP & RM & AAの登場
• Scriptable Build Pipeline(SBP)
• Player/AssetBundleのカスタム可能なビルドパイプライン
• Resource Manager
• あらゆるリソースの管理者
• Addressable Assets
• SBPとRMを使って作られた、新たなAssetBundle管理システム
- 43. #denatechcon
Scriptable Build Pipeline
• Player/AssetBundleの従来のビルドパイプラインをオーバーホールして、
汎用的なビルドパイプライン構築フレームワークの上で再構成したもの
• Pros
• ブラックボックスが解消され、不具合を自力で追える
• 様々なカスタマイズが可能になった
• Cons
• 速度面で若干の無駄が残る実装
• ドキュメントが足らない
- 44. #denatechcon
Scriptable Build Pipelineの構成
• BuildTask
• ビルド処理の1ステップ。SBPでのビルドは登録したBuildTaskを順番
に実行する形で処理される
• ContextObject
• BuildTask間での値の受け渡しに使われるDIコンテナ兼パラメータ
• InjectContext属性をBuildTask内で使うと、依存性注入される
• ContentPipeline
• AssetBundleのビルドに必要なContextObjectをセットしたりしてくれ
るWrapper
- 48. #denatechcon
Resource Managerの構成要素
• IResourceLocation
• リソースの識別子。ファイルパスと、自身を処理すべきProviderの識別子を持つ
• 自身の読み込みに必要なResouceLocationを再帰的に複数保持できる
• IAsyncOperation<T>
• T型のリソースを読み込む非同期な操作を表す
• IResourceProvider
• ResourceLocationをもとにAsyncOperationを生成する、リソースの読み込み手段
• ResourceManager
• ResourceLocationを元にResourceProviderを決定し、リソース確保/開放処理を移譲
する
- 74. #denatechcon
依存関係管理の難しさ
• Nested Prefab / Prefab Variantsという特例
• Nested PrefabはEditor内とビルド時で依存性が異なる
• Editorでは元Prefabを参照として持つがビルドすると元Prefabの内容
が展開される
• AssetBundleとしては元Prefabへの依存を持たないが、ビルド時には
元Prefabが変化したときにビルドする必要がある
• Prefabの派生関係を抜き出してキャッシュ
• 派生元が変化した際に、派生先を全てビルドしなおす
- 78. #denatechcon
Abdoolのロード戦略
• Resource Managerを全面的に利用
• ビルド時に生成したアセットリストや依存関係から、依存に沿った
ResourceLocationを生成
• CacheProviderを使うと、これだけで参照カウント付きの実用的なAssetBundle
ロード機構を実現できる
• AssetBundleとアセットのそれぞれに用いるProviderを型パラメータとして指定
できるようなResourceLocationのファクトリを実装
• 幾つかのProviderを追加で実装
• 暗号化AssetBundle
• iOSのOndemand Resources(ODR)
- 82. #denatechcon
他にもDeNAのUnity開発で取り組んでいること
• .NET 4.x
• Google公式のProtocolBuffersの採用
• gRPCの導入と開発ツールへの応用によるインタラクティブなデバッグ
• async/awaitの導入とTask<T>の安全なハンドリングのノウハウ構築
• .NET Core SDKの導入
• .NET Core SDKを利用したUnityに依存しないUnity基盤開発体制
• Scriptable Render Pipelineの導入
• SRPによるモバイル向けのカスタムレンダーパイプラインの導入
Editor's Notes
- 本日はお集まり頂きありがとうございます。
このセッションでは、Unity2018から2019を見据えたDeNAのUnity開発
というタイトルで、最近のUnityの変化と、それに関連したDeNAのゲーム開発での取り組みについて話させていただきます。
- まず自己紹介から。
僕は大竹悠人といいます。
ゲーム事業部の基盤開発部というところで、主にUnity向けのクライアントライブラリやSDKの開発をしています。
- 今日のアジェンダです。
まず、Unityに今起きている変化と、それに対して我々がすべきことについて。
その後、すべきことの中でDeNAで行っていることを、2つのトピックに絞って話していきます。
- Unityに今起きていることと我々すべきことについて
- このセッションを見に来ている方々の殆どはUnityを日頃使ってらっしゃるんじゃないかと思いますが、最近のUnityをみて”何か変わってきたな”って思っているかた、どのぐらいいますか?
ありがとうございます。N割ぐらいいますね。
実際、ここ2017-2019にかけて古くから残されてきた課題が、どんどん解決に向かってきています。たとえば、ここに出しているようなフューチャーです。
また、リリースされたものに対しても”こうじゃないんだよなぁ…”という感覚に陥る事がかなり減ったように思います。
これはざっくりした主観の話になっちゃうんですが、恐らく多くの人に伝わるんじゃないかと思います。
なぜこのような感じるのかを深掘りして、Unityに何が起きているのかを分析してみます。
- まず機能提供の方法が大きく変化しました。
そのうち大きなものが、Unity Package Managerの誕生です。以下、UPMと呼びます
UPMは、Unity 2018から正式に使えるようになったnpmベースのパッケージ管理システムです。
Editor上の操作やManifestファイルの記述に従って、Unityの公式レジストリからパッケージをダウンロードしてプロジェクトに取り込んでくれます。
これによって、Unityの機能をモジュール単位で選択して取り入れることができるようになりました。
- これらの変化によって起こったことがあります。
それは、Unity本体の開発とUnityの開発がイコールではなくなった、ということです。
今までのUnityはUnity Editorの更新によって多くの機能がアップデートされたり、追加されてきました。
しかし、UPMの導入により、多くの機能がUPMのパッケージとして提供されるようになり、Editorの更新と機能の更新の関連性は以前のような絶対的なものではなくなりました。
Unity本体側の機能への依存が無いわけではないので、特定のUnityバージョン以降でしか動かないという制約もパッケージごとにありますが、
パッケージの更新だけで多くのバグフィックスや機能追加を行うことができるようになりました。
- 提供される実装の方向性に関してはどうでしょう。
従来のUnityの実装は、正直にいってモノリシックかつクローズドなものでした。
基本的に外からの拡張というのは意識されておらず、かといってプロダクションでの需要を満たそうとすると拡張を要求される、だが実装も開発もかなりクローズドで、ナイーブな対応を余儀なくされてきました。
- このあたり、最近はかなり変わりました。
まずカスタマイズが最初から意識されるようになり、全体的にモジュラリティが向上しました。
その結果として、1パッケージで全てをまかなわずに、複数のパッケージのレイヤによって機能を提供するようなケースも増えてきています。
ソフトウェアの設計も以前のいわゆる”ゲームらしい”感じからかなりモダンなものになってきていますし、パフォーマンスが重要な場面は流儀に乗るだけで高速性を得られる枠組みを作ってくれています。
また、かなり開発のオープン化が進みました。
多くのパッケージがGithub上で公開され、Preview版などの早期の段階からユーザーが開発に参加しやすいようになりました。
Unity本体のC#ソースコードも公式から提供され、安心して内部実装を追うことができるようになったんじゃないかと思います(笑)
- これらの実装の変化は、様々な領域に対して適用されています。
公式に提供してきた機能ドメインに対しては、単に後方互換を保って機能を強化していくだけでなく、カスタマイズ可能にオーバーホールしたり、捉え方から変えて提供し直すような流れが来ています。
また、使い方、管理の仕方を提示してくれるようなモジュールも、上位レイヤとして用意されるようになってきています。
公式に提供されてこなかった新たな機能ドメインに対しては、AssetStore上の有力アセットを買収して公式に取り入れていくという流れもいくつかでてきています。
- これらをふまえて、我々はどうしていくべきでしょうか
- Unityだからこれが出来ない、または回りくどい非効率な解決しか出来ないといった事は皆さん今まで出会った事があると思います。
ですが、これまで説明してきた変化の流れの中で、この状況は大分改善してきています。
- 従って、我々はまず何が出来るようになったか、切り開かれた可能性を知るべきです。
単純に提供された機能だけでなく、何をカスタマイズできるようなり、どんな可能性が生まれたのかを知り、
そしてその知った可能性を検証して、実現して、具体的な価値に転換させていかなければなりません。
- ここから、DeNAでの具体的な事例についてお話させていただきます。
- 他にも様々な取り組みをしていますが、時間の都合もありますので今日はパッケージ管理とアセットビルド環境についてに絞っていきます。
- まず、パッケージ管理についてです。
- Unityの今までのパッケージ管理について振り返ってみましょう。
今までのUnityのパッケージと言えるものといえば、.unitypackageがありました。
これはファイルとメタ情報をセットにしてアーカイブできる、いわばアセットエクスポート機能です。
コンテキストメニューから手軽に作成できますが、そもそもパッケージ管理システムではないので、
モジュール管理目的に使おうとすると、更新時に古いアセットが残ってしまったり、安全なパッケージ削除が難しかったりといった難点があります。
- パッケージ管理なしで内製ライブラリやSDKを提供していた結果として、古いファイルが残っている事による問題がタイトル開発時に頻発していました。
また、ライブラリ間の依存関係を管理できない故に巨大な1リポジトリで多くのライブラリを管理している状況になっており、内製ライブラリの横展開を行う上ではまともなパッケージ管理システムがなければ耐えられないという状況になっていました。
- なので
- DeNAでは泣く泣く、Uptというパッケージ管理システムを内製していました。
基盤開発が逆にボトルネックになってしまうことが無いように、自由に派生やパッチを作り、自治的な管理が可能であること、
また外部開発会社への提供などを考えてUptが無くても最低限導入はできること、
最低限開発が回るサイクルが作れれば良いのでこの実装は手がかからないことを目指しました。
- 構成としては、
unitypackageと一緒にメタ情報を載せたmanifestファイルを生成するような形になっていました。
UnitypackageにパッケージングしたアセットのGUIDをmanifestに含んでおき、更新時にこのGUIDを元に古い導入済みのアセットを削除してから新しくunitypackageをインポートする形になっています。
バージョン管理については実際のインポート前にManifestファイルに記載した依存情報とバージョン情報をみて適宜SKIPすることで実現しています。
- 配布方法としては、自治的な運用と運用工数の削減を主眼に置いて中央サーバを置かない構成にしました。
Githubのリポジトリとブランチを指定してインポートする形にして、
対応しているファイル構成のリポジトリを作ればUpt対応パッケージになり、
forkしてからインポートし直せば派生も可能という形にしていました。
- これらによって当初の目的は達成できましたが、
そもそもパッケージ管理システムが社内独自であるというのは、既存のエコシステムに乗ることが出来なかったり、利用にあたっての学習コストが高かったりという問題がありました。
- そんな最中、冒頭でも上げたUPMが登場ました。
独自で運用せずに目的を達成できるのであれば、それにこしたことはありません。
一旦、インハウスでのパッケージ管理で必要な事を洗い出し、UPM移行を検討しました。
- 必要なこととしては、以下のような点が上げられるかと思います。
まず、パッケージ管理システムとしての基本機能が問題なく動くこと。
次に、独自パッケージを登録できること。
最後に、パッケージのベンダーリングができることです。
パッケージ管理システムの基本機能として求められるのは、ざっくり言うと
パッケージのバージョンと依存性を管理できること、そしてパッケージの導入・削除・更新を安定して行えることにあると思います。
独自パッケージを登録できることは、内製ライブラリの管理に用いる以上、当然できないと話にはなりません。
パッケージのベンダーリングができることは、パッケージレジストリのアベイラビリティにタイトル開発が不必要に引っ張られることを避け、
また開発中のパッケージの確認がまともなイテレーション速度で回せるようにするためには必須です。
UPMを利用する場合に、これらがどのように解決できるかを説明していきます。
- UPMはベースとしてはnpmを下敷きにしていますし、幾つか制約はあるもののUPM自体の挙動が安定さえすればパッケージ管理システムとしての基本機能は十分なものを持っています。
また、こういったものは、広く広まった物であれば、エコシステムの中に存在する豊富なツールチェインを活かすことができるという利点もあります。
UPMの場合、Unity標準であることでUnity上で実装したツールも活かしやすくなる上、npmというjavascript界での標準をベースとしているため、npm向けの様々なツールを活用できます。
- UPMで独自パッケージを運用するためには、パッケージレジストリを社内にサーバーとして立てる方法と、ローカルパッケージ運用の二通りがあります。
- パッケージレジストリの構築は、npm用のレジストリサーバーであるヴェルダッシオを用いることで実現できます。
運用する上では2点工夫が必要です。
まず、Unity管理のパッケージも取得できる必要があるため、Unity管理のモジュールについてはヴェルダッシオのuplinkというリバースプロキシ機能を使って公式のUPMレジストリに中継する必要があります。
また、ポート番号は80でListenする必要があります。これは、UPMがこれ以外のポートでの接続に対応していないためです。
- 独自パッケージの作成するには、
まずはnpmモジュールの作法に従ってpackage.jsonをルート配置し、パッケージ情報を記述します。
属性はいくつか独自のものがありますので、UPMのドキュメントを見ながら適宜設定していきます。
UPMでは、ルート以下のファイルがプロジェクト内に存在するかのような形でインポートされますが、C#スクリプトは必ず何らかのAssembly Definition Filesに属さなければならないという制約があります。
- Assembly Definition Filesとは、Scriptのコンパイル単位を分割する機能です。
.asmdefファイルを置いたディレクトリ以下のスクリプトを、独立した一つの単位としてコンパイルしてくれるようになります。
Asmdefからは、他のasmdefを参照関係として設定でき、
設定した場合は対象のasmdefに対して依存が設定され、
内のコードを参照することができるようになります。
asmdefとしての依存関係は、UPMのパッケージとしての依存とは別物です。
Asmdefとしての依存を設定したとしても、UPMとしての依存が設定されるわけでもないので、それぞれに対してきちんと依存関係を自分で設定してやる必要があります。
1つのUPMパッケージ内に複数のディレクトリを置き、その下にasmdefをそれぞれ配置することで、複数のasmdefをもたせることが出来ますが、逆に複数のパッケージに分割して1つのasmdefをもたせることは出来ません。
- Asmdefを使ったクラスプラットフォーム構成についてです。
Asmdef内では、プラットフォーム識別マクロが動かなくなるため、特にネイティブプラグインなどで問題がおきます。
これは、asmdefのプラットフォーム設定で代用可能です。
同名クラスの定義を持たせたプラットフォーム毎のasmdefを用意した上で、それら全てに依存するファサードとなるasmdefを上段に用意することで、プラットフォームに合わせて適切に利用するasmdefを切り替えてくれるようになります。
- 独自パッケージレジストリの利用方法です。
これは簡単で、Packages以下のmanifest.jsonファイルを編集し、registryキーにレジストリのURLを記述します。
あとは、dependenciesに利用したいパッケージ名とバージョンを記述すれば完了です。
Unityがフォアグラウンドになったタイミングで、自動的にこの記述に従ってパッケージがインポートされます。
また、この画像にあるように、fileスキームで任意のパッケージのディレクトリを指定することで、
ローカルにあるパッケージをパッケージレジストリを介さずにインポートすることができます。
- パッケージのベンダリングは、正にこのローカル参照を用いることで実現できます。
注意点としては、ベンダリングしたパッケージから依存するパッケージは、同様にローカルパッケージ運用をしないベンダリングされません。
ベンダリングする際にパッケージをどうやって取ってくるんだという話があると思いますが、これにもnpmのツールチェインを使えます。
yarnを利用することで、依存パッケージも含めてまとめて取得することができるので、非常に便利です。
- ベンダリングが出来るとわかったので、パッケージ開発時にもこれを活かすことができます。
動作確認用のUnityプロジェクトと開発するパッケージをgitで管理し、動作確認用プロジェクトから、ローカル参照します。
この状態で動作確認用プロジェクトをUnityで開くと、パッケージ側のコードを直接編集可能なソリューションファイルが生成されるので、これをIDEで開けばそのまま独自パッケージ開発を進めることが出来ます。
パッケージに加えた変更は、プロジェクト内のコードへの変更と同様に、Unityが検知して自動的にコンパイルしてくれます。
テストコードなどは、動作確認用のプロジェクトの内部に配置しています。
- これらによって、より簡単に、より安全に内製パッケージをタイトルが取り入れられるようになり、
気軽にタイトル側もUPMを使った、モジュラリティを高めやすい構成でのパッケージ開発をとることができるようなりました。
- 次に、最速のアセットビルド環境の実現について話します。
- Unityのアセットビルドは長らく鬼門のような存在でした。
プロダクションではほぼ必須スキルと言えるのに、自分たちで管理機構からローダまで自作しないとまともに使えなかったり、
運用していくとビルドがどんどん重くなったり、ブラックボックスすぎて何をしているかもわからないなど、散々な有様でした。
- DeNAでは、今までUnity4時代に作ったライブラリを都度改修しつつ使っていました。
アセットバンドル同士の依存関係が設定できないという割り切った仕様で、複数のアセットバンドルから使われているアセットは重複して含まれるため、アセットサイズの増大の一員となっていました。
また、インクリメンタルビルドもサポートはしていますが、全ビルド対象のハッシュを計算した上で変化したアセットをビルドするという仕様のため、最低でも必要なビルド時間がそれなりに長くなってしまっていました。
- インクリメンタルビルドや並列ビルドなどのビルド高速化施策は後付で行っており、これによってフローが複雑になったり、ビルドマシンのスペックにも頼りがちになりました。
こうなると、手元でのアセットバンドルビルドのハードルが上がっていき、
開発者は小さな変化でもビルドに必ずビルドサーバーを通すようになっていきました。
要するに、Jenkinsにビルドさせるためにコミットを行っていた、ということです。
これによって、実機での動作確認のイテレーションが回しにくくなり、開発速度や確認精度を下げる要因になってしまっていました。
- 要するにかなり大本からダメダメで、作り直してなんとかする必要がもともとありました。
新しく作り直すに達成しなければならないこととして、ビルドフローの肥大化を防ぐため、動作確認のイテレーションを速く回せ、AssetBundle同士の依存関係を柔軟に管理できることを目指していました。
ですが、このあたりにUnityが大きく手を入れていく話がUniteなどで2年ほど前から流れてきていたので、機を伺っていました
- そして具体的に登場したプロダクトが、
Scriptable Build Pipeline
Resource Manager
Addressable Assets
の三点です。
- Scriptable Build Pipelineについて説明していきます。
これは、プレイヤーやアセットバンドルの従来のビルドパイプラインをオーバーホールして、汎用的なビルドパイプライン構築フレームワークの上で再構成したものです。
これにより、ブラックボックスが解消され、今までできなかったカスタマイズが出来るようになりました。
難点としては、速度面でのムダがそれなりにあるのと、とにかくドキュメントが少なく扱いづらいことです。
最近、Unity 山村さんの発表でその輪郭が解説されたので、一度目を通してみることをおすすめします
- Scriptalbe Build Pipelineのフレームワークは、
ビルド処理の一部を表すBuildTask
BuildTask間のデータ受け渡しに使われるコンテナであるContextObject
そしてAssetBundleビルドを実行するContextObjectを生成してくれるContentPipelineが主なものとしてあります。
- DefaultBuildTaskというクラスを見ると、AssetBundleビルドの輪郭が把握できます。
これを見ると、大まかに
セットアップ
スクリプトコンパイル
依存性調査
パッキング
書き出し
の5フェイズで構成されていることがわかります。
- これらのフェイズの中で読み書きされるContextObjectの中でも、次の3つはカスタマイズを施す上で重要な位置を占めています。
BundleBuildContextは、ビルド対象の定義となる入力です。
DependencyDataには、アセット同士の依存情報が吐き出されます。
BundleWriteDataには、アセットをどういう単位でAssetBundleにするのかという、パッキング情報が吐き出されます。
- 次に、ResourceManagerについてです。
これは、リソースの場所と読み込む手段を定義するフレームワークと、その上で実装された各種リソース読み込みのデファクト実装集です。
拡張が非常にしやすく、リソースの場所を管理する層と具体的な読み込むロジックを分離することができます。
難点としては、これもドキュメントが足らず、リソース管理自体は簡単な管理で問題ないですが、他に定義する必要があります。
- Resource Managerは主に以下のような要素で構成されます
ResourceLocationはリソースの識別子、場所を表し、再帰的に依存するリソースを表現できます。
AsyncOperationはT型のリソースを読み込む非同期な操作を表します。
RsrouceProviderは、ResourceLocationを元にAsyncOperationを生成します。
ResourceManagerは、ResourceLocationを受け取って、登録されたResourceProviderの中からリソース確保/開放処理の移譲先を決定します。
- ResouceProviderは様々なものが標準で実装されていますが、主だったものとしては以下のものがあります。
AssetBundleProviderは、AssetBundleをロードします。
BundledAssetProviderは、AssetBundleからアセットをロードします。
CachedProviderは、AsyncOperationの参照カウント付きキャッシュを行い、キャッシュがなければ指定したProviderを移譲します。
- 他にも、SceneProviderやInstanceProviderなど、アセットロード以外の目的のProviderのインターフェースや実装も揃っています。
- Addressable Assetsについてです。
これは、アセットにアドレスをという文字列を振り、アドレスを用いてランタイムでアセットに対してアクセスする手段を提供するものです。
どういう単位でパッキングするのか、どういう読み込み方をするのかは、設定でカスタマイズする形になります。
バックエンドとしてScriptableBuildPipelineとResourceManagerを利用しており、概ねこれらの管理層として機能しています。
手っ取り早く使えますが、速度面に問題があるのと、設定の枠から出たことはやりづらく、プロダクションで使うには足りないものもあります。
- これで、新たなアセットビルド基盤を作る材料は出揃いました。
- これらの材料の調査の結果、いくつか選択肢が出てきましたが、我々はAddressableの利用をやめ、Scriptable Build PipelineとResourceManagerを使うという選択をしました
- 先程も欠点として若干触れましたが、Addressableを利用しないのは、速度面と、小回りの効かなさが理由です。
Addressableを試してみたところ、規模の大きいプロダクトのアセットを持ち出してテストしたところ、1アセットのみを変化させてもビルドに10分ほどかかることがありました。
これでは、手元でビルドを気軽に回せるとは言えないオーダーです。
また、アセットバンドルを作るに当たって、キャラや技単位など、適度な単位でパッキングして配信したい需要がありますが、Addressableはかなり荒い制御しかかけることができません。出来なくはないですが、かなりムリな運用になることが想定されました。
- Addressable Assetsのビルドの最低所用時間が長いのは、ビルド対象のアセット総数に比例してビルドの最低所用時間が伸びることにあります。
ビルド対象のすべての依存関係を取得しないと、ビルドに必要な情報を構築することができません。
コストが高い処理はすべて結果をキャッシュするのですが、そのキャッシュ取得だけでそれなりの時間がかかります。
そのため、ビルド対象自体を低コストで枝刈りするような形のパイプラインを組む必要があることがわかりました。
- これを踏まえて速度面や挙動面の改善を図ろうとしても、そもそもAddressableの現状の実装とのギャップは大きく、改修する方向での実現には努力が多く必要になりそうでした。
加えて、過渡期なのもあってインターフェースも安定しておらず、改変しやすい保守性のある設計にはなっているものの、外からの拡張を容易にしようと作っている訳ではないようであったので、苦労しても相応のリターンを得られるとは言い難い状況でした。
- これらの理由から、我々はAddressableを捨てて、SBPを直接使ってビルドパイプラインを構築し、直接RMを使って読み込みをさせる形でAddressableの代替となる層を独自に実装することにしました。
- そうやって生まれたのが、Abdoolです
- Abdoolの要件としては、依存のAssetBundle管理システムの課題がそのまま当てはまりました。
AssetBundle間の依存関係の構築を、設定の柔軟性を保ったままサポートしつつ、
高速に確認のイテレーションを回せる速度を担保することです。
- これを踏まえ、Abdoolは常に最速であり続けることを目指して設計しました。
イテレーションの加速のためにはビルドの高速化は当然重要ですが、モバイルゲーム開発においてはその高速性を常に維持することが難しいです。
リリース直後は10分でビルドできたとしても、1年後にリソース総数が飛躍的に膨らんだ結果、ビルドに数時間かかってしまう、ということがありえます。
そのために、ビルド時間をプロジェクトのアセット総数に依存しなくし、変化したアセットの総量のみに依存してビルド時間が伸びるようにすることを目指しました。
- そもそも、Unityで正しくAssetBundleをビルドするためには、ビルド対象のAssetBundleに直接含むアセットすべてと、そこから依存しているAssetBundleを全て同時にビルドパイプラインに投げ込む必要があります。
これを事前に枝刈りして行うためには、依存関係を辿ってビルドの影響範囲を調査する必要があるため、Abdoolではこの処理を常に最速であり続ける形で実現することを目指しました。
- このためにまずはGitベースの変更済みのファイル追跡を採用しました。
ビルド後にビルド元アセットのコミットハッシュを記録することで、最後のビルド以に変化したファイルを列挙出来るようにしています。
- これを元に、依存関係を辿って実際のビルド対象となるAssetBundleの構成を決定しなければなりません。
常に最速であり続けるために、所属するアセットバンドルの指定方法に幾つか制約をもたせています。
ビルド対象には依存アセットバンドルも含まれますが、変化が無いビルド済みのものはSBPがキャッシュを参照してくれるため、事前に大半を足切りしている前提であればこのコストはほぼ無視できます。
また、変化が無いアセットから依存するアセットが変化することはないので、これを利用して前回のビルド時のアセットバンドル間の依存関係をビルドに利用していきます。
- 所属アセットバンドルの構成に関して課しているルールは、
第一に、間接的にアセットバンドルに含まれるアセットの存在を許さないこと
第二に、どのアセットバンドルに属するのかは、ファイルパス情報のみを材料として決定することが出来ること
第三に、アセットバンドル名からそのアセットバンドルに含まれる全てのアセットを全体を走査することなく列挙できることです
要するに、アセットの総数に依存して負荷が高まるような挙動を許さないようなルールにしています。
- AssetBundleの構成の記述の自由度を担保するため、AssetBundleの構成ルールは設定としてではなく、特定のinterfaceを実装する形で指定するようにしています。
標準の実装でありそうなパターンの実装と、分岐やPrefix付与を行うデコレータ実装を用意することで、実装負荷を下げて前述する制約を逸脱したルールを実装してしまう危険性を下げられるようにもしています。
- Abdoolによるビルドの流れを説明していきます。
- Abdoolによるビルドの流れは以下のとおりです。
文字だと理解しづらいので、図解して説明していきます。
- このようなアセット構成のビルドを考えてみます。Bundle1~6を表す箱ににそれぞれアセットが含まれており、依存関係を線で表しています。
- まず、Bundle1のasset1.prefabが変更されたケースを考えてみます。Abdoolはまず、変更のあったアセットをGitを使って列挙します。
- 次に、変更されたアセットが所属するアセットバンドルをビルド対象とした上で、それに含まれる全てのアセットを調査対象にします。
- つぎに、調査対象のアセットからの依存を辿ります
- 調査対象から依存しているアセットが所属しているアセットバンドルを、ビルド対象にします。
- このままでは、依存として検知するべきBundle6を検知できないため、
ビルド対象となったアセットバンドルから、前回のビルド時に依存していたアセットバンドルもビルド対象として、調査対象から拾えない依存関係をすべて補完します。
調査対象になっているAssetBundleも、変更のあったAssetBundleなので当然ビルド対象とします。
- これをビルドにかけると、変化のないものはビルドがスキップされ、変化のあったBundle1のみが実際にビルドされます。
ビルドに関係のないBundle2やBundle5がどれだけ量があっても、変わらないコストでビルドすることができます
このあと、ビルドによって得られた今回のビルド内容の依存関係グラフと前回ビルド時の依存関係グラフを合成して、新たな依存関係グラフを作り出し、カタログファイルとして保存しています。
- また、依存性管理に関してはUnity 2018.3のNested Prefab対応として特例対応をしています。
NestedPrefabはEditor内では内包するPrefabを参照として保持しますが、ビルド時には参照が解かれて全ての内容が参照元に展開されるという特性があります。
このため、AssetBundle化したあとの依存性と、ビルド時に変更検知として考慮すべき依存性が異なっています。
Abdoolでは、Prefabの派生関係を全て抜き出した上でビルド時に保存しておき、派生元が変化した際に全ての派生先をビルド対象とすることで対処しています。
- また、スクリプトのコンパイル時間も運用が進むにつれて増大しがちですが、AssetBundleビルド時にはスクリプトコンパイルを伴います。
これをSKIPできれば、小さな変化を確認するイテレーションには効果があると判断しました。
- SBPを使って実際に行ったカスタマイズがこちらです。
そもそもAbdoolのAssetBundleビルドでは実際のビルドを行わない、依存関係の調査のみを行うパイプラインが必要なので、そちらをSBPを用いて構築しています。
スクリプトコンパイルのSKIPは、ビルド後に得られるTypeDBのインスタンスをキャッシュしておくことで実現できました。
循環参照検出は、AssetBundle同士の循環した依存を検出して複雑な依存性が生まれるのを事前に察知する試みです。
- このような工夫によって、目的としていた高速であり続けるビルドパイプラインを構築することが出来ました。
効果を確かめるために、Adressable同様のSBPのキャッシュを利用したビルドと、Abdoolの枝刈りを行うインクリメンタルビルド時のビルド時間を比較してみます。
条件として、平均250KBの、最大で3hopの依存関係を持つアセット郡を用意し、ごく小さな1アセットのみが変化した場合を計測しました。
このように、フルビルド時やキャッシュを使ったビルドではアセットバンドル数の増加につれて比例してビルド時間が伸びていきますが、
インクリメンタルビルドでは常に一定の時間でビルドができている上、絶対値としても全て1秒で処理できています。
アセットバンドル数4000という数になるとキャッシュビルド比で79倍、フルビルド比で242倍もの性能を達成できました。
- ロードをどのようにしていくかについても話します。
ロードにはResourceManagerを全面的に活用しています。
ビルド時に生成したカタログファイルを元にロードしたいAssetの所属するAssetBundleやその依存AssetBundleを抽出し、依存に沿った形のResouceLocationを生成しています。
ResourceManagerを採用することで、ResourceLocationを吐き出すだけで参照カウント付きな実用的なAssetBundleロード機構が実質100行前後のコードのみで実現できました。
また、幾つかの拡張Providerを追加で実装し、暗号化やiOSのOndemand Resources経由でのAssetBundleロードも気軽にできるようにしています。
- Abdoolを使ったロード処理のサンプルです。
AssetBundleのロードはProvideResource時に透過的に行われ、また読み込み方法もLocatorに型パラメータとして渡すProviderの組み合わせによってカスタマイズすることができます。
- カスタムProviderの一つとして、Streamを使った暗号化チャンクロードも実現しています。
通常、暗号化を施すと復号化したあとにLoadFromMemoryで読み出す必要があるためマネージドメモリを消費しますが、LoadFromStreamを使うとこれを回避することができます。
ランダムアクセスができる暗号アルゴリズムをStreamとして実装することで、暗号化されたAssetBundleをメモリに全て展開することなくチャンクロードで読み込ませることができるようになります。
1アセットバンドルのサイズに比例してアセット読み込み時のリード量が増えるという特性がUnity側にあるので、このパターンを用いるときは1AssetBundleをできるだけ小さく保つ必要があります。
- まとめます。
SBPを活用することで、常に最速であり続けるビルド環境を構築することができました。
また、ResourceManagerを活用することで、Unity標準の実装を最大限に活かす形でアセットロード基盤を構築できました。
- 他にもDeNAのUnity開発では、基盤・タイトル共に様々なことに取り組んでいます。
たとえば、.NET 4.xや.NET Coreの活用やScriptable Render Pipelineなどが上げられます。
- これからの展望です。
Abdoolに対しては、実機からの要求に応じてEditor上でオンデマンドビルドして最新のアセットを供給するオンデマンドビルドサーバーの構築や、AddressableのAssetReferenceのようなハイレベルAPIの拡充を行っていきます。
ほかにも、Prefabの進化を活かしたワークフロー検討はもちろん、ECSやUI Elementsなどのこれからの技術に対してもしっかりと検討していこうと考えています。
- 最後になりますが、
Unityの最近の変化はアンテナをより広げて覚悟を持っていかないと拾いきれないほど大きなものになってきています。
アンテナだけなく、開かれた可能性を咀嚼して、プロダクトにしっかりと反映していかなければなりません。
DeNAはそんな覚悟を持ったUnityエンジニアを待っています。