アセットバンドルの利用


アセットバンドルの扱い

アセットバンドルを利用してみたのでメモ。
Unity5では4にくらべてだいぶ楽になったと聞きましたが、それでもなかなか大変ですね。
最終的にはAssetBundle Manager & Example ScenesというUnityから公式で出されたものを利用するのが良さそうです。
このライブラリは2015/09/11に出たのでまだ新しいですね。

まずは自力でやる場合。(Unity5以降)

>サンプルダウンロード

画像を1枚用意し、それをSpriteにしてさらにPrefab化。
それをステージに配置したSceneAと、位置をちょっとだけ変えたSceneBを用意。

アセットバンドルを書き出す機能はUnityにあるのですが、それを呼び出すインターフェースがないのでEditor拡張でそのメニューを用意。

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.IO;

/// <summary>
/// アセットバンドルを作成する。
/// </summary>
public class BuildAssetBundle : MonoBehaviour {
	[MenuItem("Assets/Build AssetBundles")]
	static void CreateAssetBuneleFiles() {
		string dir = "AssetBundles";

		if (Directory.Exists(dir) == false)
			Directory.CreateDirectory(dir);

		BuildPipeline.BuildAssetBundles(dir, 0, EditorUserBuildSettings.activeBuildTarget);
	}
}

(Assets以下どの階層でもいいので「Editor」フォルダに格納。)

続いて、素材をどのアセットバンドルに含めるかの設定。
ファイルもしくはフォルダに設定可能。
赤い文字がアセットバンドル名で、下にあるAsset Labelsにて設定。
assetbundle_project

シーンファイルとその他の素材は一緒にできないのでシーンはシーンでまとめたりそれぞれ別々に分ける必要がある。
(ライトマップやNavMeshなんかを設定するとシーンファイル直下に素材ができるので、その場合はフォルダにラベルをつけてしまうとシーンとその他のファイルが一緒のラベルになってしまうのでシーンファイルそれぞれににつけるなどする。)

今回はmaterialsとscenesの2つに分ける。
先ほどのエディタ拡張からメニューAssets > Build AssetBundlesをするとプロジェクトフォルダの直下にAssetBundlesフォルダができているのでそのファイルをアセットバンドルを置きたいサーバーへアップロード。

そして下記のコードをGameObjectに貼り付けて実行するとシーンが読み込まれる。

using UnityEngine;
using System.Collections;

/// <summary>
/// アセットバンドルをロードして、そこから指定のシーンをロード。
/// </summary>
public class LoadSceneFromAssetBundles : MonoBehaviour {
	/// <summary>
	/// アセットバンドルのURL。
	/// </summary>
	const string SceneURL = "/blog/wp-content/entries/151022_AssetBundles/scenes";
	const string MaterialURL = "/blog/wp-content/entries/151022_AssetBundles/materials";

	/// <summary>
	/// 読み込むシーン名。
	/// </summary>
	public string sceneName;

	/// <summary>
	/// バージョン番号。
	/// </summary>
	int version = 0;

	/// <summary>
	/// Start.
	/// </summary>
	IEnumerator Start() {
		yield return StartCoroutine(this.LoadAndCache());
	}

	IEnumerator LoadAndCache() {

		//キャッシュの準備が出来るまで待機
		while (!Caching.ready)
			yield return null;

		// シーンファイルをキャッシュもしくはウェブからダウンロード
		using (WWW www = WWW.LoadFromCacheOrDownload(LoadSceneFromAssetBundles.SceneURL, version)) {
			yield return www;

			if (www.error != null)
				throw new UnityException("Error : " + www.error);


			AssetBundle bundle = www.assetBundle;

			yield return Application.LoadLevelAsync(this.sceneName);

			bundle.Unload (false);
		}
	}
}

これを実行すると作った、SceneAが読み込まれる。

assetbundle_load_only_scene

ただし、シーンのみ読み込まれるだけで作成したmaterialsが読み込まれていないのでSpriteがMissingになってしまいなにも見えない。
どうやらscenesのアセットバンドルにはmaterialsのアセットバンドルも必要だという依存性は解決してくれないようなので自分で読み込まなくてはならない。
そこで、先にmaterialsを読み込むようにしてみると…

	IEnumerator LoadAndCache() {

		//キャッシュの準備が出来るまで待機
		while (!Caching.ready)
			yield return null;
		
		// マテリアルをキャッシュもしくはウェブからダウンロード
		AssetBundle materialBundle = null;

		using (WWW www = WWW.LoadFromCacheOrDownload(LoadSceneFromAssetBundles.MaterialURL, version)) {
			yield return www;

			if (www.error != null)
				throw new UnityException("Error : " + www.error);


			materialBundle = www.assetBundle;
		}

		// シーンファイルをキャッシュもしくはウェブからダウンロード
		using (WWW www = WWW.LoadFromCacheOrDownload(LoadSceneFromAssetBundles.SceneURL, version)) {
			yield return www;

			if (www.error != null)
				throw new UnityException("Error : " + www.error);


			AssetBundle sceneBundle = www.assetBundle;

			yield return Application.LoadLevelAsync(this.sceneName);

			sceneBundle.Unload (false);
		}
			
		materialBundle.Unload (false);
	}

こうすると先にmaterialが読み込まれ、シーンをロードした際に読み込んでおいたmaterialの素材が読み込まれ表示されるようになった!
assetbundle_load_scene

このように、依存性の解決をするにはAssetBundleを書き出した際に一緒に吐き出されるmanifestファイルに書かれている情報を解読する必要がある。
今回の場合だとscenes.manifestに

ManifestFileVersion: 0
CRC: 1768497311
Hashes:
AssetFileHash:
serializedVersion: 2
Hash: da4a695c13af3881568b5d61b7acc82f
TypeTreeHash:
serializedVersion: 2
Hash: 27c15590f97959f54de340538dd49236
HashAppended: 0
ClassTypes:
– Class: 1
Script: {instanceID: 0}
…省略…
– Class: 258
Script: {instanceID: 0}
Assets:
– Assets/Scenes/SceneA.unity
– Assets/Scenes/SceneB.unity
Dependencies:
– AssetBundles/materials

このような記述があるので、Dependenciesにかかれているmaterialsが必要ということがわかる。
またバージョン管理も新しいものをUPしても新しいものが落としてくれるわけではなくWWW.LoadFromCacheOrDownload()のversionに新しいバージョンを入れる必要があり、その辺の仕組みも必要になる。
さらに環境ごとにアセットバンドルの用意も必要だったり…

・依存性の管理
・バージョン管理
・環境ごとのアセットバンドルの作成

などなかなか大変そうです…

そこで、Unityが提供しているAssetBundleManagerを使って少し楽にしてみることに。
>サンプルダウンロード

まずはアセットストアよりダウンロード。
自分で実装するので、AssetBundleSampleを削除。
先ほどと同じ素材を用意して、まずは
Assets > AssetBundles > Build AssetBundles
にてアセットバンドルの書き出し。
環境ごとにアセットバンドルを用意する部分も自動でやってくれてOSX, iOS, Androidなどのフォルダに分けてくれる。(書き出されるのは現在の書き出しのターゲットのみ)
そして、アップロード。
(ほとんど先ほど消したAssetBundleSample > LoadScenes.csと一緒ですが)GameObjectに下記のコンポーネントを貼り付け

using UnityEngine;
using System.Collections;
using AssetBundles;

/// <summary>
/// AssetBundleManagerを使って
/// 
/// </summary>
public class LoadSceneFromAssetBundles : MonoBehaviour {
	/// <summary>
	/// アセットバンドルのURL。
	/// </summary>
	const string AssetBundleBaseURL = "/blog/wp-content/entries/151022_AssetBundles/AssetBundlesManager/";

	/// <summary>
	/// シーンの入ったアセットバンドルの名前。
	/// </summary>
	const string SceneAssetBundleName = "scenes";

	/// <summary>
	/// 読み込むシーン名。
	/// </summary>
	public string sceneName;

	/// <summary>
	/// Start.
	/// </summary>
	IEnumerator Start() {
		yield return StartCoroutine(this.Initialize());
		yield return StartCoroutine(this.InitializeLevelAsync(this.sceneName, false));
	}

	/// <summary>
	/// 初期化。
	/// </summary>
	IEnumerator Initialize() {
		DontDestroyOnLoad(gameObject);

		#if DEVELOPMENT_BUILD || UNITY_EDITOR
		AssetBundleManager.SetDevelopmentAssetBundleServer ();
		#else
		AssetBundleManager.SetSourceAssetBundleURL(LoadSceneFromAssetBundles.AssetBundleBaseURL);
		#endif

		var request = AssetBundleManager.Initialize();

		if (request != null)
			yield return StartCoroutine(request);
	}

	/// <summary>
	/// シーンを読み込む。
	/// </summary>
	protected IEnumerator InitializeLevelAsync(string sceneName, bool isAdditive) {
		AssetBundleLoadOperation request = AssetBundleManager.LoadLevelAsync(LoadSceneFromAssetBundles.SceneAssetBundleName, sceneName, isAdditive);

		if (request == null)
			yield break;

		yield return StartCoroutine(request);
	}
}

そして、AssetBundleManager > Resources > AssetBundleServerURL
こちらのファイルにURLを入力。(UnityEditor上ではこちらを読みに行くようにしている)
これで再生すると完成!
このアセットを使うと使いたいアセットバンドルを指定すれば自動でmanifestファイルから依存関係をしらべて先にDLしてくれる。
また
Assets > AssetBundles > Local AssetBundle Server
これをチェックすると先ほどのAssetBundleServerURLの中身がローカルに切り替わり、内部的にサーバーを立ててローカル環境でテストがきる。
Assets > AssetBundles > Simulation Modeをチェックするとわざわざアセットバンドルを書き出さずにアセットバンドルを使ったコードと同じ状態で素材を扱えるらしい。(すごい!)
またこのアセットではバージョンの管理は、バージョンとして扱うのではなく書き出したアセットバンドルのハッシュ値を使うので素材を変えれば自動的に新しいものを落としてくれる。

なのでこのアセットを使うと先ほどの手間が

・依存性の管理
=> 自動で解消してくれる
・バージョン管理
=> ハッシュ値を使うので、素材を入れ替えれば自動的にDLされる
・環境ごとのアセットバンドルの作成
=> フォルダごとに分けて出してくれる。

こんな感じで解消される。
さらに、ローカルやシミュレーションモードまでできる!
なので、これを使わない手はなさそう。

キャッシュファイルの削除

別の問題としてアセットバンドルのバージョンとして、WWW.LoadFromCacheOrDownload()の引数のversionにてバージョン情報が指定できますが、このバージョンが変わるとキャッシュにあるファイルは無視して、改めてファイルを落としてくれる。
URLとversionとセットでキャッシュに持っているので、versionが新しいとか古いとか関係なくキャッシュがなければ、ダウンロード後キャッシュし、古いものはとくに削除されるわけではない…

じゃ、いつ消えるのかというと

・キャッシュへの最終アクセスからすぎた時間(Caching.expirationDelay 最大150日)
・指定の容量を超えたら(Caching.maximumAvailableDiskSpace 4GB(PCは50MB))

ということらしい…
自分で消すには

・Caching.CleanCache()を呼ぶ
・WWW.LoadFromCacheOrDownload()のcrcに偽のファイルをセットする

ということをすると消えるらしい…
なので、削除するには

・アセットバンドルの最大容量の近くにmaximumAvailableDiskSpaceを設定する
・新しいバージョンが出た際に、Caching.CleanCache()もしくは偽のcrcをいれて消す
・expirationDelayが過ぎるまで放っておく

のいずれかになるようです….
(maximumAvailableDiskSpaceを設定するのが一番楽そうですかね…?)

参考)
いまさら書けないAssetBundleのcache周りの知見
テラシュールブログ | AssetBundleの管理について
テラシュールブログ | 【Unity】AssetBundleManagerで「AssetBundleからSceneをロード」する

“アセットバンドルの利用” への3件のフィードバック

  1. […] AssetBundleを扱うときに、AssetBundleManagerを使うといままで以上に便利に扱えるんだけど、ローディング周りについてはとくに実装には含まれていないので自分なりにやらないといけないっぽい。 これでいいのかわからないけど、自分はこんな感じで実装しました。 まずはAssetBundleManager.csの […]

  2. […] ということで、上記のサイトを参考に再度設定するようにしたら動いた。 AssetBundleManagerのSimulate Modeだと再現できないけど、実際にAssetBundleを読み込んでみると再現できる。 […]

  3. […] 以前ここで書いたキャッシュファイルの削除について実際に、アセットバンドルを更新することがあったので具体的な補足。 […]

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です