Edited at

今更誰も教えてくれない、Unityにおけるアセット読み込みについての基礎知識


はじめに

仕事上AssetBundleについての情報収集をすることがあるのですが、ネット上では以下のような意見が散見されます。


  • Resourcesは使ったらダメらしい(何故かはよくわからない)

  • なんかAssetBundleとかいうのがあって、使いづらいらしいけど、なんか対応しないとまずいらしい

  • AssetBundleを使うと何かが解決するらしい

  • AssetBundleはよく分からない、怖い、闇、ゴミ、クソ、etc...

  • 新しく出るAddressable Assets Systemが全てを解決してくれるらしい

このような意見が出るのは、Resources / AssetBundleについて、ふんわりとした知識から微妙に間違った認識を持っているのが原因だと思いました。

ResourcesやAssetBundleについて、断片的な知見をまとめた記事は多いのですが、モノによっては情報が古かったり、それによって他の記事と矛盾が生じていたりして、余計に理解の混乱を招いているような気がします。

そこで、現時点(2018.2)での正しい知識を整理し、最新のベストプラクティスを導きだそうというのがこの記事の主旨です。

正しく理解すれば、AssetBundleは怖いモノではありません。(めんどくさいモノではありますが…)

この記事を読めば、きっと「Resources & AssetBundle完全に理解した」と言える…かも?


想定する読者層


  • AssetBundleやらないといけない気がする人

  • AssetBundleなんもわからん人

  • 雰囲気でAssetBundleをやっている人


この記事で分かること

現時点(Unity 2018.2)における、以下の最新の知見が得られます。


  • Unityにおけるアセット読み込みの手法についての基礎知識

  • Resources, AssetBundleの仕組み、特徴(長所、短所)

  • Resourcesではダメな理由、AssetBundleが必要な理由、AssetBundleで解決できる事

  • ResourcesとAssetBundleを使い分ける判断基準

既に上記を理解している方は、次の記事がオススメです。

AssetBundleを完全に理解する


「※ 要検証」について

記事中に「※ 要検証」と注意書きをしている部分は、「恐らく理屈上そうなると考えられるが、検証による裏付けがまだ取れていない」事項になります。

自分も時間がある時に検証したりしていますが、興味のある方は是非検証記事を書いて、コメントで情報を提供してもらえるとありがたいです。


アセット読み込みの手法について

そもそも、Unityにおいてアセットを読み込むための方法は大きく分けて以下の4種類ある。

それぞれについて詳しく触れていく。


Resources.Load


使い方


  1. Assetsフォルダ以下に"Resources"という名前のフォルダを作る

  2. 上記Resourcesフォルダ内にアセットを入れる


  3. Resources.Load(assetName)でロードできる

引数に渡す文字列はResourcesフォルダからの相対パスから拡張子を除いたもの


アセットのパスが<ProjectPath>/Assets/Resources/Hoge/texture.pngの場合、以下のようにロードする事ができる。

// 大文字・小文字は区別されないためどちらでも良い

var texture = Resources.Load<Texture2D>("hoge/texture");


圧縮の設定

デフォルト設定だと無圧縮でアプリに格納されてしまうため、圧縮の設定をしておく方が良い。

圧縮することで以下の効果がある。


  • アプリ本体のサイズが小さくなる


    • apk、ipaの容量は変わらない可能性があるが、インストール後のサイズが小さくなる



  • (サイズが小さくなる事によって)アプリの起動時間が早くなる

参考:【Unity】ゲームに最初から含めるアセット群を圧縮するオプション - テラシュールブログ


Resourcesの仕組み

Resourcesフォルダの中身はアプリビルド時に変換処理されてアプリ内に格納される。

変換処理というのはざっくりと以下のようなイメージ。


  • アセットをプラットフォームに適したデータに変換

  • ロードに必要なメタ情報の付与

  • 圧縮

  • 単一ファイルにまとめる

要は実機で読む際に問題無くなるようにいい感じにしてくれるのをアプリのビルド時にやってくれている。


長所

使い方を見て分かる通り、極めてシンプルで使いやすい

そのため、迅速に開発サイクルを回したいプロトタイプの製作時などには向いている。


短所

仕組みを読むと想像が付く通り、以下の欠点がある。


  • サイズが大きくなるほどアプリのビルド時間が伸びる

  • サイズが大きくなるほどアプリのサイズが大きくなる

  • サイズが大きくなるほどアプリの起動時間が伸びる

  • アプリ外部からの差し替えが不可能

:pencil: 実際にサイズやファイル数に比例してどれくらい影響があるのかは一度測定してみたい所

参考:The Resources folder - Unity


注意点

Resourcesフォルダ以下のアセットは全てアプリ内に格納されるため、不要なアセットが含まれないように注意する必要がある。

「Resourcesと名が付くフォルダ全て」が対象になるため、例えば<ProjectPath>/Assets/Hoge/Resources/<ProjectPath>/Assets/Fuga/Resources/が存在した場合、両方のフォルダ内のアセットが対象になる。

特にアセットストアなどから導入したアセットは注意。

Resourcesフォルダが複数存在する場合でも問題無く動くが、特に分ける利点も無く、2つのフォルダに同名のアセットが存在する場合にどちらが読み込まれるかがハッキリしないため、一つだけにしておく方が良い。


利用に適したシチュエーション


  • プロトタイプ開発時の利用

  • 外部から差し替えの必要が無く、アセットの容量もさほど大きくないようなアプリの開発


StreamingAssets


使い方



  1. Assets/StreamingAssetsというフォルダを作成する

  2. 上記フォルダ以下にアセットを配置する

  3. System.IO(Androidの場合はUnityWebRequest)でロードする


Assets/StreamingAssets/hoge/master.jsonというアセットを配置した場合、以下のようにロードできる。

using System.IO;

using UnityEngine.Networking;

var path = Application.streamingAssetsPath + "hoge/master.json";

#if UNITY_ANDROID
// Androidの場合はSystem.IOで直接ファイルとしてロードすることができないため、UnityWebRequestを用いる
var request = UnityWebRequest.Get(path);
// 注:標準ではawaitできないので何らか対応する必要がある
await request.SendWebRequest();
var jsonText = request.downloadHandler.text;
#else
var jsonText = File.ReadAllText(path);
#endif

Androidの場合、apk自体が圧縮されているため、System.IOを用いてファイルとしてロードすることができない。そこで、UnityWebRequestを使うことでロードすることができる。(あまり直感的では無いが…)

また上記では簡略化のためawaitを用いてWebRequestを待ち受けているが、標準ではできないため何らかの方法でawait可能にする必要がある。個人的にはUniTaskの導入がオススメ。

awaitを使わず普通にコルーチンで書いてもいいが、コルーチンは値を返す非同期処理の表現には向いておらず、冗長なコードになってしまう。

ちなみに、AssetBundle.LoadFromFileはAndroidにおけるStreamingAssetsからの読み込みにも対応している。(Unity 2018.2.4にて確認。古いバージョンだとできなかったりするかも?)


StreamingAssetsの仕組み

StreamingAssetsは、簡単に言えば「アセットをそのままアプリ内に格納する」ための仕組み。

StreamingAssetsに配置したアセットは、Resourcesとは異なり、そのままの状態でアプリに格納される。


長所


  • アセットをそのままの状態(バイナリ)でアプリに格納することができる

  • 変換処理を挟まないため、ビルド時間にあまり影響しない? ※ 要検証

  • アプリの起動時間にも影響しない? ※ 要検証


短所


  • アプリに格納されるため、アプリの容量は増える

  • 差し替えが不可能(StreamingAssetsは書き込み不可領域

  • Androidにおいてロード方法が特殊

  • アセットをプラットフォームに適した形式へ変換できない


    • 例えばテクスチャの圧縮をAndroidはETC、iOSはASTCにするといったような事

    • 自前でプラットフォームごとにアセットを用意して切り替えるような実装を行えば可能だが、普通にAssetBundleを使うほうが楽



  • バイナリ(byte[])としてロードすることになるので、何らかの方法でデシリアライズする必要がある


注意点

Resourcesと同じく、StreamingAssets以下のアセットは全てアプリに格納されてしまうため、不要なアセットを入れないように注意する必要がある。


利用に適したシチュエーション


  • プラットフォームごとに変換が不要なアセットの読み込み


    • 例えばゲームのマスターデータを格納した独自のjsonやらMessagePackなどのファイルなど



  • ビルド済みAssetBundleの格納


    • AssetBundleはアセットに変更がない限りビルドし直さなくて済むため、アプリビルド時間の削減に繋がる

    • ただしアプリビルド時の前処理として、buildTargetに指定されたプラットフォームのAssetBundleのみをStreamingAssetsに配置するというような工夫が必要




  • Handheld.PlayFullScreenMovie用の映像ファイルの格納


    • スマホだと動画は生データをStreamingAssetsなどから読み込んで再生する必要がある(この辺は詳しくないので誰か補足お願いします)




AssetDatabase.LoadAssetAtPath


使い方


  1. プロジェクトフォルダ内のどこでも良いのでアセットを配置する


  2. AssetDatabase.LoadAssetAtPath(assetPath)でロードする

引数に渡す文字列はプロジェクトフォルダからの相対パス

また、このAPIを使うにはusing UnityEditor;が必要。


アセットのパスが<ProjectPath>/Assets/Hoge/texture.pngの場合、以下のようにロードする事ができる。

// 大文字・小文字は区別されないためどちらでも良いはず

var texture = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/Hoge/texture.png");


長所

プロジェクトフォルダ以下のあらゆるアセットを読み込むことができる。


短所

UnityEditor上でしか動かない。


利用に適したシチュエーション


  • エディタ拡張での利用

  • エディタ上での確認用シーンなどでの利用

  • AssetBundleシミュレーターの実装(後述)


AssetBundle.LoadAsset


使い方


  1. アセットを格納したAssetBundleをビルドする

  2. ビルドしたAssetBundleをどこか(WebサーバーかStreamingAssets)に配置する

  3. 配置したAssetBundleをロードする


  4. assetBundle.LoadAsset(assetName)でAssetBundleからアセットをロードする

AssetBundle同士の依存関係があったり、WebからDLしてキャッシュする場合にはもう少し複雑になるが、基本はこの流れになる。


Assets/AssetBundleResources/Hoge/texture.pngをAssetBundle化してロードする例を解説する。


AssetBundleをビルドする

AssetBundleは対象プラットフォームごとにビルドする必要がある。また、標準ではビルドするためのメニューは用意されておらず、スクリプトを書く必要がある。

実際にはAssetGraphAssetBundleBrowserなどの便利なツールを用いるのがオススメだが、今回は解説のため自前のスクリプトでビルドする例を挙げる。

以下はAssets/AssetBundleResources/Hoge/texture.pngHoge/textureという名前のAssetBundleにビルドする例。(Androidでの例)

var builds = new List<AssetBundleBuild>();

// AssetBundle名とそれに含めるアセットを指定する
var build = new AssetBundleBuild();
build.assetBundleName = "Hoge/texture";
build.assetNames = new string[1] { "Assets/AssetBundleResources/Hoge/texture.png" };

builds.Add(build);

// 成果物を出力するフォルダを指定する(プロジェクトフォルダからの相対パス)
var targetDir = "AssetBundle/Android";
if (!Directory.Exists(targetDir)) Directory.CreateDirectory(targetDir);

// Android用に出力
var buildTarget = BuildTarget.Android;

// LZ4で圧縮するようにする
var buildOptions = BuildAssetBundleOptions.ChunkBasedCompression;

BuildPipeline.BuildAssetBundles(targetDir, builds.ToArray(), buildOptions, buildTarget);

AssetGraphは、上記のAssetBundleBuildの配列を構築してビルドするという手順を簡単にGUI上で行えるようにしてくれる。

上記スクリプトを実行すると、以下の成果物が生成される。

- <ProjectPath>

- AssetBundle/Android
- Android: AssetBundleManifestを格納したAssetBundle
- Android.manifest: 上記ファイルのメタ情報
- Hoge/texture: テクスチャを格納したAssetBundle
- Hoge/texture.manifest: 上記ファイルのメタ情報

.manifestファイルはAssetBundleのハッシュ値などを記録したyamlテキストで、実際にAssetBundleをロードする際には必要のないファイル。

AssetBundleManifestというのはAssetBundle同士の依存関係などを格納したオブジェクト。ロードには直接的には必要無い。


〜余談〜

上記ではスクリプト上からAssetBundle名とそれに含まれるアセットを明示的に指定しているが、過去にはInspector上からAssetBundle名を指定して、それを元にAssetBundleをビルドするという方法しかなかった。

一応解説を行うが、この方法では一部のAssetBundleのみビルドするといったような細かい制御ができず1、スクリプトからAssetBundle名を指定するのがやり辛かったりする2など、柔軟性に欠けるため、現在では使わないほうが良い。

まずはAssetBundle化したいアセットを選択し、InspectorからAssetBundle名を指定する。

この時、同じAssetBundle名を指定すると、それらは全て一つのAssetBundle内に格納される。



右にあるセレクトボックスはVariantsの設定だが、ここでは省略する。

一通り設定し終わったら、以下のAPIを呼び出すと、指定した通りにビルドされる。

BuildPipeline.BuildAssetBundles (string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform);



ビルドしたAssetBundleからアセットをロードする

上記で生成したAssetBundleを、Unityからロードできるどこか(Webサーバー上 or StreamingAssets)に配置する。

ここではStreamingAssetsに配置する例を挙げる。(Webからロードする際にはもう少し複雑になるが、ここでは解説を省く)

まず、生成されたAssetBundle/Android/Hoge/textureAssets/StreamingAssets/Hoge/textureへコピーする。

すると、アプリからは以下のようにロードできる。

// StreamingAssetsからAssetBundleをロードする

var assetBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/Hoge/texture");
// AssetBundle内のアセットにはビルド時のアセットのパス、またはファイル名、ファイル名+拡張子でアクセスできる
var texture = assetBundle.LoadAsset<Texture2D>("Assets/AssetBundleResources/Hoge/texture.png");

// 不要になったAssetBundleのメタ情報をアンロードする
assetBundle.Unload(false);

AssetBundleにはアセットをロードするためのメタ情報として元のアセットのパスなどが格納されており、これによって内部のアセットへアクセスできる。今回の場合、LoadAssetの引数は以下の3パターンが有効。


  • "Assets/AssetBundleResources/Hoge/texture.png"

  • "texture.png"

  • "texture"

また、AssetBundleのビルド時に以下のオプションを指定することで、ファイル名(+拡張子)でのアクセスを無効化することができる。これにより若干のパフォーマンスの向上が見込める。

:pencil: どのくらい効果があるかは要検証


AssetBundleの仕組み

AssetBundleは、簡単に言えば「Resourcesではアプリのビルド時に行っている処理を事前に行っておく事により、実行時に外部からアセットがロードできるようになる」という仕組み。

ロードに必要なメタ情報などを一緒に格納することで、格納したアセットをTexture2DやGameObjectなど必要に応じた型で読み込むことができるようになっている。


長所


  • (Webサーバー上に配置する場合)アプリ外部からの差し替えが可能

  • (Webサーバー上に配置する場合)アプリの本体容量が小さくなる

  • アプリのビルド時間が早くなる


    • AssetBundleは別途ビルドする必要があるが、アセットに変更がない限りAssetBundleはビルドし直す必要が無いため、結果として時間削減に繋がる



  • StreamingAssetsに配置する場合でもResourcesにするより起動時間的に有利? ※要検証


短所


  • Resourcesに比べて複雑で面倒くさい

  • 配信用のWebサーバーを用意する必要がある

  • ビルド成果物の管理が大変

  • アセットを変更する度にAssetBundleをビルドし直す必要がある


    • ResourcesやAssetDatabase.LoadAssetで代替させる仕組みがあれば簡易的にシミュレーションすることが可能




利用に適したシチュエーション


  • アセットのサイズが膨大になるようなゲーム

  • アプリ外部からアセットの変更・追加が必要なゲーム

利用に適したというよりかは、上記の場合はAssetBundleを使う他ない。

上記の要件を満たすゲームの典型は、いわゆる「ソシャゲ」。ソシャゲはカード(キャラ)画像、Prefabが物凄く多くなるし、カードを追加するのにいちいちアプリを更新しなきゃいけないのは有り得ないため、AssetBundleを使うのは必須。


補足:Web・ディスク上からのバイナリデータのロード

上記の方法以外にも、Web上からバイナリ(またはテキスト)データをDLし(必要であればそれをディスク上にキャッシュして)ロードするという方法も考えられます。

そもそも、わざわざデータをAssetBundleというUnity独自の形式に変換する必要があるのは、依存関係の構築GameObjectなどのUnity独自のコンポーネントをシリアライズ&デシリアライズする必要がある為であり、依存関係が存在せず、別のシリアライズ手段が存在する場合は、AssetBundleにする必要性はあまり無いです。

例えば、JSONUtilityなどでシリアライズしたjsonファイルなどは、依存関係は存在せず、単体でオブジェクトとしてデシリアライズする事が可能なため、ただのテキストデータとして読み込みJSONUtilityでデシリアライズすれば済む話です。(但し、キャッシュは自前でなんとかする必要がある)


よくある質問


AssetBundleにするとアプリ容量が小さくなる?

これは場合によってはYESです。

AssetBundleをWebサーバー上に配置する場合は、アプリ本体容量はもちろん小さくなります。

StreamingAssetsに配置する場合は結局アプリ本体に含まれるため、容量は小さくなりません。

但し、昔は「AssetBundleは圧縮が可能」で「Resourcesは圧縮が不可能」だった為、AssetBundleにして圧縮しStreamingAssetsに入れることで、Resourcesよりもサイズ軽減が見込めるという状況がありました。

Unity5.6からはResourcesでもAssetBundleと同じ圧縮が可能になった為、圧縮を目的にAssetBundle化する意味は無くなりました。


Resourcesは使わないほうが良い?

公式チュートリアルで「Don't use it.」とまで言われていますが、個人的な見解としては、必要に迫られない限りはResourcesで十分だと考えています。

「必要に迫られる」のは大体以下のようなシチュエーションです。


  • アプリ本体の容量が大きすぎる(アセットの容量が大きすぎる)

  • アプリのビルド時間が長すぎる

  • アプリの起動時間が長すぎる

何れにせよ、「Resourcesを使うことで何が問題になっているか」「AssetBundleを使うことで何を解決できるのか」「他の解決方法は無いか」をちゃんと理解・整理した上で、実装コストを考慮して判断すべきです。

例えば、アプリの容量を小さくしたい場合、AssetBundleにしてWebサーバー上に配置する他に、以下のような対処法があります。


Addressable Asset Systemを使えば全て解決する?

上記のことを理解していなくてもAddressable Asset System(AAS)がいい感じにしてくれるようになるんでしょ?と思うかもしれませんが、AASはあくまでAssetBundle周りの面倒な処理をこなしてくれるツールであり、それ以上でも以下でもありません。AASを使えばアプリが劇的に軽くなるとか、そんな魔法的な力は全くありません。

また、(現段階では)内部的にもResources & StreamingAssets & AssetBundleを使っているので、理解していないとハマるケースがあります。というか現在はまだプレビュー版で仕様が不透明な挙動やバグが多く、何が起こっているか理解していないと確実に詰みます

少なくともプレビューが外れるまでは使用はオススメできません。


まとめ


  • エディタ上でだけ動く機能ではAssetDatabase.LoadAssetを使う

  • 外部からアセットの追加・差し替えをしたい場合はAssetBundleを使う

  • それ以外の場合、必要に迫られない限りはResourcesで問題無い

  • アセットをそのまま(バイナリやテキストとして)格納・読み込みしたい場合はStreamingAssetsを使う

  • 動画は逆に生データとしてStreamingAssetsやWeb上から読み込まないと再生できない場合がある

これを読んで、「やっぱりAssetBundleを使う必要がある」と判断した方は…ようこそ、AssetBundle沼へ。

実際にAssetBundleを使うにあたってはもう少し必要な知識があるため、AssetBundle沼に浸かる人用の記事も後々書く予定。

2018.10.22追記 書きました→AssetBundleを完全に理解する


参考資料





  1. 内部的にはビルド先に既に存在している場合はインクリメンタルビルドになるが、いまいち最適化されていないのか遅い 



  2. AssetImporterからしか設定できないため、Reimportする事になる