AssetBundle取得まとめ

# つまずいていたこと
UnityでビルドしたAsseBundleをサーバから取得する処理を書いていたことがありましたが、久々にそのプロジェクトを開いたときに「何をどうやっていたのか」忘れていたのでビルド設定から実際の取得までの流れをメモ。

個人的に集めた情報のまとめです。

# 準備するもの
ビルドするときはAddresableではなく、Asset Bundles Browserを使用しています。

- 導入手順は公式マニュアル参照。
https://docs.unity3d.com/ja/2021.3/Manual/AssetBundles-Browser.html

- URLからのインストールでGitのエラーが出る場合、以下で解消しました。
　https://www.hanachiru-blog.com/entry/2021/05/24/120000

- ストレージサーバとしてFireBase利用
　Unity用にパッケージが用意されているので参考にしながらセットアップ
　https://firebase.google.com/docs/unity/setup?hl=ja


# Unity側の設定箇所（Asset Bundles Browser）

■Window>Asset Bundles Browser

Condigure：
　ビルドされるアセット一覧を確認。
　ファイルサイズや依存先アセットが共通するものがある場合に警告なんかも出してくれます。

Build：
　ビルド時の設定と、ビルド処理を行う。
　AppendHashでビルド成果物にハッシュ値をつけるようにしています。
　DBで最新ハッシュ値を管理し、Webサーバーを介してどのビルドファイルを取ってくるか
　クライアントが動的に判断できるようにする予定。　

  ビルド成果物はいったんローカルに出力し、手動でFireBase上にアップロードしています。


# Addresableを使っていた時の修正箇所（メモ）

設定周りはAddresableの画面で行いました。
■Window>アセット管理>Addresables>Profiles

サーバーから取得する前提だったので「Remote」のパスを修正
 ①Remote：Custom
 ②Remote.BuildPath：ローカルの任意の箇所のフルパス
 ③Remote.LoadPath：FireBaseのストレージフォルダを設定。手動でアップロードしているのであまり関係なさそう
 ④BuildTarget：デフォルトのまま

■Window>アセット管理>Addresables>設定

「Content Update>Build&LoadPaths」をRemoteに設定

■Window>アセット管理>Addresables>グループ

ビルド成果物単位でグループを作成します。
バンドルダウンロード後に、タグ指定してロードする方法もあるようで、一応タグ付けなどもしています。



最初はこの画面でビルドしていましたが、ビルド単位を小さくしようと思うとグループの作成が面倒だったので、フォルダごとでそのままグループ分けしてくれるAsset Bundles Browserでビルドします。


# FireBaseからAssetBundleを取得、利用する処理

大まかな処理の流れは以下。

１．FireBaseの初期化
２．FireBase認証処理
３．AssetBundleファイルの取得
４．バンドル内からアセットを抽出

## １．FireBaseの初期化
FireBase.Appのパッケージをインポートしておく必要があります。
```C#
private void InitFireBase()
    {
        Debug.Log("初期化開始");
        // 初期化処理
        Firebase.FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
        {
            var dependencyStatus = task.Result;
            if (dependencyStatus == Firebase.DependencyStatus.Available)
            {
                // 成功
                Debug.Log("FireBase初期化成功");
                init = true;
                AuthFireBase();
            }
            else
            {
                // 失敗 
                Debug.Log("FireBase初期化失敗");
                init = false;
            }
        });
        Debug.Log("初期化中…");
    }
```

## ２．FireBase認証処理

FireBase.Authのパッケージをインポートしておく必要があります。

```C#
private void AuthFireBase()
    {
        string email = "FireBaseのストレージアクセス用アカウントのメールアドレス";
        string password = "pass";
        var _auth = FirebaseAuth.DefaultInstance;

        Debug.Log("認証開始");
        _auth.SignInWithEmailAndPasswordAsync(email, password).ContinueWith(authTask =>
        {
            if (authTask.IsCanceled)
            {
                Debug.LogError("Auth処理がキャンセルされました");
                auth = false;
                return;
            }
            if (authTask.IsFaulted)
            {
                Debug.LogError("Auth処理が失敗しました: " + authTask.Exception);
                auth = false;
                return;
            }
            FirebaseUser newUser = authTask.Result;
            Debug.Log("Auth処理に成功しました");
            auth = true;
           //認証まで完了した後アセットダウンロードボタンが押せるようにしている
            startDownloadButton.interactable = true; 
        });
        Debug.Log("認証中…");
    }
```

## ３．AssetBundleファイルの取得
```C#
   //ダウンロードボタンを押したときに呼び出す処理
    public async void DownloadStartAssetAsync()
    {
        //FireBaseから画像アセット取得
        if (init && auth)
        {
            Debug.Log("ダウンロード開始");
            //取得対象のAssetBundleファイル名を配列から動的に設定していた名残で引数があります
            string url = await StorageFireBaseAsync(0);
            StartCoroutine("LoadAssetPrefab", url);
        }
    }
```

まずはAssetBundleファイルをダウンロードするため、ダウンロードURLを取得します。
```C#
private async Task<string> StorageFireBaseAsync(int i)
    {
        string uri = "";
        //アセットのまとめられたグループバンドル名。WEBサーバーからDB情報などで動的にする
        string storagePath = "FireBaseのルートパス以降のAssetBundleファイル名";

        // ストレージアクセスインスタンスの取得
        var _storage = FirebaseStorage.DefaultInstance;
        // 作成したストレージのURIを指定
        var storage_ref = _storage.GetReferenceFromUrl(referenceFromUrl);
        // ダウンロードしたい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);
            }
        });
        return uri;
    }
```

アセットの抽出
```C#
private IEnumerator LoadAssetPrefab(string uri)
    {
        Debug.Log(uri);
        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}");
                yield break;
            }
            else
            {
                // ダウンロード成功。ここから使いたいアセット抽出処理
                AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(uwr);
                //AssetNameを指定し、ダウンロードしたバンドルから抽出。
                var assetPrefab = bundle.LoadAssetAsync<Texture2D>(urls[0] + "0101-party");

                yield return assetPrefab;

                //TextureをSpriteとして使用する場合、一度スプライトとしての生成処理を挟みます。
                Texture2D t = (Texture2D)assetPrefab.asset;
                Sprite s = Sprite.Create(t, new Rect(0, 0, t.width, t.height), new Vector2(0, 0));
                partyCharacterImages[0].sprite = s;

                Debug.Log("ダウンロード完了");

                // AssetBundleのメタ情報をアンロード
                bundle.Unload(false);
            }
        }
    }
```

非同期処理のため若干遅れて画面に反映される感じです。
オブジェクトをインスタンス化したい場合、一度PrefabにしたものをAssetBundleに入れてビルドし、
バンドルからの取得後にInstantiateすれば問題ありません。

一度ダウンロードしたアセット（またはAssetBundleファイル自体）は、
クライアントアプリ起動中は再ダウンロードしないよう、
辞書リストに入れてそこから参照するようにします。
staticクラスで管理してシーンを跨いで保持できるようにします。

```C#
    public static Dictionary<string, AssetBundle> LoadedAssetBundleList = new Dictionary<string, AssetBundle>();

     /*AssetBundleのロード*/
    public void LoadAssetBundle(UnityWebRequest uwr, string key, bool allUnjectUnloadFlag)
    {
        AddLoadedAassetList(key, uwr);
    }

    /**
    * @param key fireBaseの取得済みuri 
    */
    public void UnloadAssetbundle(string key, bool allSceneUnloadFlag)
    {
        //2重インスタンス化防止のためのアンロード
        LoadedAssetBundleList[key].Unload(allSceneUnloadFlag);
        LoadedAssetBundleList.Remove(key);
    }

    /**
    * 新規ダウンロードの場合、リストに追加
    * @param key リストのキーになるURL
    * @param uwr ダウンロード処理用のURL
    */
    public void AddLoadedAassetList(string key, UnityWebRequest uwr)
    {
        if (!LoadedAssetBundleList.ContainsKey(key))
        {
            AssetBundle asset = DownloadHandlerAssetBundle.GetContent(uwr);
            LoadedAssetBundleList.Add(key, asset);
        }
    }

    /**
    * 取得済みのアセットはリストから取得
    */
    public AssetBundle GetLoadedAssetBundleByKey(string key)
    {
        return LoadedAssetBundleList[key];
    }

```

# 処理に関する参考サイト
https://satococoa.hatenablog.com/entry/2017/10/25/160043

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

https://qiita.com/k7a/items/d27640ac0276214fc850

https://stackoverflow.com/questions/47384980/issue-with-unity-tasks-in-the-ios-firebase-unity-plugin