【Unity】Addressables Asset System について調べる ②

前回
https://qiita.com/kawawa1989/items/2e1f2fe89002a5bef726
からの続き。


`settings.json` を読み込み **ResourceManagerRuntimeData** インスタンスを取得したところから。
そもそもsettings.jsonの中身はどうなっているのか？

```json
{
    "m_buildTarget": "StandaloneOSX",
    "m_SettingsHash": "",
    "m_CatalogLocations": [
        {
            "m_Keys": [
                "AddressablesMainContentCatalog"
            ],
            "m_InternalId": "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/catalog.json",
            "m_Provider": "UnityEngine.AddressableAssets.ResourceProviders.ContentCatalogProvider",
            "m_Dependencies": [],
            "m_ResourceType": {
                "m_AssemblyName": "Unity.Addressables, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
                "m_ClassName": "UnityEngine.AddressableAssets.ResourceLocators.ContentCatalogData"
            },
            "SerializedData": []
        }
    ],
    "m_ProfileEvents": false,
    "m_LogResourceManagerExceptions": true,
    "m_ExtraInitializationData": [],
    "m_DisableCatalogUpdateOnStart": false,
    "m_IsLocalCatalogInBundle": false,
    "m_CertificateHandlerType": {
        "m_AssemblyName": "",
        "m_ClassName": ""
    },
    "m_AddressablesVersion": "1.19.19",
    "m_maxConcurrentWebRequests": 500,
    "m_CatalogRequestsTimeout": 0
}
```
今回特に必要なのは `m_CatalogLocations`の中身。
settings.jsonの中には ResourceLocationDataが含まれている。
このm_CatalogLocationsを使ってResourceLocationMapインスタンスを生成している。

```csharp
AddressablesImpl m_Addressables; // フィールド

// ProvideResource で ResourceManagerRuntimeData を取得
AsyncOperationHandle<ResourceManagerRuntimeData> rtdOp = m_Addressables.ResourceManager.ProvideResource<ResourceManagerRuntimeData>(runtimeDataLocation);

.
.
.

// Operationが完了したとする
ResourceManagerRuntimeData rtd = m_rtdOp.Result; // Result には ResourceManagerRuntimeData インスタンスが含まれるはず

// CatalogLocations の情報を使用して ResourceLocationMap を生成する
ResourceLocationMap locMap = new ResourceLocationMap("CatalogLocator", rtd.CatalogLocations);

// 生成した locMap を追加する
m_Addressables.AddResourceLocator(locMap);

if (locMap.Locate(ResourceManagerRuntimeData.kCatalogAddress, typeof(ContentCatalogData), out var catalogs))
{
    Type provType = typeof(ProviderOperation<ContentCatalogData>);
    var catalogOp = m_Addressables.ResourceManager.CreateOperation<ProviderOperation<ContentCatalogData>>(provType, provType.GetHashCode(), null, null);

    IResourceProvider catalogProvider = null;
    foreach (IResourceProvider provider in m_Addressables.ResourceManager.ResourceProviders)
    {
        if (provider is ContentCatalogProvider)
        {
            catalogProvider = provider;
            break;
        }
    }

    // Dependencies が存在する場合は、そちらの解決を行うGroupOperation を生成
    var dependencies = addressables.ResourceManager.CreateGroupOperation<string>(loc.Dependencies, true);
    // catalogOp に必要な情報を設定
    catalogOp.Init(addressables.ResourceManager, catalogProvider, loc, dependencies, true);

    // Operation を実行
    // (Dependencies、catalogOpの両方の処理が完了したらDoneになるOperation)
    var catalogHandle = m_Addressables.ResourceManager.StartOperation(catalogOp, dependencies);
    dependencies.Release();

    // catalogHandle が完了するまで待機し、完了したらデリゲートを実行する Operation
    // ChainOperation を生成する。
    // catalogHandle が完了したら OnCatalogDataLoaded が呼ばれる。
    var chainOp = m_Addressables.ResourceManager.CreateChainOperation(catalogHandle, res => OnCatalogDataLoaded(addressables, res, providerSuffix, remoteHashLocation));
    m_loadCatalogOp = chainOp;
}
```
<font color=red>注意: 上記コードは実際の実装をかなり割愛しています。</font>
ResourceManagerRuntimeData の CatalogLocations に実際に読み込むべき catalog の Location 情報が含まれている。
今回の場合はInternalId: <font color=red>"{UnityEngine.AddressableAssets.Addressables.RuntimePath}/catalog.json"</font>
(つまり<font color=red>"Library/com.unity.addressables/aa/OSX/catalog.json"</font>)
を読み込む。

理由はよくわからないが、locationの中にあるProviderを参照しておらず、ハードコードでContentCatalogProviderを使用するように実装されている点が場合によっては少し注意する必要がある。
なぜかというと最初にロードする catalog に関しては必ず `ContentCatalogProvider` を使用しないといけない性質上、それ以外の ResourceProvider を使って catalog を読み込むことができない。

その為、例えば catalog を暗号化したいといった場面があるときに対応することができない。
もし、ここの ResourceProvider を外部から指定できるなら独自に暗号化した catalog を復号しながら読み込む ResourceProvider などを独自定義することも可能だが、残念ながらそういった事をしたい場合は Addressables を書き換える必要がある。(メンテナンスコストの観点から考えるとそれはそれで微妙な気がするが...)


ContentCatalogProvider がどのような処理を行なっているかは、前回のTextDataProviderと似た話になるのでここでは割愛。
先ほど生成したChainOperationの完了時コールバック **OnCatalogDataLoaded** メソッドを調べる。

```csharp
AsyncOperationHandle<IResourceLocator> OnCatalogDataLoaded(AddressablesImpl addressables, AsyncOperationHandle<ContentCatalogData> op, string providerSuffix, IResourceLocation remoteHashLocation)
{
    ContentCatalogData data = op.Result;
    foreach (var providerData in data.ResourceProviderData)
    {
        var provider = providerData.CreateInstance<IResourceProvider>(providerData.Id);
        m_Addressables.ResourceManager.ResourceProviders.Add(provider);
    }

    m_Addressables.InstanceProvider = data.InstanceProviderData.CreateInstance<IInstanceProvider>();
    m_Addressables.SceneProvider = data.SceneProviderData.CreateInstance<ISceneProvider>();

    ResourceLocationMap locMap = data.CreateCustomLocator(data.location.PrimaryKey, providerSuffix);
    m_Addressables.AddResourceLocator(locMap, data.localHash, data.location);
    m_Addressables.AddResourceLocator(new DynamicResourceLocator(addressables));
    // 結果だけを返す CompletedOperation を生成して返す
    return m_Addressables.ResourceManager.CreateCompletedOperation<IResourceLocator>(locMap, string.Empty);
}

```
<br>
<br>

おおまかな流れとしては以下のような感じ
1. ResourceRuntimeDataの取得
2. ResourceRuntimeDataの **CatalogLocations** を使って ResourceLocationMapを生成
3. 2.で生成した ResourceLocationMap を使用して ContentCatalogData を ContentCatalogProvider を使って取得する
4. ContentCatalogDataの取得に成功したらcatalogの情報を使い各種設定を行う
5. 初期化完了


### AsyncOperation の処理に関して
ここまででいくつかのAsyncOperationが出てきたが、非常にややこしいので情報をまとめる

クラス図
```plantuml

Interface IGenericProviderOperation {
}

Interface IAsyncOperation {
    object GetResultAsObject();
    Type ResultType { get; }
    int Version { get; }
    string DebugName { get; }
    void DecrementReferenceCount();
    void IncrementReferenceCount();
    int ReferenceCount { get; }
    float PercentComplete { get; }
    DownloadStatus GetDownloadStatus(HashSet<object> visited);
    AsyncOperationStatus Status { get; }
    Exception OperationException { get; }
    bool IsDone { get; }
    Action<IAsyncOperation> OnDestroy { set; }
    void GetDependencies(List<AsyncOperationHandle> deps);
    bool IsRunning { get; }

    event Action<AsyncOperationHandle> CompletedTypeless;
    event Action<AsyncOperationHandle> Destroyed;

    void InvokeCompletionEvent();
    System.Threading.Tasks.Task<object> Task { get; }
    void Start(ResourceManager rm, AsyncOperationHandle dependency, DelegateList<float> updateCallbacks);

    AsyncOperationHandle Handle { get; }

    void WaitForCompletion();
}

abstract class AsyncOperationBase<TObject> {
    #{abstract} void Execute()
    #void Destroy()
    #float Progress
    #string DebugName { get; }
    +void GetDependencies(List<AsyncOperationHandle> dependencies)
    +TObject Result { get; set; }
    +void WaitForCompletion()
    #bool InvokeWaitForCompletion()
    ~void DecrementReferenceCount()
}

class ChainOperation<TObject, TObjectDependency> {
    #bool InvokeWaitForCompletion()
}

class ProviderOperation {
    -AsyncOperationHandle<IList<AsyncOperationHandle>> m_DepOp
    #bool InvokeWaitForCompletion()
}

class GroupOperation {
    #bool InvokeWaitForCompletion()
}


class AsyncOperationHandle<TObject> {
}


AsyncOperationHandle <. IAsyncOperation
AsyncOperationHandle <. ProviderOperation

IAsyncOperation <|.. AsyncOperationBase
AsyncOperationBase <|-- ChainOperation
AsyncOperationBase <|-- ProviderOperation
AsyncOperationBase <|-- GroupOperation
IGenericProviderOperation <|.. ProviderOperation
```


ひとつひとつ順番に調べてみる
#### GroupOperation
```plantuml
class GroupOperation<IList<AsyncOperationHandle>> {
    -int m_LoadedCount
    -CompleteIfDependenciesComplete()
    #bool InvokeWaitForCompletion()
    #void Execute()
    +void GetDependencies(List<AsyncOperationHandle> deps)
}
AsyncOperationBase <|-- GroupOperation
IAsyncOperation <|.. AsyncOperationBase
```
`IList<AsyncOperationHandle>` を戻り値として持つ非同期処理クラス
各AsyncOperationHandle がロードを完了する度に m_LoadedCount をインクリメントさせ、全ての要素のロードが完了したらCompleteが実行される。
主に依存しているアセットバンドルをまとめてロード、同期するために使用される。


#### ProviderOperation
```plantuml
class AsyncOperationHandle<IList<AsyncOperationHandle>> {
}

class ProviderOperation<TObject> {
    -Func<float> m_GetProgressCallback
    -Func<DownloadStatus> m_GetDownloadProgressCallback
    -Func<bool> m_WaitForCompletionCallback;
    -IResourceProvider m_Provider
    -AsyncOperationHandle<IList<AsyncOperationHandle>> m_DepOp

    -CompleteIfDependenciesComplete()
    #bool InvokeWaitForCompletion()
    #void Execute()
    +void GetDependencies(List<AsyncOperationHandle> deps)
}

class ProvideHandle {
    -int m_Version
    -IGenericProviderOperation m_InternalOp
    -ResourceManager m_ResourceManager
}

AsyncOperationBase <|-- ProviderOperation
IAsyncOperation <|.. AsyncOperationBase
IGenericProviderOperation <|.. ProviderOperation
ProvideHandle <. ProviderOperation
AsyncOperationHandle <. ProviderOperation
AsyncOperationHandle <. GroupOperation
```
ResourceProvider の各種リソース読み込み処理を管理する非同期処理クラス
`TObject`型を戻り値に持つ。
`m_DepOp`はリソースに結びついた依存しているアセットバンドルの ProviderOperation インスタンスのリストで、まずm_DepOpの依存関係を先に解決し、m_DepOpに含まれる全ての ProviderOperation のロードが終わってからExecuteが実行される。


#### ChainOperation
```plantuml
class AsyncOperationHandle<TObjectDependency> {
}

class ChainOperation<TObject, TObjectDependency> {
    -AsyncOperationHandle<TObjectDependency> m_DepOp;
    -AsyncOperationHandle<TObject> m_WrappedOp;
    -Func<AsyncOperationHandle<TObjectDependency>, AsyncOperationHandle<TObject>> m_Callback;
    #float Progress { get; }

    ~DownloadStatus GetDownloadStatus(HashSet<object> visited)
    #void Destroy()
    #bool InvokeWaitForCompletion()
    #void Execute()
    #void Destroy()
}

AsyncOperationBase <|-- ChainOperation
IAsyncOperation <|.. AsyncOperationBase
IGenericProviderOperation <|.. ChainOperation
AsyncOperationHandle <. ChainOperation
```
非同期処理と非同期処理を連結させるときに使用されるクラス
`m_DepOp`に設定されたOperationが完了すると`m_WrappedOp`が実行され、`m_WrappedOp`が完了するとDoneとなる。
 

#### CompletedOperation
```plantuml

class CompletedOperation<TObject> {
    +Init(TObject result, bool success, Exception exception, bool releaseDependenciesOnFailure = true)
}

AsyncOperationBase <|-- CompletedOperation
IAsyncOperation <|.. AsyncOperationBase
```
その名の通り、ただresultを受け取ってCompleteを実行するだけのクラス。
既にロード済みのリソースを返す時などに使われる
先ほど書いた ContentCatalogData のロード処理でも OnCatalogDataLoaded で CompletedOperation を返している。


### AsyncOperationの共通ルール
・ResourceManager の CreateOperation でインスタンス生成する。
・ResourceManager の StartOperation から実行される。
・Init → Start → Execute の順番で実行される。
・依存が存在する場合(Start呼び出し時に有効なdependencyが存在する場合)、依存先が先に完了しないと実行されない

InitializationOperation.CreateInitializationOperationの初期化の流れで各Operationの処理の流れを辿ってみる

まず最初にInitializationOperationインスタンスを生成する。
```csharp
var initOp = new InitializationOperation(aa);
```
InitializationOperation も AsyncOperationBase を継承しているので、
他のAsyncOperationと同じように使うことができる。

```csharp
return aa.ResourceManager.StartOperation<IResourceLocator>(initOp, groupOpHandle);
```
ここでStartOperation内でStartが実行される。
しかし、`groupOpHandle`をdependencyとして渡しているため、
`groupOpHandle` が完了されてから `initOp` が Execute される。

groupOpHandle は `initOp.m_rtdOp` を dependency としているため、
`initOp.m_InitGroupOps` は `initOp.m_rtdOp` が終了されてから Execute される。
```csharp
var groupOpHandle = aa.ResourceManager.StartOperation(initOp.m_InitGroupOps, initOp.m_rtdOp);
```


図に起こすと以下のような流れ
![Addressables_img_08.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421285/b4aef62c-200a-203f-43e2-ec779bb3d92d.jpeg)