環境
- Unity 2020.3.25 LTS
- VContainer 1.10.0
解決法
[Inject]
されるだけで他のクラスに注入されないクラスなら EntryPoint
を呼べるので、
必要な EntryPoint
へのインターフェイスだけを付けたクラスを用意してそのクラスに注入し、間接的に動かしてもらえばよい。
状況
UniRx + VContainerを使ってMV(R)Pで設計を進めると、基本的にはViewはuGUIと紐づくので MonoBehaviour
に、それ以外をできるだけピュアC#クラスにしてイベント駆動する、という方針になるかと思います。
(私もまだ初心者なので何がベストプラクティスなのかは把握できていません)
表示されるものにだけ MonoBehaviour
をつけて、それ以外はピュアC#クラスで書いておき、依存関係はすべてDIに任せる、整った世界ですね。
ところで、JobSystemなどで「1フレーム以内には終わらない処理なので定期的に進捗を確認する」というような処理をMVPのどれが担当するかを考えると、ロジックなのでModelに持たせたくなると思います。
MonoBehaviour
なしに PlayerLoop
に相当するタイミングで任意のC#クラスを動かす仕組みとして、VContainer
は EntryPoint
を提供しています。
じゃあそれを使おう、ということで Model
クラスに Update()
のタイミングで呼ばれるインターフェイス、Tick()
をつけて、無事……動きません。
原因
VContainer
に依存関係を教える LifeTimeScope
内では、注入用のクラスの定義とEntryPoint
用のクラスの定義が別になっているためです。
using VContainer;
using VContainer.Unity;
public sealed class YourLifeTimeScope : LifeTimeScope
{
protected override void Configure(IContainerBuilder builder)
{
// 注入されるクラスの生成方法の定義
builder.Register<Model>(LifeTime.Singleton);
// EntryPoint を呼ぶためのクラスの定義
builder.RegisterEntryPoint<Presenter>();
// 両方書くことはできるが Register<T>() とは別にもう一つ生成され、
// DI されるものと EntryPoint されるものが異なる状態になる
builder.RegisterEntryPoint<Model>();
// Scene 中に配置した GameObject についている Component に
// Register<T>() 済みのクラスを注入するための定義
builder.RegisterComponentInHierarchy<View>()
}
}
理由としては、EntryPoint
が呼ばれるクラスをLifeTime.Singleton
以外で好き勝手生成していたら、副作用が生じる関数を実装していた場合実行回数が定まらなくなり大変なことになってしまうので、EntryPoint
は専用で管理して注入用のクラスとは別けておく設計にしたと考えられます。
対策
要は、「ほかのクラスに注入するクラスにEntryPoint
をつけなければいい」というわけで、
EntryPoint
を呼ぶための専用のクラス、上図では ModelEntryPoint
を作り、Model
は ModelEntryPoint
に注入して間接的に動かしてもらえばよい、ということになります。
一応動かすだけなら
-
Model
もMonobehaviour
にして Unity標準のPlayerLoopを使う
(ただしSceneに配置しておく必要はある) -
Presenter
にEntryPoint
の定義も任せる
(ただしPresenter
の本来の責務: イベント処理の結合を定義 とは関連の薄い処理が混ざる)
といった方法もありますが、クリーンな設計を保つ意味でこのやり方がよいと思います。
参考
Official:
VContainer