こんにちは。開発G所属のエンジニアです。
ソーシャルゲームの開発と運用上リソースのビルドと更新は必ず必須になっています。
ソーシャルゲームの場合、Unityのリソースは主にAssetBundleとしてビルド、ダウンロード、更新を行います。
目次
1.リソースバージョン設計
リソースバージョンはリソースタイプによって、2種類に分けました。
- Master Data
- AssetBundle + Rawファイル実運用上二つのバージョン管理になります。
マスターデータのバージョンを別管理にしたのは、マスターデータは頻繫に更新されますので、開発効率と運用効率を改善するため、マスターデータはUnityに依存しないように設計しました。
管理画面上設定例:
Application version , Asset Version ,MasterData Version
2.Assetの種類設計
- AssetBundle
- Raw
- MasterData
AssetBundle
Unityのリソース、texture、prefabなど
Raw
Unityに依存しないリソース、movieなど
Androidの場合MovieはDecompressまたはメモリからロードした場合、再生できない問題があります。
AssetBundleにし、1. AssetBundleをロード、2. Fileとして吐き出し、3. Movieを再生する方法もありますが、直接Rawファイルとして、ローカルにDownloadした方が、AssetBundle.Load分負荷がかからないので、Unityに依存しないようにしました。
MasterData
ExcelからCsvとして吐き出し、csvを一つのjsonにまとめ、jsonを圧縮、ロード時にメモリに一括展開
3.アセットロケーション設計
運用上アセットは4種類必要になります。
- Build In Resources
- Build In Streaming Asset
- 初期Download
- 都度Download
Build In Resources
できる限り使わないこと、使う場合はResource Unloadで綺麗に解放すること、たとえFontなどが参照に残った場合、メモリが無駄に使われます。
Build In Streaming Asset
Build Inに入れるリソースは積極こちらを使いましょう。
注意点:Androidの場合はwebrequestを利用してアクセスすること
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public static async Task<string> LoadTextFromStreaming(string path) { #if UNITY_ANDROID && !UNITY_EDITOR UnityWebRequest www = UnityWebRequest.Get(path); await www.SendWebRequest(); if (www.isHttpError || www.isNetworkError) { Debug.Log(www.error); } byte[] content = www.downloadHandler.data; www.Dispose(); #else byte[] content = File.ReadAllBytes(path); #endif string str = FileUtil.LoadFromCompressedJson(content); return str; } public static async Task<byte[]> LoadByteFromStreaming(string path) { #if UNITY_ANDROID && !UNITY_EDITOR UnityWebRequest www = UnityWebRequest.Get(path); await www.SendWebRequest(); if (www.isHttpError || www.isNetworkError) { Debug.Log(www.error); } byte[] content = www.downloadHandler.data; www.Dispose(); #else byte[] content = File.ReadAllBytes(path); #endif return content; } |
初期Download
多くのソーシャルゲームと同じく、チュートリアル完了したら、初回一括Download
都度Download
運用上イベント、ガチャなどのリリースより増えていくリソース
Rawファイルの格納場所
Rawファイルは基本UnityのImportが不要です。Rawファイルの無駄なImportが走らないようにするため、今回はRaw対象ファイルはUnityの
Assets/StreamingAssets/rawフォルダーの中に入れことにしました。Jenkinsビルド時Assets/StreamingAssets/rawを暗号化し、一定のネーミングルールでアセットバンドル名ルールと合わせます。
Applicationのビルド時にはAssets/StreamingAssets/rawフォルダーを自動削除して、ビルド行います。
4.設定Editor拡張
- ロケーション設定
- バンドル名設定
- LinkXml生成(アセットバンドルのみ参照しているScript対応)
- ビルドOS
- バンドルビルド
全体画面
ロケーション設定
Editor上はFolderまたはファイルの右クリックで設定、またはScriptで一括設定できるようにします。
- Initial(初期ダウンロード対象リソース)
- Partial(都度ダウンロード対象リソース)
- S(StreamingAsset対象リソース)
5.リソースDownload種類
- MasterData
1await ResourceManager.DownloadFile(ResourceManager.GetMasterDataUrl(), TazFilePath.GetLocalMasterDataFilePath()); - Initial
1await ResourceManager.DownloadAsset(initialList); - Partial
1GameObject go = await ResourceManager.LoadAssetAsync<GameObject>(assetBundleName);
12345678//ローカルに存在しないリソースをDownloadResourceManager.DownloadAsset(list)//ローカルに存在ししなかったら、Download且つロードResourceManager.LoadAssetBundleAsync(list)//ローカルに存在しないリソースをDownload(依存するassetも一緒にDownload)DownloadAssetWidthDependencies(list)
- 用意したResourceManager
123456789101112131415161718192021222324252627282930313233343536public static void Init() {AssetBundleDownloader.SetUp();#if !ASSETBUNDLE_SIMULATOR && UNITY_EDITOR_loader = new ResourceLoaderForLocalResource();#else_loader = new ResourceLoaderForAssetBundle();#endif}public static async Task PrepareStreamingAssetInfo()public static async Task<bool> IsInStreaming(string assetBundleName)public static void SetAssetBundleBaseUrl(string assetBundleBaseUrlpublic static void SetMasterDataUrl(string masterDataUrl)public static string GetCatalogUrl(int version = 1)public static string GetMasterDataUrl(int version = 1)public static string GetAssetBundleUrl(string assetBundleName, int version = 1)public static async Task<bool> LoadRemoteAssetBundleManifest(bool useAsync = true)public static async Task<bool> LoadStreamingAssetBundleManifest(bool useAsync = true)public static async Task<GameObject> InstantiateGameObjectAsync(string assetBundleName, Transform parent, bool worldPositionStays = false,bool useAsync = true)public static async Task<T> LoadAssetAsync<T>(string assetBundleName, bool useAsync = true) where T : Objectpublic static async Task<T> LoadAssetAsync<T>(string assetBundleName, string assetName, bool useAsync = true) where T : Objectpublic static async Task<T[]> LoadAllAssetsAsync<T>(string assetBundleName, bool useAsync = true) where T : Objectpublic static async Task<AssetLoaderBase> LoadAssetBundleAsync(string assetBundleName, bool useAsync = true)public static async Task<Dictionary<string, AssetLoaderBase>> LoadAssetBundleAsync(List<string> assetBundleNameList, bool useAsync = true)public static AssetBundleDownloadProcessInfo GetDownloadInfo(List<string> assetBundleNameList, bool isHashList = false)public static async Task<bool> DownloadAsset(List<string> assetBundleNameList, bool isHashList = false)public static async Task<bool> DownloadAssetWidthDependencies(List<string> assetBundleNameList, bool isHashList = false)public static async void SetResourceLoadType(TazEnum.ResourceLoadType loadType, bool all = false)public static async void UnloadAssetBundle(bool unloadUnusedResource = true, bool includeGlobal = false)public static AssetBundleDownloadProcessInfo GetAssetBundleDownloadProcessInfo()public static void Retry()public static void CancelDownload()public static Dictionary<string, AssetBundleData> GetCatalogData()public static void SetGlobalAssetBundleNameList(List<string> assetBundleNameList)public static async Task<bool> DownloadFile(string url, string localSavePath)public static void UpdateVersionDataAndRemoveUnused()
6.リソースLoad And Unload
- Streaming Asset Load
- Remote Asset Load
- Resource Unload
- 同期、非同期Load
- await実装
Streaming Asset Load
初期ゲーム起動から初期ダウンロードまで、Streaming Assetを参照(Streaming Assetに含まれなかったAssetは自動でRemoteからDownloadし参照するように)
Remote Asset Load
初期Downloadが完了後、Remoteのみ参照するように(Streaming AssetとしてロードされたAsset Bundleを破棄し、Remoteのみ参照する)
Resource Unload
常駐Asset(例:UiのAtlasなど)として設定したリソースはStreaming Mode -> Remote Modeに切り替わるタイミングでUnload
それ以外のAssetは適切なタイミングでUnloadを行うように実装
同期、非同期Load
Defaultは非同期ロード、明示的useAsyncをfalseとして呼ばれた場合のみ同期Loadを行う。
同期の場合:
Assetを暗号化する場合、LoadFromMemory またはLoadFromStream
Assetを暗号化しない場合、LoadFromFile
非同期の場合:
Assetを暗号化する場合、LoadFromMemoryAsync またはLoadFromStreamAsync
Assetを暗号化しない場合、LoadFromFileAsync
非同期の場合同じフレームで同じAssetBundleのLoadが走る可能性がありますので、Lockをかける必要があります
semaphoreslimでロックをかけることは可能だが、パフォーマンスが悪かったので、下記ように一フレームを待つようにしました。
1 2 3 4 5 6 7 |
while (waitList.Contains(assetBundleName)) { await new WaitForNextFrame(); } // いろいろ処理 waitList.Remove(assetBundleName); |
await実装
今回はリソースLoadはすべてawaitとして実装しCallBackをなくしました。
7.リソースDownload
- http+複数スレッドDownload
- http2Download
- Downloadエラー処理
- Downloadキャンセルとリトライ処理
http+複数スレッドDownload
http + 複数スレッド
1 2 |
cpu数分スレッドを立ち上げ、Downloadを行う(SystemInfo.processorCountで取得可能) iphoneの場合はパフォーマンスが良いので、SystemInfo.processorCount以上設定しても大丈夫そうです。 |
http2 Download
Akamai 、Cloudfrontなどhttp2対応済みなので、http2のDownloadを検証しました。
MaxConcurrentStreams 128
InitialStreamWindowSize 65535
MaxFrameSize 16384
http 1スレッドの場合より 3 、4倍速い
http 8スレッドの場合より、遅い
多機種での検証はまだですが、機種またはCPU数によって、http、http2切り替えた方がよさそうでした。
CloudFrontの場合http2を有効にした場合、httpsでアクセスするとhttp2.0、httpでアクセスするとhttp1.1になっているようなので、たとえCPU数が4以下の場合はhttp2を使用し、CPU数が8以上の場合http(8スレッド)のほうが良いかも
しりません。
Downloadエラー処理
通信不安定などが原因でエラーが発生した場合のハンドリング
複数スレッドを考えなければならないので、エラー処理が発生した場合、次の処理をQueueに入れでスレッドに処理が入らないようにして、現在の実行中のスレッド処理を待つ必要があります。
全部のスレッド処理が完了したら、エラーハンドリングにエラー処理を任すように実装しました。
Downloadキャンセル処理とリトライ処理
Download中エラーが発生した場合、キャンセルまたはリトライ処理が必要になります。
今回はCallBackではなく、すべてをawait化にしましたので、特別な処理を行う必要があります。
もしAddressable Asset Systemのawaitを使用する場合も下記ようなキャンセル、リトライ処理実装が必要かもしりません。
8.Downloadフォルダー管理
- Unity Application.temporaryCachePath問題
- Android ファイルシステム問題
Unity Application.temporaryCachePath問題
iOSの場合システム側で勝手にApplication.temporaryCachePath(Library/Caches)のファイルを削除する場合があります。
そのため、Unity Cacheみたいに別フォルダーにDownloadして管理するほうが良いと思います。
たとえば:Library/Downloadフォルダーを作成し、リソースのDownload先はLibrary/Downloadにします。バックアップ対象外にするのを忘れずに
1 2 3 4 5 6 |
#if !UNITY_EDITOR && UNITY_IOS string dirPath = $"{Application.temporaryCachePath}/../Local/"; if (System.IO.Directory.Exists(dirPath)) { UnityEngine.iOS.Device.SetNoBackupFlag(dirPath); } #endif |
※Application.persistentDataPathにすることも可能ですが、あまりにもサイズが大きい場合はrejectになります。
Android ファイルシステム問題
Androidのファイルシステムは製造メーカー、AndroidOSバージョンによって、ファイルシステムの動作がかなり違いがあります。
一つのフォルダーに多いファイルが格納されている場合、端末によってGetFiles、CreateFileのパフォーマンスは5倍以上も差が出る場合があります。
それで、ファイルの名前をHash化しフォルダーを分けて保存したほうが良いと思います。
1 2 3 4 5 6 |
00/ 01/ . . . ff/ |
のような感じでフォルダーをを分けた方が良いと思います。
9.Sprite Atlasのアセットバンドル化
描画バッチ化のためUnityでよく使う2D Atlasは、Sprite Packerと Sprite Atlasがあります。
今回はSprite Atlasを使用することにしましたので、Asset Bundle化するための注意点を書きたいと思います。
Window -> 2D -> Sprite Packer
Sprite Packer Mode: Enabled For Builds または Always Enabledに設定します。
設定:
1.Include In Buildのチェックを外す
2.Allow Rotationのチェックを外す
3.Tight Packingのチェックを外す
ロード時注意点
AssetBundle化にしますので、
1 2 3 4 5 6 7 8 9 10 11 |
protected override void OnAwake() { SpriteAtlasManager.atlasRequested += OnAtlasRequested; } private async void OnAtlasRequested(string tag, Action<SpriteAtlas> action) { if (_uiSpriteAtlas == null) { _uiSpriteAtlas = await ResourceManager.LoadAssetAsync<SpriteAtlas>(COMMON_ATLAS_BUNDLE_NAME, false); } action(_uiSpriteAtlas); } |
で実装します。
SpriteAtlasManager.atlasRequestedはSprite Atlas化された画像がロードされるとき必要なAtlasを求める処理です。
注意点:
- 最初Requestを返した場合、_uiSpriteAtlasの参照が残っている限り2度requestは来ません。
再ロードをするためには、まずAssetBundleをUnloadし、且つResources.UnloadAsset(_uiSpriteAtlas);をする必要があります。
例えばリソースをStreamingModeからRemoteModeに変更する場合など必要です。 - SpriteAtlasManager.atlasRequested += OnAtlasRequestedの タイミング
この関数が登録される前に、どちらかのPrefabにAtlas画像の参照が入ってしまい、Requestを返すことができなかった場合2度Requestが来ないので、ゴミ画像が表示されてしまいます。
まとめ
Unity2019.3のリリースよりAddressable Asset Systemが正式にリリースされ、使いやすくなったと思いますが、
AssetBundle以外のファイルバージョン管理、一括Download機能、ダウンロードプロトコル(http2)切り替え機能が必要だったので、今回は自作することにしました。