テラシュールブログ

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

【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

【Unity】Addressableのローカルサーバーを使って、AssetBundleをダウンロードする動作を確認する

今回はAddressableのホスティングサービスを試します。

本記事は Addressable Asset System ver 0.6.7 preview を使用しました。

動作確認にサーバーを用意するのが面倒くさい

Addressable(Addressable Asset System)は概ね外部リソースと内部リソースの区分無く使えるシステムです。またAssetBundleを構築せず動作を確認出来るという点で非常に手軽に変更を確認出来ます。

とはいえ、AssetBundleを実際にビルドしないと何らかの不具合を確認出来ない事もあります。また、外部リソースにはダウンロードという手順が必ず存在するため、そういった部分の確認にも出来ればアセットを配置するサーバーを立てて実際に確認したい所です。

なので、ローカル環境にサーバーを立てて動作を確認するホスティングサービスの機能を使ってみます。ファイルはサーバー上に配置し、Addressable経由でファイルをダウンロード、使用するという感じにする訳です。

手順1:動作確認

Addressableの最低限のセットアップ(パッケージのインポート及び初期アセットの登録的なもの)はスキップします。

強いて言うならグループの出力先はリモートに設定しておきます。

f:id:tsubaki_t1:20190313114024j:plain

さて、最初にやるべきは動作確認です。Packは最も厳密な動作なので事前に Virtual Mode で動作を確認し、スクリプト的にちゃんと動作することを確認します。今回はキャラクターを1体表示するだけの機能を試してみました。実行すると、ユニティちゃんのPrefabが表示されます。

これで正常に動作するならばスクリプト的には概ね問題はなく、問題があれば設定やAddressable側の問題になります。

f:id:tsubaki_t1:20190312231850j:plain
Virtual Modeで動作を確認

f:id:tsubaki_t1:20190312232338g:plain
Virtual Modeで正常に動作するなら、スクリプト的には概ね問題はない

手順2:ホスティングサービスの起動

ホスティングサービスを起動します。

  1. Hostingボタンを押す
  2. Add Serviceボタンを押した後、 Addボタンを押してサービスを登録
  3. Enable Serviceボタンを押してサービスを起動

これでファイルサービスが起動します。

f:id:tsubaki_t1:20190312232805j:plain

f:id:tsubaki_t1:20190312233010j:plain

f:id:tsubaki_t1:20190312233019j:plain

手順3:AddressableのRemote接続先を設定

Addressableの接続先を変更します。つまりホスティングサービスに接続するようにします。

  1. Inspector Prifile Settingsを開く
  2. Profile To Editの内容を変更する
    1. + ボタンを押し、プロファイル名を変更
    2. RemoteLoadPathを変更。例えば http://[PrivateIpAddress_3]:[HostingServicePort]のような感じに
  3. GeneralRebuild Remote Catalogにチェックを入れ、Remote Catalog Build PathRemote Catalog Load PathRemote Build PathRemote Load Pathへ変更
  4. GeneralPlayer Version Overrideに適当な値を設定。例えば 00001 等。 この設定をするとCatalog名が固定されます

これで接続先の設定が出来ました。

f:id:tsubaki_t1:20190312233842j:plain

f:id:tsubaki_t1:20190312233709g:plain
RemoteLoadPath接続先を設定

f:id:tsubaki_t1:20190312234638j:plain

初期設定では[BuildTarget]が入ってるしServerDataフォルダにはプラットフォームのフォルダがあるので必要かと思いますが、実際には不要です。

手順4:Addressableの接続先変更~ビルドまで

Addressableの接続先を変更して、ソコ向けにビルドします。

  1. Profile : Defaultを、先程作成したプロファイルに変更する(自分の場合はMy Profile
  2. Play Mode ScriptPacked Play Modeに変更
  3. Build>Build Player Contentを実行

プロファイルは設定を書き換える度にビルドする必要があります。

f:id:tsubaki_t1:20190312235201g:plain
プロファイルの切り替えからビルドまで

手順5:実際に動かしてみる

実際に動かしてみます。これでちゃんと動作するなら問題無しです。

もし404が出たら、手順2に戻ってホスティングサービスをEnable Serviceの隣にある Removeボタンで一旦削除し、やり直すと治ることがあります。

f:id:tsubaki_t1:20190312235619j:plain

ビルドしてみる

あとは実際にビルドしてみます。ホスティングで指定したURLによっては普通にアクセス出来ます。

またCatalog名を固定して生成しているので、Unityエディター側でアクセスするAssetBndleの中身を差し替えるとリソースを差し替えられます。

https://user-images.githubusercontent.com/1644563/54213423-b8530380-4527-11e9-9d0c-f3466a4288eb.gif

感想

本格的になると外部サービスになるかもですが、動作確認ならコレでもまぁ十分かなと。

関連

マニュアル docs.unity3d.com

外部のサーバーに配置する場合 section31.jp

Addressableの概要 qiita.com

本当は今回でAddressable 1.0になハズだったんですけどね

https://forum.unity.com/threads/release-announcements-and-notes.543537/

【Unity】ECSで複数のComponentGroupを使う場合

複数のComponentGroupを使う

ComponentGroupは、Systemが処理するEntity(及びComponent)を取得する為に使用します。データ処理の起点とも言って良いです。

このComponentGroupですが、一つのSystem内に複数持つことが出来ます。例えばキャラクターとアイテムといった 異なる特性のEntity同士をかけ合わせて計算したい場合 など、色々な要因で複数のComponentGroupを使いたくなります。

全く異なるComponentGroupを複数定義する場合はRequireForUpdateを使う

Systemは有り難い事に、ComponentGroupが要求するEntityが一つも無い場合は、動作しないようになっています。完全にシステムを停止する訳ではないのでコストが完全に消滅する訳ではないですが、ほぼ負荷は無くなります。 これは外部からオブジェクトの状態を観測してSystemをON/OFFするよりは理に適っているかなと個人的には思います。

f:id:tsubaki_t1:20190310214225j:plain

問題は、ComponentGroupを複数要求していた場合 どれか一つでも条件を達成してしまっていれば動いてしまう という点です。

例えば「プレイヤーの位置(Group1)」を「敵(Group2)」が追跡するようなプログラムの場合、「プレイヤー(Group1)」が消滅してもプレイヤーを追跡するプログラムは停止しません。

f:id:tsubaki_t1:20190310214122j:plain

こういった場合には、RequireForUpdate( ComponentGroup )RequireSingletonForUpdate<T>()を使用します。このAPIOnCreateManager()等で呼んでおけば、該当のComponentGroupが全て揃わなければSystemは動作しません

  • 単純に複数のComponentGroupを定義した場合:OR
  • RequireForUpdateを使用した場合:AND

と思っておけば良さそうです。まぁANDとORを組み合わせた複雑な条件分岐は出来ないんですが。

f:id:tsubaki_t1:20190310215932j:plain

gist.github.com

設定違いという点で複数のComponentGroupを使用したい場合は、ChunkIterationを考える

用途は同じだがパラメーターが違う的な意味で複数のComponentGroupが必要という場合もあります。

例えばColliderの形状で、BoxもあればSphereもあります。この2つの役割は本質的には同じですが、保有するパラメーターは全く別物になるため、異なるComponentDataが適応されます。

こういったケースでは以前は複数のComponentGroupを定義していましたが、 現状だとChunkIterationを使用することが推奨される と思います。

tsubakit1.hateblo.jp

もちろん、ComponentData内に複数のケースでのCollider設定を突っ込むことは可能でしょうが、メモリレイアウト的に無駄が多いのと、同じ処理を繰り返せないという点からECS向きではありません。

感想

IJobProcessComponentDataなど、ジョブの発行それ自体に地味なコストがある場合は、RequireForUpdateで不要なればSystemが動作しないように停止しておきたい所です。まぁOnUpdateの最初でComponentGroupの長さを測って即returnすれば同じようなものでしょうが。

ところでChunkIterationがある今、複数のComponentGroupでORになるって、使うケースあるんですかね