前提
前提としてプロジェクトには Addressables を既に組み込んでいるものとする。
バージョンは 1.19.19
を使用する。
MacOSで動かしているのでプラットフォーム名は OSX
などになっているが、実行する環境によってここの名前は変化する点に注意。
各ファイルの役割について
Addressables を動作させるにあたって絶対に必要なものとして settings.json
がある。
このファイルはAddressables を初期化する際(Addressables.InitializeAsync
を呼び出した時)に使用される。
Addressables 側で必要とするため、このファイルに関してはアプリビルド時に必ず組み込まれる。
各種プロパティについて説明
- LibraryPath
- "Library/com.unity.addressables/" を指す
- StreamingAssetsSubFolder
-
"aa" を指す。
アプリをビルドした際、必ずStreamingAssets以下に aa というフォルダが作られ、そこにsettings.jsonがコピーされる。 - RuntimePath
-
settings.jsonがどこに存在するのかが定義されている。
Unityエディタ上の場合は LibraryPath + "aa" + "/" + Application.platform
であり、実機の場合は
Application.streamingAssetsPath + "/" + "aa"
を指す。
Addressables の初期化の流れ
Addressables
クラスは内部に AddressablesImpl という型のインスタンスを内包しており、基本的にこのクラスに書かれた処理を実行する。
public AsyncOperationHandle<IResourceLocator> InitializeAsync()
{
var settingsPath =
#if UNITY_EDITOR
PlayerPrefs.GetString(Addressables.kAddressablesRuntimeDataPath, RuntimePath + "/settings.json");
#else
RuntimePath + "/settings.json";
#endif
return InitializeAsync(ResolveInternalId(settingsPath));
}
Unityエディタ上ではエディタ専用の初期化を行うように実装されている。
具体的にはPlayerPrefs にGUID:xxxxxxx
というような形式でGUIDが設定されている。
このGUIDはAddressablesAssetSettingsのGUIDであり、Unityエディタ上ではAddressablesAssetSettings.CreatePlayModeInitializationOperation
というメソッドを呼び出して初期化を行う。
この為、エディタ上では基本的に実機と同じ状況にすることが出来ない
(Addressables 初期化前にPlayerPrefsのAddressables.kAddressablesRuntimeDataPathをDeleteKeyするなどすれば エディタ上でも実機と同じような状態にすることはできる)
今回はエディタ環境から強引な手法を使って実機と同じ流れでAddressablesが動くようにする
Addressables を初期化する際には InitializationOperation
というクラスを生成して、そちらで初期化を行うように実装されている。
// InitializeAsyncメソッド内
if(!m_InitializationOperation.IsValid())
m_InitializationOperation = Initialization.InitializationOperation.CreateInitializationOperation(this, runtimeDataPath, providerSuffix);
if (autoReleaseHandle)
AutoReleaseHandleOnCompletion(m_InitializationOperation);
return m_InitializationOperation;
この生成された InitializationOperation
インスタンスがInitializeAsyncメソッドの戻り値となっている。
次に、CreateInitializationOperation
内部がどのような実装になっているか調べる。
internal static AsyncOperationHandle<IResourceLocator> CreateInitializationOperation(AddressablesImpl aa, string playerSettingsLocation, string providerSuffix)
{
var jp = new JsonAssetProvider();
aa.ResourceManager.ResourceProviders.Add(jp);
var tdp = new TextDataProvider();
aa.ResourceManager.ResourceProviders.Add(tdp);
aa.ResourceManager.ResourceProviders.Add(new ContentCatalogProvider(aa.ResourceManager));
var runtimeDataLocation = new ResourceLocationBase("RuntimeData", playerSettingsLocation, typeof(JsonAssetProvider).FullName, typeof(ResourceManagerRuntimeData));
var initOp = new InitializationOperation(aa);
initOp.m_rtdOp = aa.ResourceManager.ProvideResource<ResourceManagerRuntimeData>(runtimeDataLocation);
initOp.m_ProviderSuffix = providerSuffix;
initOp.m_InitGroupOps = new InitalizationObjectsOperation();
initOp.m_InitGroupOps.Init(initOp.m_rtdOp, aa);
var groupOpHandle = aa.ResourceManager.StartOperation(initOp.m_InitGroupOps, initOp.m_rtdOp);
return aa.ResourceManager.StartOperation<IResourceLocator>(initOp, groupOpHandle);
}
JsonAssetProvider、TextDataProvider、ContentCatalogProvider がここで登録されている。
また、RuntimeData という名前の ResourceLocationBase インスタンスが生成されている。
ここはどのような処理となっているのか調べる。
ResourceLocationBase は IResourceLocation インターフェースを実装している。
各種インターフェースプロパティとフィールドは以下のように対応付けされている。
public class ResourceLocationBase : IResourceLocation
{
public string InternalId => m_Id;
public string ProviderId => m_ProviderId;
public IList<IResourceLocation> Dependencies => m_Dependencies;
public bool HasDependencies => m_Dependencies != null && m_Dependencies.Count > 0;
public object Data => m_Data;
public string PrimaryKey => m_PrimaryKey;
public int DependencyHashCode => m_DependencyHashCode;
public ResourceLocationBase(string name, string id, string providerId, Type t, params IResourceLocation[] dependencies)
{
m_PrimaryKey = name;
m_HashCode = (name.GetHashCode() * 31 + id.GetHashCode()) * 31 + providerId.GetHashCode();
m_Name = name;
m_Id = id;
m_ProviderId = providerId;
m_Dependencies = new List<IResourceLocation>(dependencies);
m_Type = t == null ? typeof(object) : t;
ResourceLocationBaseはIResourceLocationインターフェースを実装したクラスであり、
クラス図にするとこのような関係になっている。
上記の実装から照らし合わせると、runtimeDataLocationのパラメータはこのように設定される
var runtimeDataLocation = new ResourceLocationBase("RuntimeData", playerSettingsLocation, typeof(JsonAssetProvider).FullName, typeof(ResourceManagerRuntimeData));
そして、以下のようにResourceManager からProviderResourceというメソッドを呼び出して上記のパラメータが指定された runtimeDataLocation を渡している。
var initOp = new InitializationOperation(aa);
initOp.m_rtdOp = aa.ResourceManager.ProvideResource<ResourceManagerRuntimeData>(runtimeDataLocation);
ResourceManager.ProviderResource の実装はやや複雑なので図で表す。
流れとしては以下の図のような感じ。
つまり、ResourceProvider は各種アセットのロード処理を提供するインターフェースと考えれば良いと思う。
利用する側はAsyncOperationを内部に持った AsyncOperationHandle のみを受け取り、ロードの進捗管理や結果の取得などはすべてこの AsyncOperationHandle を経由して行う。
今回のケースの場合、JsonAssetProvider
を使って処理を行うのでJsonAssetProviderの実装を確認する。
public class JsonAssetProvider : TextDataProvider
{
/// <summary>
/// Converts raw text into requested object type via JSONUtility.FromJson.
/// </summary>
/// <param name="type">The object type the text is converted to.</param>
/// <param name="text">The text to convert.</param>
/// <returns>Returns the converted object.</returns>
public override object Convert(Type type, string text)
{
return JsonUtility.FromJson(text, type);
}
}
どうやら TextDataProvider
というクラスを継承しているようなのでTextDataProviderを確認する。
public class TextDataProvider : ResourceProviderBase
{
// 実装は省略
internal class InternalOp
{
public void Start(ProvideHandle provideHandle, TextDataProvider rawProvider)
{
/* 省略 */
}
}
public override void Provide(ProvideHandle provideHandle)
{
new InternalOp().Start(provideHandle, this);
}
}
TextDataProvider は内部に InternalOp というのを保持しており、このインスタンスに処理を委ねているらしい。
internal class InternalOp
{
ProvideHandle m_PI;
public void Start(ProvideHandle provideHandle, TextDataProvider rawProvider)
{
m_PI = provideHandle;
var path = m_PI.ResourceManager.TransformInternalId(m_PI.Location);
Startメソッドの実装を見てみると TransformInternalId というメソッドで Location のInternalIdを変換する作業を行なっている。
public string TransformInternalId(IResourceLocation location)
{
return InternalIdTransformFunc == null ? location.InternalId : InternalIdTransformFunc(location);
}
ResourceManager.InternalIdTransformFuncはAddressables.InternalIdTransformFuncから指定が可能だが、特に指定がない限りはそのままInternalIdを返す。
このTransformInternalIdによって変換されたInternalId値をpathとして使っている。
今回はInternalIdTransformFuncは指定していないので "Library/com.unity.addressables/aa/OSX/settings.json"がpathに設定されているはずである。
var path = m_PI.ResourceManager.TransformInternalId(m_PI.Location);
if (ResourceManagerConfig.ShouldPathUseWebRequest(path))
{
SendWebRequest(path);
}
else if (File.Exists(path))
{
var text = File.ReadAllText(path);
object result = ConvertText(text);
m_PI.Complete(result, result != null, result == null ? new Exception($"Unable to load asset of type {m_PI.Type} from location {m_PI.Location}.") : null);
m_Complete = true;
}
ResourceManagerConfig.ShouldPathUseWebRequest は path の中に "://"が含まれる場合にtrueを返す。(ただしAndroidの場合のみ指定のpathにファイルが存在する場合はfalseが返る)
今回の場合はpathに "://" は含まれないのでこの条件は通らない。
その為、その次の else if の確認が行われる。
アセットのビルドを行なっていれば "Library/com.unity.addressables/aa/OSX" 以下にsettings.jsonが入っているはずなので、つまりここの条件式が通ることになる。
よってFile.ReadAllText(path)でテキストファイルが読み込まれるはず。
その後、読み込んだtextをConvertText
で変換しているようなので、次にこのメソッドを確認する。
private object ConvertText(string text)
{
try
{
return m_Provider.Convert(m_PI.Type, text);
}
catch (Exception e)
{
if (!m_IgnoreFailures)
Debug.LogException(e);
return null;
}
}
ここで、TextDataProvider.Convertが呼び出されていることがわかる。
JsonAssetProvider であれば JsonUtility.FromJson が呼ばれるはず。
つまりここまで非常に長く複雑な流れを辿ってきたがやっている事は"Library/com.unity.addressables/aa/OSX/settings.json"をFile.ReadAllTextしてきてJsonUtility.FromJsonしているだけであることがわかる。
Addressables はロード先がローカルファイルなのかそれともサーバからのダウンロードなのかまでをInternalIdで判別するように実装されているので、もしInternalIdにアセットサーバのURLなどが含まれた場合、今度はUnityWebRequestを使って取得してくるようである。
長くなってしまったので一旦ここまで