【Unity】Addressables Asset System のまとめ① Addressablesの各種プロパティ

#### 前提
前提としてプロジェクトには Addressables を既に組み込んでいるものとする。
バージョンは `1.19.19` を使用する。
MacOSで動かしているのでプラットフォーム名は `OSX` などになっているが、実行する環境によってここの名前は変化する点に注意。



#### 各ファイルの役割について
Addressables を動作させるにあたって絶対に必要なものとして `settings.json` がある。
このファイルはAddressables を初期化する際(`Addressables.InitializeAsync` を呼び出した時)に使用される。
Addressables 側で必要とするため、このファイルに関してはアプリビルド時に必ず組み込まれる。


##### 各種プロパティについて説明

<dl>
  <dt>LibraryPath</dt>
  <dd>
<font color=red>"Library/com.unity.addressables/"</font> を指す
  </dd>

  <dt>StreamingAssetsSubFolder</dt>
  <dd>
<font color=red>"aa"</font> を指す。<br>
アプリをビルドした際、必ずStreamingAssets以下に <b>aa</b> というフォルダが作られ、そこにsettings.jsonがコピーされる。
  </dd>

  <dt>RuntimePath</dt>
  <dd>
<b>settings.json</b>がどこに存在するのかが定義されている。<br>
Unityエディタ上の場合は LibraryPath + <font color=red>"aa"</font> + <font color=red>"/"</font> + Application.platform<br>
であり、実機の場合は<br>
Application.streamingAssetsPath + <font color=red>"/"</font> + <font color=red>"aa"</font><br>
を指す。
  </dd>
</dl>


#### Addressables の初期化の流れ
`Addressables` クラスは内部に AddressablesImpl という型のインスタンスを内包しており、基本的にこのクラスに書かれた処理を実行する。

```csharp
        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` というメソッドを呼び出して初期化を行う。
この為、エディタ上では基本的に実機と同じ状況にすることが出来ない

:::note info
(Addressables 初期化前にPlayerPrefsのAddressables.kAddressablesRuntimeDataPathをDeleteKeyするなどすれば エディタ上でも実機と同じような状態にすることはできる)
:::

今回はエディタ環境から強引な手法を使って実機と同じ流れでAddressablesが動くようにする
Addressables を初期化する際には `InitializationOperation` というクラスを生成して、そちらで初期化を行うように実装されている。

```csharp
// InitializeAsyncメソッド内
            if(!m_InitializationOperation.IsValid())
                m_InitializationOperation = Initialization.InitializationOperation.CreateInitializationOperation(this, runtimeDataPath, providerSuffix);
            if (autoReleaseHandle)
                AutoReleaseHandleOnCompletion(m_InitializationOperation);

            return m_InitializationOperation;
```
この生成された `InitializationOperation` インスタンスがInitializeAsyncメソッドの戻り値となっている。



次に、`CreateInitializationOperation`内部がどのような実装になっているか調べる。
```csharp
        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 インターフェースを実装している。
各種インターフェースプロパティとフィールドは以下のように対応付けされている。

```csharp
    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インターフェースを実装したクラスであり、
クラス図にするとこのような関係になっている。
```plantuml
Interface IResourceLocation {
    string InternalId { get; }
    string ProviderId { get; }
    IList<IResourceLocation> Dependencies { get; }
    int DependencyHashCode { get; }
    bool HasDependencies { get; }
    object Data { get; }
    string PrimaryKey { get; }
    Type ResourceType { get; }
    int Hash(Type resultType)
}

class "ResourceLocationBase" {
    - string m_Name
    - string m_Id
    - string m_ProviderId
    - object m_Data
    - int m_DependencyHashCode
    - int m_HashCode
    - Type m_Type
    - List<IResourceLocation> m_Dependencies
    - string m_PrimaryKey
      
    +ResourceLocationBase(name, id, providerId, type, dependencies)
}

IResourceLocation <|.. ResourceLocationBase
```


上記の実装から照らし合わせると、runtimeDataLocationのパラメータはこのように設定される
```csharp
var runtimeDataLocation = new ResourceLocationBase("RuntimeData", playerSettingsLocation, typeof(JsonAssetProvider).FullName, typeof(ResourceManagerRuntimeData));
```

![Addressables_img_04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/82cb4302-54b6-b8e2-dc79-522f85e0b863.jpeg)

そして、以下のようにResourceManager からProviderResourceというメソッドを呼び出して上記のパラメータが指定された **runtimeDataLocation** を渡している。
```csharp
var initOp = new InitializationOperation(aa);
initOp.m_rtdOp = aa.ResourceManager.ProvideResource<ResourceManagerRuntimeData>(runtimeDataLocation);
```

ResourceManager.ProviderResource の実装はやや複雑なので図で表す。
流れとしては以下の図のような感じ。
![Addressables_img_07.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/5778aed8-08fc-79f9-f29c-76774c0520a3.jpeg)

つまり、ResourceProvider は各種アセットのロード処理を提供するインターフェースと考えれば良いと思う。
利用する側はAsyncOperationを内部に持った AsyncOperationHandle のみを受け取り、ロードの進捗管理や結果の取得などはすべてこの AsyncOperationHandle を経由して行う。


今回のケースの場合、`JsonAssetProvider` を使って処理を行うのでJsonAssetProviderの実装を確認する。

```csharp
    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を確認する。

```csharp
    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 というのを保持しており、このインスタンスに処理を委ねているらしい。

```csharp
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を変換する作業を行なっている。


```csharp
public string TransformInternalId(IResourceLocation location)
{
    return InternalIdTransformFunc == null ? location.InternalId : InternalIdTransformFunc(location);
}
```
**ResourceManager.InternalIdTransformFunc**はAddressables.InternalIdTransformFuncから指定が可能だが、特に指定がない限りはそのままInternalIdを返す。
このTransformInternalIdによって変換されたInternalId値をpathとして使っている。
今回はInternalIdTransformFuncは指定していないので <font color=red>"Library/com.unity.addressables/aa/OSX/settings.json"</font>がpathに設定されているはずである。


```csharp
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 の中に <font color=red>**"://"**</font>が含まれる場合にtrueを返す。(ただしAndroidの場合のみ指定のpathにファイルが存在する場合はfalseが返る)
今回の場合はpathに <font color=red>**"://"**</font> は含まれないのでこの条件は通らない。
その為、その次の else if の確認が行われる。
アセットのビルドを行なっていれば <font color=red>"Library/com.unity.addressables/aa/OSX"</font> 以下にsettings.jsonが入っているはずなので、つまりここの条件式が通ることになる。

よってFile.ReadAllText(path)でテキストファイルが読み込まれるはず。
その後、読み込んだtextを`ConvertText`で変換しているようなので、次にこのメソッドを確認する。

```csharp
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 が呼ばれるはず。

つまりここまで非常に長く複雑な流れを辿ってきたがやっている事は<font color=red>"Library/com.unity.addressables/aa/OSX/settings.json"</font>をFile.ReadAllTextしてきてJsonUtility.FromJson<ResourceManagerRuntimeData>しているだけであることがわかる。

Addressables はロード先がローカルファイルなのかそれともサーバからのダウンロードなのかまでをInternalIdで判別するように実装されているので、もしInternalIdにアセットサーバのURLなどが含まれた場合、今度はUnityWebRequestを使って取得してくるようである。


長くなってしまったので一旦ここまで