メインコンテンツまでスキップ

Dependency Injection (Zenject)

Source / Studio エディション限定

このページは、Source / Studio エディションのライセンス所有者のみが利用可能な内部エンジンコードについて説明しています。

インストーラーの配置

Zenject はそれらのモジュール境界をまたいで配線しますが、その方向だけです。Core のインストーラーが Online から何かを解決することはありません。

MCEは依存性注入にZenject(Extenjectフォーク経由)を使用しています。このページでは、独自のシステムでDIグラフを拡張できるように、慣例、パターン、およびエンジンのすべてのインストーラーについて説明します。

Zenjectを選ぶ理由​

MCEは、バトル、モンスター、セーブ、オーディオ、UIなどのシステムがハードカップリングなしに相互参照する必要がある大規模でモジュール式のエンジンです。Zenjectは以下を提供します:

  • コンストラクターインジェクション — FindObjectOfTypeやシングルトンに依存せずに、MonoBehaviourのConstruct()で[Inject]を使用。
  • ScriptableObjectベースのインストーラー — Inspectorで設定可能で、プロジェクト間で交換可能。
  • Factoryパターン — 自動インジェクションによるランタイムのプレハブインスタンス化。
  • スコープ付きコンテナ(ProjectContext vs SceneContext) — グローバルシステムとシーン単位のシステムをクリーンに共存させます。

ProjectContext​

MCEのグローバルバインディングは、Assets/Resources/ProjectContext.prefabのProjectContextプレハブにあります。Zenjectはシーン実行前にこのプレハブを自動的にロードします。このプレハブで参照されているすべてのScriptableObjectInstallerアセットは、一度インストールされ、アプリケーションの全寿命期間中持続します。

モンスターデータベース、セーブシステム、オーディオ、入力、レンダリング、バトルランチャー、進化管理、ローカライゼーションなど、エンジンの中核システムがここにバインドされます。

インストーラーパターン​

MCEはほぼ独占的にScriptableObjectInstallerサブクラスを使用します。各インストーラーは、インストールするシステムへのシリアル化された参照を保持するScriptableObjectアセット(CreateAssetMenu経由で作成)です。

基本規約: LazySingletonScriptableInstaller​

ほとんどのMCEインストーラーは便利なベースクラスを拡張します:

public class LazySingletonScriptableInstaller<T> : ScriptableObjectInstaller
where T : ScriptableObject
{
[SerializeField]
protected T Reference;

public override void InstallBindings()
{
Container.QueueForInject(Reference);
Container.Bind<T>().FromInstance(Reference).AsSingle().Lazy();
}
}

これにより3つの処理が行われます:

  1. ScriptableObjectを注入キューに入れる — 自身の[Inject]フィールドが解決されるようにします。
  2. シングルトンとしてバインドする — [Inject] private T myRef;を宣言するすべてのクラスが同じインスタンスを受け取るようにします。
  3. バインディングを.Lazy()としてマークする — コンテナ構築時にオブジェクトが即座に解決されないようにします。

MonoBehaviourベースのシステム向けにLazySingletonMonoInstaller<T>バリアントもあります。

.AsSingle().Lazy() 規約​

すべてのMCEバインディングは.AsSingle().Lazy()を使用します。その理由は以下の通りです:

  • .AsSingle() — コンテナごとに正確に1つのインスタンスが存在することを保証します。これにより、誤った重複マネージャーを防ぎます。
  • .Lazy() — 最初の使用時まで解決を延期します。MCEには20以上のインストーラーがあるため、即時解決では初期化順序の問題が発生し、あるシステムがまだセットアップが完了していない別のシステムを使用しようとする可能性があります。

すべてのMCEインストーラー​

以下がエンジン内のインストーラーの完全なリストです:

コアランタイムインストーラー​

インストーラー場所バインディング内容
GameFlowInstallerRuntime/GameFlow/NewGameInitializer, PlayerTeleporter, TimeManager
MonsterDatabaseInstallerRuntime/MonsterDatabase/DependencyInjection/MonsterDatabaseInstance, ExperienceLookupTable
PlayerDataInstallerRuntime/Player/Roster, Bag, MonsterStorage, Dex, CharacterData, GlobalGameData, PlayerSettings, QuestManager
BattleLauncherInstallerRuntime/Battle/IBattleLauncher(BattleLauncher経由)
SinglePlayerPlayerControlManagerInstallerRuntime/Battle/PlayerControl/バトルプレイヤーコントロールマネージャー
SavegameInstallerRuntime/Saves/SavegameManager, ISerializer<string>(MCESavesSerializer経由)
DialogManagerInstallerRuntime/UI/Dialogs/DialogManager.Factory
PlayerCharacterInstallerRuntime/Characters/PlayerCharacter.Factory
EvolutionManagerInstallerRuntime/Monster/Evolution/EvolutionManager
HatchingManagerInstallerRuntime/Monster/Breeding/HatchingManager
TradeManagerInstallerRuntime/Monster/Trade/TradeManager
InputManagerInstallerRuntime/Input/IInputManager
ConfigurationManagerInstallerRuntime/Configuration/IConfigurationManager
AudioSettingsInstallerRuntime/Audio/オーディオ設定マネージャー
RenderingInstallerRuntime/Rendering/RenderingManager
UIElementsInstallerRuntime/UI/UI要素参照
MapSceneLauncherInstallerRuntime/UI/Map/マップシーンランチャー
GlobalGridManagerInstallerRuntime/World/GlobalGridManager
GridInstallerRuntime/World/GridController
SceneInfoInstallerRuntime/World/SceneInfo
TileDataInstallerRuntime/World/TileData
WorldDatabaseInstallerRuntime/World/WorldDatabase
SDKInstallerRuntime/SDK/IMonsterCaptureEngine(公開 SDK のエントリポイント)

オンライン層はさらに1つ、OnlineBattleInstaller(MCE.Online.Runtime、Online/Runtime/Battle/ 配下)を追加します。Foundation の Events/ フォルダー(EventChannel ScriptableObject)にはインストーラーは不要です。

Foundationインストーラー​

インストーラー場所バインディング内容
AudioInstallerFoundation/Runtime/Audio/IAudioManager
LocalizationInstallerFoundation/Runtime/Localization/ILocalizer
SceneManagerInstallerFoundation/Runtime/SceneManagement/ISceneManager
ConfigurationManagerInstallerFoundation/Runtime/Configuration/Foundationレベルの設定

注入パターン​

パターン1: Construct()での[Inject](MonoBehaviours)​

これはMonoBehaviourの推奨パターンです。Zenjectはオブジェクト作成後にConstruct()を自動的に呼び出します:

public class MCEInitialization : OpenMonBehaviour<MCEInitialization>
{
[Inject]
public void Construct(DialogManager.Factory dialogManagerFactory,
RenderingManager renderingManager,
MonsterDatabaseInstance monsterDatabase,
WorldDatabase worldDatabase,
ILocalizer localizer) =>
StartCoroutine(Initialize(dialogManagerFactory,
renderingManager,
monsterDatabase,
worldDatabase,
localizer));
}

フィールドインジェクションではなくConstruct()を使う理由:

  • 明示的な依存関係 — クラスが必要とするすべてを1つのメソッドシグネチャで確認できます。
  • 不変性 — 注入された参照をprivate readonlyフィールドに保存し、誤って再割り当てされることを防ぎます。
  • テスト容易性 — ユニットテストでモックオブジェクトを使ってConstruct()を直接呼び出せます。

パターン2: [Inject] フィールドインジェクション(ScriptableObjects)​

QueueForInjectされるScriptableObjectsは、Construct()ライフサイクルがないためフィールドインジェクションを使用します:

public class EvolutionManager : OpenMonScriptable<EvolutionManager>
{
[Inject] private ILocalizer localizer;
[Inject] private ISceneManager sceneManager;
[Inject] private IInputManager inputManager;
[Inject] private GlobalGridManager globalGridManager;
[Inject] private TimeManager timeManager;
[Inject] private MCESettings settings;
[Inject] private MonsterDatabaseInstance database;
}

これを機能させるには、インストーラーがバインディング前にContainer.QueueForInject(reference)を呼び出す必要があります。

パターン3: Factoryパターン(ランタイムインスタンス化)​

完全なDIでランタイムにインスタンス化する必要があるプレハブのために、MCEはZenjectのPlaceholderFactoryを使用します:

// DialogManagerInstaller内:
Container.BindFactory<DialogManager, DialogManager.Factory>()
.FromComponentInNewPrefab(Prefab);

// PlayerCharacterInstaller内:
Container.BindFactory<PlayerCharacter, PlayerCharacter.Factory>()
.FromComponentInNewPrefab(Prefab)
.AsSingle()
.Lazy();

任意のクラスがファクトリーを注入してインスタンスを作成できます:

[Inject]
public void Construct(DialogManager.Factory dialogManagerFactory)
{
DialogManager dialog = dialogManagerFactory.Create();
// 'dialog' のすべての [Inject] フィールドが自動的に解決されます
}

パターン4: WhenInjectedIntoによる条件付きインジェクション​

PlayerDataInstallerは、IPlayerDataReceiverを実装するクラスにのみプレイヤーデータバインディングを制限します:

Container.Bind<Roster>()
.FromInstance(PlayerRoster)
.AsSingle()
.WhenInjectedInto<IPlayerDataReceiver>()
.Lazy();

これにより、依存すべきでないシステムへのプレイヤー固有データの誤った注入を防ぎます。

パターン5: インターフェースバインディング​

システムに抽象化レイヤーがある場合、インストーラーはインターフェースを具象インスタンスにバインドします:

// BattleLauncherInstaller
Container.Bind<IBattleLauncher>().FromInstance(Reference).AsSingle().Lazy();

コンシューマーは具象クラスではなくインターフェースを注入するため、テストやオンラインプレイ用の代替が可能になります。

独自のインストーラーを作成する​

ステップ1: ScriptableObjectインストーラーの作成​

using UnityEngine;
using OpenMon.Foundation.DependencyInjection;

[CreateAssetMenu(menuName = "MCE/Dependency Injection/MySystemInstaller",
fileName = "MySystemInstaller")]
public class MySystemInstaller : LazySingletonScriptableInstaller<MySystem>
{
// システムが追加のバインディングを必要とする場合、InstallBindingsをオーバーライド:
public override void InstallBindings()
{
base.InstallBindings(); // MySystemをシングルトンとしてバインド

// ここに追加バインディングを記述
Container.Bind<IMyService>().To<MyServiceImpl>().AsSingle().Lazy();
}
}

ステップ2: インストーラーアセットの作成​

Projectウィンドウで右クリックし、CreateAssetMenuパスを使用してアセットを作成します。InspectorでScriptableObject参照を割り当てます。

ステップ3: ProjectContextに登録​

  1. Assets/Resources/ProjectContext.prefabを開きます。
  2. ProjectContextコンポーネントを選択します。
  3. インストーラーアセットをScriptableObjectInstallersリストに追加します。

ステップ4: クラスにインジェクト​

public class MyConsumer : OpenMonBehaviour<MyConsumer>
{
[Inject]
public void Construct(MySystem mySystem, IMyService myService)
{
// 使用準備完了
}
}

よくある落とし穴​

問題原因解決策
ZenjectException: Unable to resolveバインディングがない、またはインストーラーがProjectContextにないインストーラーがProjectContextに追加されているか、タイプが正確に一致しているか確認
[Inject] フィールドで NullReferenceExceptionScriptableObjectに対してQueueForInjectが呼ばれていない.Bind()呼び出しの前にContainer.QueueForInject(reference)を追加
オブジェクトが早すぎるタイミングで注入されるシステムがAwake()で注入された依存関係を使用しているロジックをConstruct()またはStart()に移動;ZenjectはAwake()後に注入
重複バインディングエラー同じタイプが2つのインストーラーでバインドされている.WhenInjectedInto<>()でスコープを設定するか、インターフェースにバインド
ファクトリーが注入なしでオブジェクトを作成するInstantiate()を直接使用したZenjectのファクトリーパターン(BindFactory + Factory.Create())を使用

モックコンテナを使ったテスト​

DIが必要なユニットテストでは、手動でDiContainerを作成します:

[Test]
public void MySystem_ShouldDoSomething()
{
var container = new DiContainer();
var mockLocalizer = Substitute.For<ILocalizer>();

container.Bind<ILocalizer>().FromInstance(mockLocalizer);
container.Bind<MySystem>().AsSingle();

var system = container.Resolve<MySystem>();
// 動作をアサート
}

完全なMCEコンテナが必要な統合テストでは、ProjectContextが設定されたシーンで[UnityTest]を使用します。