【Unity】ZenjectSceneLoaderをAddressables.LoadSceneAsync()に対応させる【Extenject】

# はじめに
最近やっとAddressable Asset System（Addressables？正式名称わからず）を使い始め、シーンのロードにも対応していることに驚きました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">UnityのAddressable Asset System、<br><br>専用の参照を使ってアセットを非同期ロードできる<br>↓<br>シーンファイルにも対応（！）<br>├→AssetBundle化されるのでBuildSettingsに登録不要<br>↓ <br>シーンの参照をインスペクタにD&amp;Dで設定できる🤩🤩🤩<br>↓<br>読込にマジックストリング・シーン名定数が不要<br><br>神だわ🙏</p>&mdash; su10@ハイパーカジュアルゲーム開発 (@su10_dev) <a href="https://twitter.com/su10_dev/status/1272352992532955136?ref_src=twsrc%5Etfw">June 15, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

自分は [Extenject](https://github.com/svermeulen/Extenject) 使いなので今後シーンのロードは Addressables + `ZenjectSceneLoader` でやろうと思ったのですが、`ZenjectSceneLoader` は `AssetReference` によるシーン読み込みに対応してなかったので、検証したり対応してみたりしました。

# Extenject with Addressablesの状況
[Unity Addressable Assets #8](https://github.com/svermeulen/Extenject/issues/8)（Extenjectリポジトリのissue）

>In terms of addressable assets, I'm not really sure. I haven't played with it yet. But we could certainly add it as an alternative to prefabs when it's ready

Addressablesからのプレハブのバインドに関するissueはあるっぽいのですが、シーンのロードに関してはなさそうでした。

# そもそも対応しないとどうなる？
|         API         | ProjectContext Bindings | Parent Bindings | Self Bindings |
| :------------------ | :---------------------: | :-------------: | :-----------: |
| `Addressables.LoadSceneAsync(.. Single)`                           | ⭕ | - | ⭕ |
| `Addressables.LoadSceneAsync(.. Additive)`                         | ⭕ | ❌ | ⭕ |
| `Addressables.LoadSceneAsync(.. Additive)` ＋ Contract Names       | ⭕ | ⭕ | ⭕ |
| `ZenjectSceneLoader#LoadSceneAsync(.. Single)`                     | ✅ | - | ✅ |
| `ZenjectSceneLoader#LoadSceneAsync(.. Additive, .. Child)`         | ✅ | ✅ | ✅ |
| `ZenjectSceneLoader#LoadSceneAsync(.. Additive)` ＋ Contract Names | ✅ | ✅ | ✅ |

* ⭕,❌：そのままの状態のときの挙動
* ✅：APIを足したことにより実現した挙動

検証してみたら `Addressables.LoadSceneAsync(.. Additive)` + Contract Names は普通に動いたので驚きましたが、 `ZenjectSceneLoader` を用いて動的にペアレンティングしたい場合は対応する必要がありそうでした。

# 対応させてみる

## 環境
* macOS Catalina 10.15.5
* Unity 2018.4.21f1
* [Extenject 8.0.1](https://github.com/svermeulen/Extenject/releases/tag/v8.0.1)
* Addressables 1.10.0
* エディター上・スタンドアロンビルドで簡単な動作検証済み

動作検証用に簡単なプロジェクトを作ったので興味ある人は[こちらのリポジトリ](https://github.com/su10/Extenject-with-Addressables)をクローンして触ってみてください。

## ExtenjectからAddressablesを参照できるようにする
それぞれAssembly DefinitionでDLLが区切られているので `zenject.asmdef` にAddressables関連の `asmdef` への参照を追加します。

<img width="425" alt="zenject_asmdef.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27863/a7741999-e11d-a28d-5fd4-26c161fa772f.png">

## AssetReferenceSceneAssetを実装する
`AssetReference` をそのまま使ってもいいのですが、 `SceneAsset` 以外の参照を渡したときにコンパイルエラーにしたいので `SceneAsset` 専用の参照クラスを実装します。

参考：[something like AssetReferenceT<SceneAsset>](https://forum.unity.com/threads/something-like-assetreferencet-sceneasset.812085/)（Unityフォーラム）

```AssetReferenceSceneAsset.cs
using System;
using UnityEngine.AddressableAssets;

[Serializable]
public class AssetReferenceSceneAsset : AssetReferenceT<
#if UNITY_EDITOR
    UnityEditor.SceneAsset
#else
    UnityEngine.Object
#endif
>
{
    public AssetReferenceSceneAsset(string guid) : base(guid)
    {
    }
}
```

このクラスをExtenjectから参照できるようにします。

* `Assets/Plugins/Zenject/Source` とかの `zenject.asmdef`配下に置く
* 自前の `asmdef` の配下に置いて先の手順のように `zenject.asmdef` に参照を足す

などプロジェクトに応じてよしなに設定します。

## `ZenjectSceneLoader` にメソッドを追加する

以下を追加します。

```ZenjectSceneLoader.cs
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;

...

public AsyncOperationHandle<SceneInstance> LoadSceneAsync(AssetReferenceSceneAsset sceneReference)
{
    return LoadSceneAsync(sceneReference, LoadSceneMode.Single);
}

public AsyncOperationHandle<SceneInstance> LoadSceneAsync(AssetReferenceSceneAsset sceneReference, LoadSceneMode loadMode)
{
    return LoadSceneAsync(sceneReference, loadMode, null);
}

public AsyncOperationHandle<SceneInstance> LoadSceneAsync(
    AssetReferenceSceneAsset sceneReference, LoadSceneMode loadMode, Action<DiContainer> extraBindings)
{
    return LoadSceneAsync(sceneReference, loadMode, extraBindings, LoadSceneRelationship.None);
}

public AsyncOperationHandle<SceneInstance> LoadSceneAsync(
    AssetReferenceSceneAsset sceneReference,
    LoadSceneMode loadMode,
    Action<DiContainer> extraBindings,
    LoadSceneRelationship containerMode)
{
    return LoadSceneAsync(
        sceneReference, loadMode, extraBindings, containerMode, null);
}

public AsyncOperationHandle<SceneInstance> LoadSceneAsync(
    AssetReferenceSceneAsset sceneReference,
    LoadSceneMode loadMode,
    Action<DiContainer> extraBindings,
    LoadSceneRelationship containerMode,
    Action<DiContainer> extraBindingsLate)
{
    PrepareForLoadScene(loadMode, extraBindings, extraBindingsLate, containerMode);

    return Addressables.LoadSceneAsync(sceneReference, loadMode);
}
```

ちなみにこれらは`ZenjectSceneLoader` の既存の `LoadSceneAsync()` をコピペして

* 返り値
* 引数
* `SceneManager`

をAddressables対応仕様に書き換えただけのものです。

これで `ZenjectSceneLoader` にシーンの参照を渡してロードしつつ、シーン内の `[Inject]` に適切にInjectされるようにできました。

![Extenject_with_Addressables.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27863/9877752b-3342-e83c-e711-26f2c256f2d9.gif)
*↑スタンドアロンビルドでの検証(gif)*

# 感想
Addressablesを使ったバインディングのベストプラクティスが知りたい。