2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-03-14

確認環境

Unity 2020.2.0f1

内容

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

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

準備

アセットバンドル名も独自に付加してもよかったのですが今回はAsset Bundle Browserを使用しています。
unity 2020.1からAsset Bundle Browserなどの一部のパッケージがPackage Manager(パッケージマネージャー)からインストールできなくなっているらしくこちらの記事を参考にインストールしました。

PackageManager01.PNG

com.unity.assetbundlebrowserを入力してAdd
PackageManager02.PNG

Window>AssetBundle Browserを開きアセットバンドルにしたアセットをドラッグ&ドロップでアセットバンドル名が付与されます。
assetbundlebrowser01.PNG

ビルド

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

ビルドの手順は次の通りです。

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

アセットバンドル管理用のハッシュについて

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

CRC

CRCなのですがこちらはアセットバンドルをダウンロードする場合に引数で渡さないといけないものです。
(よくわからないので0にしてスキップさせますが)これはマニュアルを見てみると

インテグリティチェックのためにダウンロードしたデータと比較するためのチェックサムです。

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

取得自体は

BuildPipeline.GetCRCForAssetBundle(assetsPath, out uint crc)

で取得します。

ハッシュ生成

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


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にしました。


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);

使用例(UniTask)


    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;
        }
    }

今回のサンプルコード

アセットバンドルのサンプルビルドコード
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);
    }
}


AssetBundleLoadDataList.cs(管理用のScriptableObjectクラス)
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} ]";
    }
}


おわり

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

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?