【Unity】AssetBundleの取得から後続処理までのテンプレート

# 目的
勉強中のUnityプロジェクトで、APIを使ってDBからAssetBundleのファイル名を取得し、FireBaseaからAssetBundleをダウンロードする構成にしています。

FireBaseから取得する際の前準備や、複数取得した後の処理順を制御するためのソースコードがある程度テンプレート化できそうだったのでメモです。

# プロジェクト構成
ざっくりとした構成。サーバーサイドはGCPと書いていますが、今のことろローカル環境です。
Webアプリ側はLaravelで構築しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2796558/996507e6-ab4b-ebcf-3652-84804414b697.png)

# テンプレート

以下は「ユーザーが取得済みのユニット情報をもとに、FireBaseから画像のAssetBundleを取得する」例の処理です。
GS2ではモデルIDを「unit-XXXX」と振っており、DBでは数値でIDを振っているためIDの変換処理を挟みます。

| MySQL | GS2 |
|:---|---------:|
| 1  | unit-0001 |
| 2  | unit-0002 |
| 3  | unit-0003 |

## 大元の処理
``` C#
List<int> ids = new List<int>();
//Gs2のItemSetからID部分を文字切出しして数値に変換します。(プレフィックス部の「unit-」を除外するでも可)
arg1.ForEach(i => ids.Add(int.Parse(i.ItemName[^4..])));

//キャラのアセットバンドル情報をDBから取得。getAssetBundleInfoの中身は後述
List<AssetBundleJsonResponse> responses =
 await _requestService.getAssetBundleInfo(ids.ToArray());

//FireBaseからAssetBudle取得
foreach (AssetBundleJsonResponse item in responses){
   //FireBaseのストレージパスを取得する(テンプレ処理①)
   string uri = AssetBundlemanagereParam.CHARACTER_ASSET_PATH + "/" + item.assetbundle_name;
   string url = await _assetBundleManager.StorageFireBaseAsync(uri);
  
   //テンプレ処理②
   StartCoroutine(GetAssetBundleInitialize(item, url,
       AssetBundle bundle) => {
        //テンプレ処理③
        StartCoroutine(SettingTimeline(bundle, 
           "assets/prefabs/notestieline/parenttimeline/gametimeline.prefab",
           (int)res2.assetbundle_id,
            //テンプレ処理④
            (bool comp) => {
			//処理③が完了かつ全AssetBundle取得
            i++;
            if (comp && i >= responses.Count){
              //後続処理
              InitializeNotes();
            }
          }));
         }));
      }
```

## テンプレ処理①ーFireBaseのAssetBundle保存先パスを取得するー
こちらは他の記事でも記載したものです。
https://qiita.com/mijinko88/items/07c62bf47599f43e3ce8
https://atelier-hinata.hatenablog.com/entry/2020/07/27/140406#AssetBundle%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B

```
/**
     * FireBaseファイルストレージの初期化
     * @param assetBundlePath AssetBundleのストレージパス
     */
    public async Task<string> StorageFireBaseAsync(string assetBundlePath)
    {
        string uri = "";
        //アセットのまとめられたグループバンドル名。WEBサーバーからDB情報などで動的にする
        string storagePath = assetBundlePath;

        // ストレージアクセスインスタンスの取得
        var _storage = FirebaseStorage.DefaultInstance;
        // 作成したストレージのURIを指定
        var storage_ref = _storage.GetReferenceFromUrl(AssetBundlemanagereParam.REFEREBCE_FROM_URL);
        // ダウンロードしたいAssetBundleのストレージ内におけるパスを指定
        var targetAsset = storage_ref.Child(storagePath);


        // AssetBundleのURIを取得
        await targetAsset.GetDownloadUrlAsync().ContinueWith((Task<Uri> fetchTask) =>
        {
            if (!fetchTask.IsFaulted && !fetchTask.IsCanceled)
            {
                // 取得成功
                uri = fetchTask.Result.AbsoluteUri;
            }
            else
            {
                Debug.Log("await: " + fetchTask.Status);
                Debug.Log(storage_ref);
                Debug.Log(targetAsset);
            }
        });
        return uri;
    }
```

## テンプレ処理②ーAssetBundleファイルのダウンロードー
生成したパスを利用し、実際にFireBaseからAssetBundleをダウンロードしてくる処理です。
ダウンロード後にDownloadAssetBundleAndAddListという自作メソッドで、staticなリストに「アセットIDとAssetBundle」のDictionaryを保持しておき、ゲーム内でダウンロード済みのものはDictionaryから呼び出せるようにします。
処理判定後に呼び出しているcomplateが、テンプレ処理③に続きます。

``` C#
    /// <summary>
    /// ①AssetBundleのIDを分析してURLを取得する
    /// </summary>
    /// <param name="assetBundleJsonResponse">AssetBundle情報</param>
    /// <param name="uri">FireStorageで生成した取得パス</param>
    /// <param name="complate">処理完了フラグの返答アクション</param>
    /// <returns></returns>
    /// <exception cref="AssetBundleNullException"></exception>
    public IEnumerator GetAssetBundleInitialize(AssetBundleJsonResponse assetBundleJsonResponse, string uri, Action<AssetBundle> complate)
    {
        var crc = 0U;
        using (UnityWebRequest uwr = UnityWebRequestAssetBundle.GetAssetBundle(uri, version: 0, crc))
        {
            yield return uwr.SendWebRequest();
            if (uwr.isNetworkError || uwr.isHttpError)
            {
                Debug.Log($"AssetBundleのダウンロードに失敗しました: {uwr.error}");
                Debug.Log(uri);
                complate(null);

                yield break;
            }
            else
            {
                // アセットがダウンロードできたかチェック
                AssetBundle bundle = DownloadAssetBundleAndAddList(uwr, (int)assetBundleJsonResponse.assetbundle_id);

               //ダウンロードに成功していたら後続処理へアセット情報を渡す
                if (bundle != null)
                {
                    complate(bundle);
                }
                else
                {
                    Debug.LogFormat("AssetBundle: {0} is NULL", assetBundleJsonResponse.assetbundle_id.ToString());
                    complate(null);
                }
            }
        }
    }
```

## テンプレ処理③ー取得したAssetBundleで必要な処理を実行ー
今回の例では、取得したTimelineアセットをシーン上にインスタンス化するものになります。
complateの引数で渡されたAssetBundleを使用し、その中から欲しいアセットをロードします。

SetDownloadedAssetBundleでstaticなDictionaryに登録していますが、処理②の方で終わらせてもいいと思います。
終わったらまた別のアクションcomplateで、メソッド完了の判定フラグを返します。

``` C#
 private IEnumerator SettingTimeline(AssetBundle assetBundle, string assetName, int assetId, Action<bool> complate)
    {
        // PlayerDirecterオブジェクトをインスタンス化
        string assetName2 = assetBundle.GetAllAssetNames()[0];
        var prefab = assetBundle.LoadAssetAsync<GameObject>(assetName2);

        yield return prefab;

        //取得したAssetBundleはリストに保持
        AssetBundleManager.SetDownloadedAssetBundle((int)assetId, assetBundle);

        GameObject instans = Instantiate((GameObject)prefab.asset, new Vector3(0, 0, 0), Quaternion.Euler(0, 0, 0));
        instans.name = prefab.asset.name;
        playableDirector = instans.GetComponent<PlayableDirector>();

        complate(true);
    }

```

## テンプレ④ーすべてのAssetBundle処理が完了したかの判定と後続処理ー
大元の処理で最後に以下のような判定がありましたが、こちらはすべてのAssetBundleのロード、インスタンス化処理が完了したことを確認したうえで後続処理を実施する箇所になります。
（例：インスタンス化したアセットをGameObject.Findして初期化処理を実行するなど）

DBからアセットファイル名を取得するのもFIreBaseからダウンロードするのも非同期のため、判定をせずそのまま進めようとするとエラーになる可能性があります。

テンプレ処理③の最後にcomplate(true)を受取り、取得対象のAssetBundleの完了をカウントしていきます。
最後にカウント数がロードしたいAssetBundle数になっていれば後続処理に入れるようになります。
complate(null)など、失敗判定が返ってきていた場合はエラーを出すか、その場で無視していいものだった場合は何かしらのカバー処理も必要になります。


``` C#
 //テンプレ処理④
            (bool comp) => {
			//処理③が完了かつ全AssetBundle取得
            i++;
            if (comp && i >= responses.Count){
              //後続処理
              InitializeNotes();
            }
```

処理ごとに変更する箇所としては主に以下になります。
- 大元との処理の「uri」
- テンプレ処理③の個別メソッドの内容（最後にAction<bool>を引数に持つのは共通）
- テンプレ④の後続処理の内容

テンプレ①、②の内容も共通処理として別クラスで定義しておくことで実装は楽になると思います。