【Unity】AddressableAssetsをアブノーマルに使ってみる【AddressableAssets】

はじめに

AddressableAssets は AssetBundle に変わる ファイルパッケージングシステムで、徐々に現場でも使われるようになってきています。

アセットバンドルより ファイルの 所在を意識せず アセットを利用できるのが特徴です。

詳しい説明は他のサイトに任せて、気軽に使える方法を提案します。

ぼくのかんがえたさいきょうのワークフロー

エディット時とランタイム時で同じ様にファイルを扱える流れを考えます。

まずエディター上では 通常の UnityEditor.AssetDatabase.LoadMainAssetAtPath などで直接アセットをロードするように作業していきます。

ビルド時に AddressableAssets を作成し、そちらからロードするような仕組みに切り替える流れです。

ソースコード

実際に利用している部分はこちら
エディット中もビルド後のランタイムも同じ様に動きます。

using System;
using UnityEngine;
using UnityEngine.Playables;

public class AddressableAssetsTest : MonoBehaviour
{
	//----------------------------------------------------------------------------
	private System.Collections.IEnumerator Start()
	{
		StartCoroutine( neko.Loader.CoLoad<GameObject>(
			"Prefabs/SD_unitychan_humanoid.prefab",
			( prefab ) =>
			{
				var unityChan = GameObject.Instantiate( prefab, transform);
			}
		));
		yield break;
	}
} // class AddressableAssetsTest

このような感じで、Loader.CoLoad で アセットを

"Prefabs/SD_unitychan_humanoid.prefab"

からロードしています。
ロードが完了したら、プレハブをインスタンス化して表示してます

エディター上の実行結果

ディレクトリ構造は

この様に AddressableAssets というフォルダを作り それ以降は上で指定した path になってます。

プレハブに使われている実態は #NonAddressableAssets というフォルダに無造作に配置しています。

ローダー部

実際ロードしている部分はこちらになります。

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

namespace neko {

public static class Loader
{
	static public string EntryPoint{ get; } = "Assets/AddressableAssets";

	//----------------------------------------------------------------------------
	public static System.Collections.IEnumerator CoLoad<T>(
		string addressableName,
		System.Action<T> onDone
	) where T : class
	{
#if UNITY_EDITOR
		var path = $"{EntryPoint}/{addressableName}";
		var asset = UnityEditor.AssetDatabase.LoadMainAssetAtPath( path);
		onDone?.Invoke( asset as T);
#else
		var path = addressableName;
		var asyncHandle = UnityEngine.AddressableAssets.Addressables.LoadAssetAsync<T>( path);
		asyncHandle.Completed += ( _handle) =>
		{
			onDone?.Invoke( _handle.Result);
		};
		while( !asyncHandle.IsDone) yield return null; // 完了待ち
#endif
		yield break;
	}

} // class Loader


} // namespace neko

EntryPoint プロパティ は アプリで使用するディレクトリ構造の root を入れてます。

EDITOR 時は UnityEditor.AssetDatabase.LoadMainAssetAtPath を利用して直接読んで、ランタイム時は UnityEngine.AddressableAssets.Addressables.LoadAssetAsync を利用してファイルを呼んでいます。

*例外処理などエラー対策は省いています。

ビルド時

Windows なり Mac なりでビルドした場合は

このような Addressables Groups が生成されます。
グループ名はパスから生成した適当な名前。
アドレス部に EntryPoint からのローカルパスと同じ文字列。
path は 実際にファイルがあるpath

using System.Linq;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;

public class AddressableAssetsBuilder
{
	//----------------------------------------------------------------------------
	[InitializeOnLoadMethod]
	private static void Initialize()
	{
		BuildPlayerWindow.RegisterBuildPlayerHandler( OnBuildPlayer);
	}
	//----------------------------------------------------------------------------
	// ビルドする前に実行される
	private static void OnBuildPlayer( BuildPlayerOptions options)
	{

		// 再帰的にアセット収集
		System.Action<string,List<string>> recursive = null;
		recursive = ( path, list) =>
		{
			var entries = System.IO.Directory.EnumerateFileSystemEntries( path).Where( x => !x.EndsWith( ".meta"));
			foreach( var it in entries)
			{
				if( System.IO.File.Exists( it))
				{
					// ファイルなら
					var guid = AssetDatabase.AssetPathToGUID( it);
					list.Add( guid);
				}
				else
				if( System.IO.Directory.Exists( it))
				{
					// ディレクトリなら自分自身を呼ぶ
					recursive( it, list);
				}
			}
		};

		string root_path = neko.Loader.EntryPoint;

		// アセット収集する
		var assetList = new List<string>();
		recursive( root_path, assetList);

		// グループ名命名
		System.Func<string,string> getGroupName = ( _path) =>
		{
			var groupName = _path.Replace( $"{root_path}/", "");
			var index = groupName.LastIndexOf( '/');
			if( index == -1)
			{
				return "Default Local Group";
			}
			else
			if( index != -1)
			{
				groupName = groupName.Remove( index);
			}
			groupName = groupName.Replace('\\','-').Replace('/','-');
			return groupName;
			
		};

		// グループ取得
		System.Func<AddressableAssetSettings,string,AddressableAssetGroup> getGroup = ( _settings, _name) =>
		{
			AddressableAssetGroup group = null;
			group = _settings.FindGroup( _name);
			if( group == null)
			{
				var schema = ScriptableObject.CreateInstance<BundledAssetGroupSchema>();
				schema.IncludeInBuild = true;
				schema.BundleNaming = BundledAssetGroupSchema.BundleNamingStyle.FileNameHash;
				group = _settings.CreateGroup(
					_name,
					false,
					false,
					true,
					new List<AddressableAssetGroupSchema>{ schema, ScriptableObject.CreateInstance<ContentUpdateGroupSchema>() }
				);
			}
			return group;
		};

		var settings = AddressableAssetSettingsDefaultObject.Settings;

		// アセットをグループに追加していく
		foreach( var guid in assetList)
		{
			if( settings.FindAssetEntry( guid) != null) settings.RemoveAssetEntry( guid);
			var path = AssetDatabase.GUIDToAssetPath( guid);
			// グルーク作成
			var groupName = getGroupName( path);
			var group = getGroup( settings, groupName);
			AddressableAssetEntry entry = settings.CreateOrMoveEntry( guid, group);

			// アドレス解決
			var address = path.Replace( $"{root_path}/", "");
			entry.address = address;
		}
		AddressableAssetSettings.CleanPlayerContent( AddressableAssetSettingsDefaultObject.Settings.ActivePlayerDataBuilder);
		AddressableAssetSettings.BuildPlayerContent();
		// ビルド
		BuildPlayerWindow.DefaultBuildMethods.BuildPlayer( options);
	}
} // class AddressableAssetsBuilder

こちらをどこか Editor フォルダの下に入れて ビルド時にフックして AddressableAssetSettings を生成した後にビルドしています。

順番に見ていくと

この部分でビルドに独自メソッドをフックしています。

//----------------------------------------------------------------------------
[InitializeOnLoadMethod]
private static void Initialize()
{
	BuildPlayerWindow.RegisterBuildPlayerHandler( OnBuildPlayer);
}

[InitializeOnLoadMethod] は Editor 起動時に走るメソッドが定義できます。

BuildPlayerWindow.RegisterBuildPlayerHandler はビルドボタンを押した時に別のメソッドを実行させます。今回は AddressableAssetSettings の設定。リファレンス

UnityEditor.Build.IPreprocessBuildWithReport で OnPreprocessBuild() を呼ばれるタイミングでも試したのですが、これでは AddressableAssetSettings が作られるのがおそすぎてうまく出来ませんでした。

	// アセット収集する
	var assetList = new List<string>();
	recursive( root_path, assetList);

ここで neko.Loader.EntryPoint 下(“Assets/AddressableAssets”) のアセットをすべて収集しています。

アセットのパスからグループ名を決定するデリゲート。 “/” や “\” は使えないので “-” に置換してます。

	// グループ名命名
	System.Func<string,string> getGroupName = ( _path) =>
	{
		var groupName = _path.Replace( $"{root_path}/", "");
		var index = groupName.LastIndexOf( '/');
		if( index == -1)
		{
			return "Default Local Group";
		}
		else
		if( index != -1)
		{
			groupName = groupName.Remove( index);
		}
		groupName = groupName.Replace('\\','-').Replace('/','-');
		return groupName;
	};

グループを検索しなければ作るデリゲート。

	// グループ取得
	System.Func<AddressableAssetSettings,string,AddressableAssetGroup> getGroup = ( _settings, _name) =>
	{
		AddressableAssetGroup group = null;
		group = _settings.FindGroup( _name);
		if( group == null)
		{
			var schema = ScriptableObject.CreateInstance<BundledAssetGroupSchema>();
			schema.IncludeInBuild = true;
			schema.BundleNaming = BundledAssetGroupSchema.BundleNamingStyle.FileNameHash;
			group = _settings.CreateGroup(
				_name,
				false,
				false,
				true,
				new List<AddressableAssetGroupSchema>{ schema, ScriptableObject.CreateInstance<ContentUpdateGroupSchema>() }
			);
		}
		return group;
	};

ここで一気にグループを作ってアセットを登録・pathをアドレスとして指定することで、UnityEngine.AddressableAssets.Addressables.LoadAssetAsync でパスと同じ文字列のアドレスからアセットをロードしています。

	var settings = AddressableAssetSettingsDefaultObject.Settings;

	// アセットをグループに追加していく
	foreach( var guid in assetList)
	{
		if( settings.FindAssetEntry( guid) != null) settings.RemoveAssetEntry( guid);
		var path = AssetDatabase.GUIDToAssetPath( guid);
		// グルーク作成
		var groupName = getGroupName( path);
		var group = getGroup( settings, groupName);
		AddressableAssetEntry entry = settings.CreateOrMoveEntry( guid, group);

		// アドレス解決
		var address = path.Replace( $"{root_path}/", "");
		entry.address = address;
	}
	AddressableAssetSettings.CleanPlayerContent( AddressableAssetSettingsDefaultObject.Settings.ActivePlayerDataBuilder);
	AddressableAssetSettings.BuildPlayerContent();

最後は通常のビルドを実行します。

	// ビルド
	BuildPlayerWindow.DefaultBuildMethods.BuildPlayer( options);

これでランタイム時も使用者はアセットの所在がよくわからないけど同じ様に実行されました。

ビルドの実行結果

アドレッサブルアセットを意識せずにファイルを読み込む仕組みの完成です。

注意

プロジェクトが大規模になって来た場合、ビルド時間が長くなる恐れがあるので、利用時は要検討です。

エディット時とランタイム時のロード時間は結構違うので頭に入れておいたほうが良いです。Unityの強烈なキャッシュやASync系を使ってないのでほぼ一瞬です。一方AddressableAssetsは初回のロードはアセットバンドルの展開から始まるので、結構時間がかかります。その後はキャッシュが効いて高速です。ゲーム開始のロード画面などで一気にプレハブを読み込むのが良いですね。

EntryPoint に指定した path の下は問答無用でパッキングするので、テストのゴミや未使用のアセットは置かない心配りが必要になってきます。

最後に

AssetBundle より作業者はそれほどファイルのパッキングを意識しないで作業をすすめることができるのではないでしょうか?

この作品はユニティちゃんライセンス条項の元に提供されています

関連記事

2 Comments

Add a Comment

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