概要

Unityで開発を行う場合、Monobehaviourの存在はとても便利な反面、常に批判の対象でした。
『できる限りピュアC#で!』というような合言葉が飛び交っている現場もあるかと思います。
そのいくつかの問題の解決策の一つとしてDIがあり、VContainerはDIを行う上で強力なパッケージの一つです。

今回は、PhotonFusionを利用したマルチプレイのアプリにVContainerを導入した際にハマった問題点を整理しつつ、どのように実装に落とし込んだのかをさらっと紹介したいと思います。
基本的なVContainerの説明やPhotonFusionの説明は行わないのでご了承くださいー!

SDKバージョンや使用モードについて

PhotonFusionバージョンは1.1.8を利用しており、使用モードはSharedモードです。

VContainer+PhotonFusionで考えないといけない要素

通常のプロジェクトと異なり、NetworkBehaviourに対してVContainerを利用する場合、考慮しないければならないPhotonFusionの特徴があります。そして、それによって引き起こされる解決しなければならない課題が存在します。

考慮しなければならない特徴は以下の三つです。

1. プレイヤー間で共有したい値がある場合Monobehaviour継承クラスが必要

値共有を行うためのNetworkPropatyは、NetworkBehaviourを継承したクラスで利用するする必要があり、NetworkBehaviourはMonobehaivourを継承しています。また、NetworkBehaviourはNetworkObjectを持つオブジェクトの配下にある必要があります。

2. 他ユーザーが生成したNetworkObjectが自動的に生成される

プレイヤーAがSpawnさせたNetworkObjectは、同じセッションにいる他プレイヤーのシーンにFusionが自動で同じNetworkObjectを生成します。そのため、他プレイヤーからは明示的にSpawnを行うタイミングがなくてもNetworkObjectが生成される場合があります。

3. オブジェクトが生成されていてもNetwork関連のメソッドが利用できない場合がある

オブジェクトが生成された後、NetworkObjectがNetwork化されるまでにすこしラグが存在し、オブジェクトが生成されていたとしてもNetworkBehaviour関連のメソッド等が動くわけではありません。NetworkBehavourが持つSpawned() というメソッドが呼ばれたタイミングでNetworkBehaviour関連のメソッドが利用できるようになります。

これらの特徴から、VContainerで解決しないといけない課題として

  • 動的に生成されたMonobehaviourクラスに対してコンテナへのRegisterやInjectを行えるようにする。
  • VContainerの依存関係の解決のタイミングを調整してNetworkObjectがNetwork化されるタイミングを待ってからクラスを初期化、起動させる。

の2点を解決する必要があります。

動的に生成したNetworkObjectのクラスをコンテナにRegister,Injectできるようにする

VContainerで動的に生成したオブジェクトのクラス等をInjectやRegisterする場合は、RegisterComponentInNewPrefabなどで登録を行い、Resolveのタイミングで生成したり、Factoryメソッドをコンテナに登録したりすることでクラス等をコンテナにRegisterすると思います。

しかし、Fusionの場合Fusion側が自動でオブジェクトを生成してしまうため明示的にこちらで生成するタイミングがない場合があります。

NetworkRunnerのStartGame時にINetworkObjectPool というものを設定できます。このインターフェイスをもつクラスはユーザーがSpawnを明示的に行った場合や自動でオブジェクトが生成される過程で経由されます。しかし、実際にこのクラスに生成を直接命令することはできません。そのため、VContainerにファクトリーメソッドを登録する方法では、このインターフェイスを利用してもVContainerのタイミングで依存関係の解決を行うことがことできないと思われます。(もしできたら、方法をご教示いただけたら嬉しいです。)

そのため、アプローチとしてはVContainerに登録したオブジェクトをInstantiateのタイミングで解決するのではなく、NetworkObjectが動的に生成された後、VContainerにNetworkObject関連のクラス等を登録しに行くアプローチをとりました。

具体的には、生成されるNetworkObjectのPrefabにLifeTimeScopeを継承したクラスとして、MutliPlayContextをアタッチし、生成時に親としてMainContextを設定することでMultiPlayContextでRegisterされたクラスからもMainContenxtのコンテナに登録されたものを参照できるようにしました。

これを実現する方法としては、二つあります。

一つは、生成するPrefabにアタッチしたMultiPlayContextのParentをMainContextにしておく方法です。下のように、ContextのParentを設定するとMultiPlayContextのLifeTimeScopeの親としてMainContextを設定できます。

二つ目は、Scriptから設定する方法です。

Awakeのタイミングでシーン上のMainCotenxtをFindし、parentReference.Object に指定すると、指定した親の子LifeTimeScopeとして設定することができます。

namespace Sample
{
public class MultiPlayContext : LifetimeScope
{
protected override void Awake()
{
var LifeTimeScope = FindObjectOfType<MainContext>();
if (LifeTimeScope != null)
parentReference.Object = LifeTimeScope;
else
{
Debug.LogError("Can't fine MainContext");
}
base.Awake();
}

protected override void Configure(IContainerBuilder builder)
{

}
}
}

これで、明示的にオブジェクトのSpawnを行うプレイヤーであっても、自動的にFusionからSpawnされるプレイヤーの場合でも、MainCotenxtの子LifeTimeScopeとしてコンテナにクラス等をregisterすることができるようになります。

Network化されるタイミングを考慮した依存関係の解決

次に、Network化されるタイミングを考慮した依存関係の解決についてですが。こちらについても、二つ方法があります。

NetworkObjectがSpawned()されるのを待って処理する。

これは、他の場面でも利用する方法でNetworkClassAでの初期化をSpawnedしていることを確認してから行う方法です。下のコードは、NetworkClassAをEntryPointからInitializeAsyncを呼び出される想定です。このようにSpawnedされてから初期化処理を行うことでNetwork化されたことが保証された状態で初期化できます。

    public class NetworkClassA : NetworkBehaviour
{
private bool isSpawn = false;
private HogeClass _hogeClass;
public NetworkClassA(HogeClass hogeClass)
{
_hogeClass = hogeClass;
}

public async UniTask InitializeAsync()
{
await UniTask.WaitUntil(() => isSpawn);
// 初期化処理
}

public override void Spawned()
{
base.Spawned();
isSpawn = true;
Debug.Log("Spawn");
}
}

しかし、この方法は、networkClassAが他のクラスから呼ばれるタイミング次第では問題が起こります。

    public class NetworkClassA : NetworkBehaviour
{
public IReadOnlyReactiveProperty<Unit> OnClickEvent => _onClickEvent;
private IReadOnlyReactiveProperty<Unit> _onClickEvent = new ReactiveProperty<Unit>();
[SerializeField] private Button _button;

private bool isSpawned = false;

public async UniTask Initialize()
{
await UniTask.WaitUntil(() => isSpawned);
_onClickEvent = _button.onClick.AsObservable().ToReactiveProperty();
// 初期化処理
}

public override void Spawned()
{
base.Spawned();
isSpawned = true;
Debug.Log("Spawned");
}
}

実際は、ボタンをSubScribeするこのような状態は起きないかもしれませんが、他のクラスがnetworkClassAの初期化が行われる前に、OnClickEventをSubscribeした場合正しく動作しません。
このような形で処理をする場合は、EntryPointで初期化の順序を考える必要があります。

VContainerはEntryPointとしてIAsyncStartableを提供しているので、遅延初期化を行うことも可能ですが、順序を考慮する必要があるのは少し面倒ではあります。

Spawned()した後にMultiPlayContextを生成し、Network化を保証する

上の方法では、生成するNetworkObjectにMultiPlayContextをアタッチしていました。それによって、オブジェクトの生成タイミングでMultiPlayContextの初期化が走ってしまい、先ほどのようなSpawnedするまでの遅延処理が必要になっています。

二つ目の方法では、Spawnedした後にMultiPlayContextをAddComponentで追加することでContextの初期化タイミングではSpawnedしていることを保証することができます。

    public class ContextCreator : NetworkBehaviour
{
public override void Spawned()
{
base.Spawned();
this.gameObject.AddComponent<MultiPlayContext>();
}
}

上のようなNetworkBehaviourを作成し、NetworkObjectをもつオブジェクトにアタッチすることで、Spawned()が起こった後にContextを初期化できます。

ただし、この場合はインスペクター上でContextの親を決めることができないので、MultiPlayContextのAwakeで親となるMainContextを設定する必要があります。

この方法だと、ContextのEntryPointでSpawnedしているかどうかを考えなくてよいので便利ではあります。しかし、オブジェクトが生成された時点でContextが生成されていないことになるので、UnityのゲームループのStartやUpdateの処理がContextが生成されていない状態で走ることに注意する必要があります。

おわりに

VContainerをがっつり触ったのがPhotonFusionの関わるプロジェクトだったのでここまでに至る試行錯誤が結構あったのですが、最終的にVContainerのみで依存関係の解決を行うことができました。FusionのINetworkObjectPoolを利用したらもう少し楽な方法があるのかもしれないなーと思ってはいるのですが、もし方法をご存じの方がいらっしゃればご教示いただければ幸いです。

PhotonFusionの話題としてはSDKのバージョン2.0がリリースされ、かなり大幅な変更があったようです。(変更点はこちら)ざっくりしか見ていませんがNetworkPropatyの変更をOnChangedで受け取る処理がなくなり、今後追加されるか検討中のようです。今回開発しているプロジェクトで結構使ってしまっているので、移行は一旦様子見かな。。と思っています。

参考にさせていただいた記事

VContainer入門
https://qiita.com/sakano/items/b91e01f7fc0a946090ac

VContainer公式サイト
https://vcontainer.hadashikick.jp/ja



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる