【Unity】アセットバンドルのビルドと読み込み時のハッシュ管理について

# 確認環境
Unity 2020.2.0f1

# 内容

自分自身アセットバンドルは使ってきましたが、サーバなどからダウンロードしてアセットバンドルの読み込みをしたことがありませんでした。
streamingassetsから読み込んでいるとハッシュを気にしなくてよかったのですが、キャッシュを効かせるとなると**AssetBundleManifest**(アセットバンドルをビルドしたときに勝手に作られるフォルダー名と同じアセットバンドル)の**GetAssetBundleHash**を使用してハッシュの管理をしてもよいですが、ビルドしたマシンの影響でハッシュが変わったり不安定で、jenkinsなど1か所でビルドしていてもjenkinsPC自体が変わってしまった場合にすべてハッシュが変わってしまうのではと思ったり、ダウンロードサイズを表示しないといけないなどがあったので独自にアセットバンドル一覧の管理アセットを用意する必要があったので備忘録的に残しました。

もしかしたらAddressableだとこの辺りもしっかりしているのかもしれませんがAddressableのことは何も調べていません。

# 準備

アセットバンドル名も独自に付加してもよかったのですが今回は**Asset Bundle Browser**を使用しています。
unity 2020.1から**Asset Bundle Browser**などの一部のパッケージがPackage Manager（パッケージマネージャー）からインストールできなくなっているらしく[こちらの記事](https://helpdesk.unity3d.co.jp/hc/ja/articles/900002171066-Unity-2020-1-%E3%81%8B%E3%82%89%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%83%9E%E3%83%8D%E3%83%BC%E3%82%B8%E3%83%A3%E3%81%A7%E7%99%BA%E8%A6%8B%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%8F%E3%81%AA%E3%81%A3%E3%81%9F%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95)を参考にインストールしました。



![PackageManager01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175167/86f7c824-d0ee-966c-17eb-ccbc1da67dd8.png)

com.unity.assetbundlebrowserを入力して**Add**
![PackageManager02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175167/9363da1b-0633-4238-b8ae-def5cbd9a1de.png)

Window>AssetBundle Browserを開きアセットバンドルにしたアセットをドラッグ＆ドロップでアセットバンドル名が付与されます。
![assetbundlebrowser01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175167/4933efcb-2702-77c0-5454-5f47b0fd124e.png)


# ビルド
**Asset Bundle Browser**を使用してそのままビルドもできますが。
今回はアセットのサイズ、ハッシュを管理できるアセット(ScriptableObject)も生成するので独自でビルドすることにしました。

ビルドの手順は次の通りです。
1. アセットバンドルをビルド
2. ビルド後該当のアセットバンドルのサイズ、ハッシュを計算して管理用のScriptableObjectを生成
3. 2で生成した管理用のScriptableObjectをアセットバンドル化


#### アセットバンドル管理用のハッシュについて
アセットバンドルのサイズはFileInfoのLengthでとれると思いますがハッシュをAssetBundleManifestのGetAssetBundleHashで取得した場合マシンによって変わり不安定なので自分で作る必要がありました。
調べるとCRCという元のアセットが変わった場合のみ変更される一貫性のある値が存在したので
**アセットバンドル名＋CRC**をハッシュにすればいい感じになると思いこちらでハッシュを作ることにしました
＊**結局CRCもOSによって変わってしまっているPrefabが存在しましたが・・・** [^1]

#### CRC
CRCなのですがこちらはアセットバンドルをダウンロードする場合に引数で渡さないといけないものです。
（よくわからないので0にしてスキップさせますが）これは[マニュアルを見てみると](https://docs.unity3d.com/ja/current/ScriptReference/Networking.DownloadHandlerAssetBundle-ctor.html)
> インテグリティチェックのためにダウンロードしたデータと比較するためのチェックサムです。

正常なデータのチェックに使われているみたいですが今回はこちらをハッシュに使用します。

取得自体は

```cs
BuildPipeline.GetCRCForAssetBundle(assetsPath, out uint crc)
```
で取得します。

#### ハッシュ生成

アセットバンドルのダウンロード時のキャッシュはUnityEngine.Hash128で管理しています。

```cs

namespace UnityEngine
{
    public struct Hash128 : IComparable, IComparable<Hash128>, IEquatable<Hash128>
    {
        public Hash128(ulong u64_0, ulong u64_1);
        public Hash128(uint u32_0, uint u32_1, uint u32_2, uint u32_3);
    }
}
```
生成にはuintやulongで生成する必要があったのでcrcはuintでそのまま取得できましたがアセットバンドル名(string)をuintにする必要がありました。

アプローチとしては
**string→byte[]→uint**
に変換するのですが
**byte[]→uint**
これをそのまま変換してしまうとuint.Maxを超える場合があったのでSha1に変換してからuintにしました。

```cs

SHA1Managed sha1 = new SHA1Managed();
byte[] textBytes = Encoding.UTF8.GetBytes("assetName");
byte[] sha1Bytes = sha1.ComputeHash(textBytes);
uint nameInt = BitConverter.ToUInt32(sha1Bytes, 0);
```

```cs

Hash128 hash128 = new Hash128(crc, nameInt);
```
# 使用例(UniTask)

```cs

    public static async UniTask<T> LoadAssetBundle<T>(AssetBundleLoadData assetBundleLoadData) where T : UnityEngine.Object
    {
        string assetName = assetBundleLoadData.AssetName;
        CachedAssetBundle cachedAssetBundle = new CachedAssetBundle(assetName, Hash128.Compute(assetBundleLoadData.Hash128));
        string uri = "ここはURL";
        using (var request = UnityWebRequestAssetBundle.GetAssetBundle(uri))
        {
            request.downloadHandler = new DownloadHandlerAssetBundle(uri, cachedAssetBundle, 0);
            await request.SendWebRequest();
            var assetBundle = DownloadHandlerAssetBundle.GetContent(request);
            T obj = assetBundle.LoadAsset<T>(assetName);
            assetBundle.Unload(false);
            return obj as T;
        }
    }
```

# 今回のサンプルコード


<details><summary>アセットバンドルのサンプルビルドコード</summary><div>

```AssetsBundleBuilder.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;

public class AssetsBundleBuilder
{
    [MenuItem("Sample/AssetBundleBuild")]
    private static void AssetBundleBuild()
    {
        BuildTarget buildTarget = BuildTarget.Android;
        BuildAssetBundleOptions buildAssetBundleOptions = BuildAssetBundleOptions.DeterministicAssetBundle;
        string platformName = buildTarget.ToString().ToLower();
        string assetbundledatalistName = "assetbundledatalist" + platformName;
        string outputFolderPath = Path.Combine(Application.dataPath, "Scenes/AssetBundleScene/Output");     // 出力フォルダ
        string outputAssetbundlesFolderPath = Path.Combine(outputFolderPath, platformName);     // アセットバンドルビルド出力フォルダ
        CheckDirectory(outputFolderPath);
        CheckDirectory(outputAssetbundlesFolderPath);


        string[] assetbundleNames = AssetDatabase.GetAllAssetBundleNames();
        List<AssetBundleBuild> assetBundleBuildList = new List<AssetBundleBuild>();

        foreach (var item in assetbundleNames)
        {
            // アセットバンドル管理用のアセットバンドルは別でビルドする
            if (item.Contains(assetbundledatalistName)) continue;
            string[] assetsPaths = AssetDatabase.GetAssetPathsFromAssetBundle(item);
            AssetBundleBuild assetBundleBuild = new AssetBundleBuild();
            assetBundleBuild.assetBundleName = item;
            assetBundleBuild.assetNames = assetsPaths;
            assetBundleBuildList.Add(assetBundleBuild);
        }
        AssetBundleManifest assetBundleManifest = BuildPipeline.BuildAssetBundles(outputAssetbundlesFolderPath, assetBundleBuildList.ToArray(), buildAssetBundleOptions, buildTarget);


        // コンバート先のフォルダーからハッシュタグとbyteサイズを計算して。
        AssetBundleLoadDataList assetBundleLoadDataList = ScriptableObject.CreateInstance<AssetBundleLoadDataList>();
        foreach (var item in assetBundleManifest.GetAllAssetBundles())
        {
            FileInfo file = new FileInfo(Path.Combine(outputAssetbundlesFolderPath, item));
            long size = file.Length;
            Hash128 hash128 = GenerateHash128AssetNameCRC(outputAssetbundlesFolderPath, item, assetBundleManifest);
            assetBundleLoadDataList.SetAssetBundleLoadData(new AssetBundleLoadData(item, hash128, (uint)size));
        }


        // ScriptableObjectの作成はAssetsからのパスなので注意
        string filePath = Path.Combine(@"Assets/Scenes/AssetBundleScene/Output", assetbundledatalistName + ".asset");
        CheckDirectory(Path.GetDirectoryName(filePath));
        AssetDatabase.DeleteAsset(filePath);
        AssetDatabase.CreateAsset(assetBundleLoadDataList, filePath);
        AssetImporter importer = AssetImporter.GetAtPath(filePath);
        importer.assetBundleName = assetbundledatalistName;
        importer.SaveAndReimport();
        EditorUtility.SetDirty(assetBundleLoadDataList);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();

        AssetBundleBuild[] assetBundleBuildArray = new AssetBundleBuild[1];
        assetBundleBuildArray[0].assetBundleName = assetbundledatalistName;
        assetBundleBuildArray[0].assetNames = new string[] { filePath };
        BuildPipeline.BuildAssetBundles(outputAssetbundlesFolderPath, assetBundleBuildArray, buildAssetBundleOptions, buildTarget);

    }

    /// <summary>
    /// アセットバンドル名とCRCからアセットバンドルのハッシュを生成
    /// </summary>
    /// <param name="outputFolder"></param>
    /// <param name="assetName"></param>
    /// <param name="assetBundleManifest"></param>
    /// <returns></returns>
    private static Hash128 GenerateHash128AssetNameCRC(string outputFolder, string assetName, AssetBundleManifest assetBundleManifest)
    {
        string assetsPath = Path.Combine(outputFolder, assetName);
        if (BuildPipeline.GetCRCForAssetBundle(assetsPath, out uint crc))
        {
            SHA1Managed sha1 = new SHA1Managed();
            byte[] textBytes = Encoding.UTF8.GetBytes(assetName);
            byte[] sha1Bytes = sha1.ComputeHash(textBytes);
            uint nameInt = BitConverter.ToUInt32(sha1Bytes, 0);
            Hash128 hash128 = new Hash128(crc, nameInt);
            return hash128;
        }
        else
        {
            return assetBundleManifest.GetAssetBundleHash(assetName);
        }
    }

    private static void CheckDirectory(string outputFolder)
    {
        if (Directory.Exists(outputFolder)) return;
        Directory.CreateDirectory(outputFolder);
    }
}


```

</div></details>

<details><summary>AssetBundleLoadDataList.cs(管理用のScriptableObjectクラス)</summary><div>

```AssetBundleLoadDataList.cs
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class AssetBundleLoadDataList : ScriptableObject
{
    [SerializeField]
    private List<AssetBundleLoadData> m_AssetBundleLoadDataList;

    public AssetBundleLoadDataList()
    {
        m_AssetBundleLoadDataList = new List<AssetBundleLoadData>();
    }

    public void SetAssetBundleLoadData(AssetBundleLoadData assetBundleLoadData)
    {
        m_AssetBundleLoadDataList.Add(assetBundleLoadData);
    }

    public List<AssetBundleLoadData> GetAssetBundleLoadDataList()
    {
        return m_AssetBundleLoadDataList;
    }
}

[Serializable]
public class AssetBundleLoadData
{
    public string AssetName;
    public string Hash128;
    public uint Bytes;

    public AssetBundleLoadData(string assetName, Hash128 hash128, uint bytes)
    {
        AssetName = assetName;
        Hash128 = hash128.ToString();
        Bytes = bytes;
    }


    public override string ToString()
    {
        return $"<color=red>{AssetName}</color>\nHash128[ {Hash128} ]\nBytes[ {Bytes} ]";
    }
}


```

</div></details>

# おわり

様々な管理方法があるかと思います、今回初めてだったこともありもっと良い方法などもあるかと思いますが。
これからアセットバンドルのロードなどを実装する方たちに何かしらの参考になればと思います。


[^1]: 詳しくは調べられませんでしたがSpineなどのアセットはwindowsとmacでアセットバンドルをビルドした場合でもcrcが変更しました。同じOS内なら変更されなかったのでビルドするOSは統一させないといけなかったです。