テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】AssetBundleに実際に含まれるアセット一覧を確認する

f:id:tsubaki_t1:20190328095213j:plain

AssetBundleに含めたアセットやシリアライズしたデータ一覧を確認出来る機能を作ってみました。

AssetBundleに自動で含まれるアセット

AssetBundleは、AssetBundle NameやAddressを設定したアセットが参照するアセット…例えばキャラクターにおけるAnimationやMaterialにおけるTexturerを自動的に含みます。これにより開発者側はPrefabを展開する上で必要なアセット群を全て把握する必要なく、Prefabが使えます。

ただし、意図通りではないアセットも含まれる可能性があり、それがAssetBundleの利用面で色々と問題を起こしやすい部分でもあります。

https://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20181224/20181224030957.jpg

AssetBundle.LoadAsset(...) や Manifest に記載されているアセットは、コンテナとして登録されているアセットで、こういった暗黙的に参照され含まれるAssetBundleを確認出来ません。

AssetBundleに含まれるアセットを全て確認する

f:id:tsubaki_t1:20190328093839g:plain

コンテンツの中身を全て確認します。

下のコードをEditorフォルダ以下に格納した後、 AssetBundleを右クリック→AB/Content Checker でABの中身を確認出来ます。

テクスチャの中身等やモデル等も確認が可能です。

using System.Collections.Generic;
using UnityEditor;
using System.Linq;
using UnityEngine;

public class Checker : EditorWindow
{
    List<Object> allObjects = new List<Object>();
    Vector2 pos;

    private void OnGUI()
    {
        using (var scroll = new EditorGUILayout.ScrollViewScope(pos))
        {
            pos = scroll.scrollPosition;
            foreach (var obj in allObjects)
            {
                EditorGUILayout.ObjectField(obj, obj.GetType(), false);
            }
        }
    }

    [MenuItem("Assets/AB/Content Checker")]
    static void Open()
    {
        var window = CreateInstance<Checker>();
        window.ReadContent();
        window.Show();
    }

    void ReadContent()
    {
        AssetBundle bundle = null;
        try
        {
            bundle = AssetBundle.LoadFromFile(Application.dataPath + AssetDatabase.GetAssetPath(Selection.activeObject).Remove(0, 6));

            if (bundle != null)
            {
                SerializedObject so = new SerializedObject(bundle);

                foreach (SerializedProperty content in so.FindProperty("m_PreloadTable"))
                {
                    allObjects.Add(content.objectReferenceValue);
                }

                allObjects = allObjects
                    .Where(c => c != null)
                    .Where(c => c is Component == false )
                    .Where(c => c is GameObject == false)
                    .Distinct().OrderBy(c => c.name).ToList();
                bundle.Unload(false);
            }
            else
            {
                Debug.LogWarning("this is not assetbundle?");
                Close();
            }
        }
        finally
        {
            if( bundle != null)
                bundle.Unload(false);
        }
    }
}

例えば下のように、アドレスとしてはPrefabとAnimatorControllerのみを設定しています。

これをチェッカーで見ると、Prefabが暗黙的に参照している内で、同じAssetBundleに格納されている全てのアセットを確認出来ます。

f:id:tsubaki_t1:20190328095830j:plain

またSprite Atlasでの動き等もココで確認しやすいです。下の画像を見ると、Sprite AtlasとAssetBundleを組み合わせる場合、SpriteAtlasに登録したAssetBundleを全て同じAssetBundleに登録している場合、含まれるアセットはパック済みTextureのみである事が確認出来ます。

f:id:tsubaki_t1:20190328100839j:plain

現状の問題

  • シェーダーが上手く取れません。

  • 他のAssetBundleをLoadしていると、開いているAssetBundleに含まれるアセットも表示されるかもしれません。

CSVで出力もついてでに作ってみる

複数のAssetBundleの中身を一気に比較する用でCSV出力もつけてみました。

f:id:tsubaki_t1:20190328102218g:plain
Default-SpriteがAssetBundleに複製格納されているせいで、バッチが破壊されるの図

using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEngine;
using System.Linq;

public class ExportCSV : MonoBehaviour
{
    [MenuItem("Assets/AB/ExportCSV")]
    static void Export()
    {
        var valuePairs = new Dictionary<string, string>();
        StringBuilder builder = new StringBuilder();

        foreach (var t in Selection.objects)
        {
            AssetBundle bundle = null;
            try
            {
                bundle = AssetBundle.LoadFromFile(Application.dataPath + AssetDatabase.GetAssetPath(t).Remove(0, 6));
                if (bundle != null)
                {
                    builder.Append($"{bundle.name}, ");
                    SerializedObject so = new SerializedObject(bundle);

                    List<Object> objs = new List<Object>();
                    foreach (SerializedProperty content in so.FindProperty("m_PreloadTable"))
                    {
                        objs.Add(content.objectReferenceValue);
                    }

                    objs = objs
                       .Where(c => c != null)
                       .Where(c => c is Component == false)
                       .Distinct().OrderBy(c => c.name).ToList();

                    foreach (var obj in objs)
                    {
                        builder.Append($"{obj.name}({obj.GetType()}), ");
                    }
                    builder.AppendLine();
                }
            }
            finally
            {
                if (bundle != null) bundle.Unload(true);
            }
        }

        System.IO.File.WriteAllText("ab content.csv", builder.ToString());
    }
}

感想

エディター基本機能を少し拡張しましたが、案外悪くないかも? ABに含まれるテクスチャの確認が楽で良いです。

それとFBXにAssetBundle Nameを含めることでどんだけ無駄なゴミが増えるのかを確認出来ます。

関連

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp

【Unity】AddressableAssetSystemで、他のプロジェクトのAASが作ったAssetBundleを利用する

f:id:tsubaki_t1:20190326225814j:plain

Addressableで他のプロジェクトが作成したAssetBundleを使用する方法についてです。

内容

  • AssetBundleを「上書き」する場合は、ContentCatalogを上書きするだけでいい
  • "AssetBundleに含まれるPrefab"が参照するスクリプトは利用する側に移植する
  • AssetBundleを「追加」する場合は、ContentCatalogをロードしてResourceLocationsに追加
  • Builtinshader assetbundleが重複する場合は、ビルドから外す

Addressableで他のプロジェクトが作成したAssetBundleを使用したい

他のプロジェクトのAddressableが作成したAssetBundleを 使用する というのであれば、実はそれ程難しくはありません。 ContentCatalogを他のプロジェクトが作った物に差し替えるだけです。

ただし、幾つか制限が追加されます。

  • Play Mode Script の Fast Mode や Virtual Mode は使用できない。
  • AssetReference系のAPIは使用できない

このあたりは完全にローカルにアセットが無いことが理由なので、仕方がないといえば仕方がないと言えます。

何にせよやってみようと思います。

なお今回はせっかくなので新しくリリースされたSDトーコちゃんズを使ってみました。Spring Boneが新型になり、ShaderもUTS2ベースになったので非常に良い感じです*1

f:id:tsubaki_t1:20190326230855p:plain

SDトーコちゃんズ登場! | UNITY-CHAN! OFFICIAL WEBSITE

Addressableのビルド用プロジェクトを用意する

今回は「ビルド用」と「実行用」のUnity Projectを用意します。どちらもAddressableは導入している状態です。

まずビルド用では、普通にAddressableを用意する環境を用意して Build Player Content します。

f:id:tsubaki_t1:20190326233600j:plain

ビルドした後は、以下の内容をexport packageしておきます。

  • StreamingAssets の中身・・・特に aaフォルダ 以下の内容
  • AssetBundleに含めたPrefabが使用しているscript全て

スクリプトはファイルのGUIDが同じ事、そしてハッシュが同じ(シリアライズ可能な設定が一致)なら、実装違っていても普通に使用できます。なので他のプロジェクト側でAssetBundleを使用したいならば、スクリプトも持っていく必要があります。

持っていくスクリプトはlink.xmlで確認するのが手っ取り早いです。

f:id:tsubaki_t1:20190326233216j:plain

【Unity】AddressableでAssetBundleをビルドすると link.xml も自動で生成される - テラシュールブログ

Addressableでビルド済みAssetBundleを使用する

次にビルド済みのAssetBundleを他のプロジェクトで使用します。

先程Exportした Unitypackageを「AssetBundleを利用するUnityProject」にインポートします。

重要なのは2つ。ContentCatalogデータと、catalogと同じパスにAssetBundleを配置することです。CatalogデータにABへのパス等が含まれるので、この2つさえ移植できれば概ね何とかなります。

あとは play mode scriptpack modeに設定すれば、他のAddressableが生成したAssetBundleを使用出来ます。

f:id:tsubaki_t1:20190326233952j:plain

public class LoadAddress : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Addressables.Instantiate("Toko_sum");
    }
}

f:id:tsubaki_t1:20190326234305j:plain

既存のAddressableを使いつつ、動的にカタログを追加する

既にローカルでAddressableを使用して云々している場合で、既存のプロジェクトに外部で作成したカタログを追加してみます。

例えば以下のような形でカタログを新しく追加出来ます。

using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Assertions;
using UnityEngine.ResourceManagement.AsyncOperations;

public class LoadAddress : MonoBehaviour
{
    IEnumerator Start()
    {
        // Webから新しいカタログを取得。ファイルパスかURL
        var requestCatalog = Addressables.LoadContentCatalog("http://192.168.100.100:51879/catalog_extra.json.json");
        yield return requestCatalog;
        Assert.AreEqual(AsyncOperationStatus.Succeeded, requestCatalog.Status);

        // カタログを追加
        Addressables.ResourceLocators.Add(requestCatalog.Result);

        // 新しいカタログ上のキャラクターを取得
        Addressables.Instantiate("Toko_sum");
    }
}

ただ、お互いに default shader保有しているためにAssetBundleがぶつかる事があるかもしれません。そういった場合は、ビルドスクリプトを少し書き換えてやります。

BuildScriptBase を継承したクラスを作成し、BuildScriptPackedModeの中身をほぼコピペで新しいビルドスクリプトを作ります。LinkXmlGeneratorなど一部internalがあったので Addressables System/Editorフォルダ以下に配置しました。

コードの変更点として、Built in shaderを作るタスクをビルドタスク一覧からコメントアウトで排除します。

f:id:tsubaki_t1:20190327003959j:plain

f:id:tsubaki_t1:20190327004345j:plain

ScriptableObjectを作成し、Data Buildersに登録、あとはBuild Scriptをカスタマイズした物に変更します。

f:id:tsubaki_t1:20190327004527j:plain

f:id:tsubaki_t1:20190327004643j:plain

これでローカルでは普通にAddressableを使用しつつ、外部で任意にカタログを追加して云々が出来ました。外部アセット(キャラクターや演出等)は外部でビルドしつつ、UIや幾つかのエフェクト等はローカルAddressableAssetSystemを使うとかすると良いかもしれません。

f:id:tsubaki_t1:20190327005239j:plain

f:id:tsubaki_t1:20190327005443j:plain

感想

AddressableはContentCatalogにアセットへのパスが含まれるので、コレをなんとかすれば割と素直に動いてくれます。 まだ未確認ですが、ContentCatalogをリロードしたい場合、一旦全てのアセット/インスタンスをリリースしてからコンテンツカタログを再ロードとかすると良いんじゃないかなと思ってます。変更チェックには.hash を使ってあげると良いかも。

ということで他のAASが使用したAssetBundleを使用してみましたが、Addressableは設計的には「全部のアセットを一つのプロジェクトに突っ込んで使う」「コンテンツ作成はFastモードで行い、CIでコンテンツ更新、動作確認は実機」だと思うので、ABを通さないと動作確認が出来なくなるモードは若干微妙な気分です。

関連

www.slideshare.net

www.slideshare.net

*1:インポート時にGUIDが変更されていなかったため、SD Unitychanが既にある場合はGUIDが重複して新しいSpringBoneが外れる現象が起こりました。

【Unity】AddressableでAssetBundleをビルドすると link.xml も自動で生成される

f:id:tsubaki_t1:20190317204037j:plain

AddressableでAssetBundleをビルドしている場合、自動的にlink.xmlが生成されます。

Addressableがlink.xmlを生成する

Addressableをビルドする際、StreamingAssetsに幾つかのファイルを自動的に生成します。

f:id:tsubaki_t1:20190317204539j:plain

このlink.xmlですが、AssetBundleに含まれるべきアセンブリの一覧が登録されています。なので、ストリップ対策のlink.xmlがまだ無ければそのまま、既にあるなら生成されたlink.xmlの中身を移植することで、AssetBundleでしか使わないコードが剥がされるのを回避出来ます。

f:id:tsubaki_t1:20190317204808j:plain
AssetBundleでしか使わないコードも自動で追加される

なおlink.xmlファイルはStreamingAssetsに追加されてしまうので、出来ればAssets以下に移動したほうが良いです。

link.xmlを作成しているコード

作成しているのは、BuildScriptPackedMode.BuildData(...)です。

正しくはこのコードにAddressableのビルド作業は大体含まれます。ContentPipeline.BuildAssetBundles()が出力するIBundleBuildResultsの中にアセンブリが全部入ってるので、ソレをlink.xmlに出力している感じです。Scriptable Build Pipeline様様です。

f:id:tsubaki_t1:20190317211038j:plain

関連

BuildScriptPackedMode.BuildDataを読むなら知っておいたほうが良いかも

tsubakit1.hateblo.jp

link.xmlの問題について

tsubakit1.hateblo.jp

SRB以前(マニフェスト)を使用していた場合

tsubakit1.hateblo.jp

【Unity】暗号化したAssetBundleはLoadFromStreamでロードすればメモリに優しい

暗号化したAssetBundleを、「メモリに一旦全展開してからの復号」ではなく、メモリ負荷的に優しい「Streamで復号しながら読む」アプローチについてです。

AssetBundleを暗号化でAssetBundle.LoadFromMemoryを使うとメモリに優しくない

DeNA TechCon 2019のUnity 2018-2019を見据えたDeNAのUnity開発のこれからにて、AssetBundleを暗号化した場合でもStreamに乗せれば比較的低い負荷でロード出来る事が紹介されました。

f:id:tsubaki_t1:20190316143007p:plain

以前、暗号化されたAssetBundleは一旦データをメモリ上で復号してからの AssetBundle.LoadFromMemory(...) という形で復号する事が多かったのですが、これはヒープにメモリを全て乗せるという都合上、幾つかデメリットがありました。

  • AssetBundleを展開しっぱなしに出来ない
  • 全てのデータをロードしなければ使用できない
  • 復号前とAssetBundleの両方のメモリ負荷が計上されてあまりよろしくない
  • ヒープが伸びる

今回はソレを回避するアプローチです。

AssetBundle.LoadStreamで少しずつ復号する

AssetBundle.LoadFromStream( Stream )は、.NETのStreamを通してAssetBundleをロードするAPIです。

上手く使えば割と色々な事が出来る反面、利用には幾つかの条件があり中々に手軽に使うには面倒くさいという性質もありました。特にstream.CanSeekがTrueを返す暗号化Streamは無かった気がするので、ソコの所は自作する必要があります。

tsubakit1.hateblo.jp

暗号化に対応したStream

自作が面倒だったので探した所、ありました。 念の為コードは写しておきますが、オリジナルはコチラから確認出来ます。

https://gist.github.com/tsubaki/4c15f08f592fc303cce97f13c686243f

使用する場合は、下のようなコードになります。Seekが可能なので、AssetBundle.LoadFromStreamでも使用できます。

void test()
{
        // bufの中身を1 ~ 255の数値で埋める
        var buf = new byte[255];
        for (byte i = 0; i < buf.Length; i++)
            buf[i] = i;

        //暗号化したデータをbaseStream に書き込む
        var uniqueSalt = new byte[16]; //** WARNING **: MUST be unique for each stream otherwise there is NO security
        var baseStream = new MemoryStream();
        var cryptor = new SeekableAesStream(baseStream, "password", uniqueSalt);
        cryptor.Write(buf, 0, buf.Length);

        //200までシークして、200から50まで復号
        // 200 ~ 250の数値がdecryptedBufferに格納される
        cryptor.Position = 200;
        var decryptedBuffer = new byte[50];
        cryptor.Read(decryptedBuffer, 0, 50);
}

内容は多分、一定のメモリをブロックにして暗号化というものだと思われます。

あとはコレを使ってAssetBundleを暗号化し、ロードします。

AssetBundleの構築

AssetBundleを構築は下のようなコードにしました。あまり汎用的ではないですが、サンプルです。お察しください。 今回は普通にAssetBundle Nameで設定しています。

f:id:tsubaki_t1:20190316161611j:plain

内容は何も考えずAssetBundleを暗号化して「e + assetbundle名」という名前で保存します。

f:id:tsubaki_t1:20190316161808j:plain

gist.github.com

なお、AssetBundleの圧縮フォーマットは必ず「LZ4(ChunkBasedCompression)」もしくは「無圧縮(UncompressedAssetBundle)」を使用します。何も指定しないとLZMAが使用され、メモリに全展開されます。

tsubakit1.hateblo.jp

暗号化したAssetBundleの利用

AssetBundleを復号してロードします。当然ですが、パスワードとsoltは暗号化した値と同じものを使用する必要があります。

gist.github.com

上でも書きましたが、このアプローチはLoadFromMemoryと比較してメモリに優しいです。試しに合計120MBくらいになる量のTextureを暗号化してAssetBundle化、復号をしてみても、Reserved Monoのメモリは殆ど増えませんでした。

f:id:tsubaki_t1:20190317044011j:plain

感想

今回は暗号化を使いましたが、単純に「簡単にAssetBundleの中身を見られたくない」といったモチベーションならば、もっと簡単なやり方はありそうな気がします。バッファの中身を少しズラすとか、最初のブロックだけ暗号化とか、それだけでカジュアルにアセットをハックされにくくなると思います。この辺りは手間との相談になりそうな

次はAddressableと組み合わせたい所

関連

LoadFromStreamの説明

tsubakit1.hateblo.jp