Dependency Injection (Zenject)
This page documents internal engine code available exclusively to Source tier licensees.
MCE uses Zenject (via the Extenject fork) for dependency injection. This page covers the conventions, patterns, and all of the engine's installers so you can extend the DI graph with your own systems.
Why Zenject?
MCE is a large, modular engine where systems like battles, monsters, saves, audio, and UI need to reference each other without hard coupling. Zenject provides:
- Constructor injection for MonoBehaviours (via
[Inject]onConstruct()) without relying onFindObjectOfTypeor singletons. - ScriptableObject-based installers that can be configured in the Inspector and swapped between projects.
- Factory pattern for runtime prefab instantiation with automatic injection.
- Scoped containers (ProjectContext vs SceneContext) so global systems and per-scene systems coexist cleanly.
ProjectContext
MCE's global bindings live on the ProjectContext prefab at Assets/Resources/ProjectContext.prefab. Zenject automatically loads this prefab before any scene runs. All ScriptableObjectInstaller assets referenced on this prefab are installed once and persist for the entire application lifetime.
This is where the engine's core systems are bound: the monster database, save system, audio, input, rendering, battle launcher, evolution manager, localization, and more.
The Installer Pattern
MCE uses ScriptableObjectInstaller subclasses almost exclusively. Each installer is a ScriptableObject asset (created via CreateAssetMenu) that holds serialized references to the systems it installs.
Base Convention: LazySingletonScriptableInstaller
Most MCE installers extend a convenience base class:
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();
}
}
This does three things:
- Queues the ScriptableObject for injection so its own
[Inject]fields are resolved. - Binds it as a singleton so any class that declares
[Inject] private T myRef;receives the same instance. - Marks the binding as
.Lazy()so the object is not eagerly resolved at container build time.
There is also a LazySingletonMonoInstaller<T> variant for MonoBehaviour-based systems.
The .AsSingle().Lazy() Convention
Every MCE binding uses .AsSingle().Lazy(). Here is why:
.AsSingle()-- Ensures exactly one instance exists per container. This prevents accidental duplicate managers..Lazy()-- Defers resolution until first use. Since MCE has 20+ installers, eager resolution would cause initialization order issues where a system tries to use another system that has not finished setting up yet.
All MCE Installers
Here is the complete list of installers in the engine:
Core Runtime Installers
| Installer | Location | What It Binds |
|---|---|---|
GameFlowInstaller | Runtime/GameFlow/ | NewGameInitializer, PlayerTeleporter, TimeManager |
MonsterDatabaseInstaller | Runtime/MonsterDatabase/DependencyInjection/ | MonsterDatabaseInstance, ExperienceLookupTable |
PlayerDataInstaller | Runtime/Player/ | Roster, Bag, MonsterStorage, Dex, CharacterData, GlobalGameData, PlayerSettings, QuestManager |
BattleLauncherInstaller | Runtime/Battle/ | IBattleLauncher (via BattleLauncher) |
SinglePlayerPlayerControlManagerInstaller | Runtime/Battle/PlayerControl/ | Battle player control manager |
SavegameInstaller | Runtime/Saves/ | SavegameManager, ISerializer<string> (via MCESavesSerializer) |
DialogManagerInstaller | Runtime/UI/Dialogs/ | DialogManager.Factory |
PlayerCharacterInstaller | Runtime/Characters/ | PlayerCharacter.Factory |
EvolutionManagerInstaller | Runtime/Monster/Evolution/ | EvolutionManager |
HatchingManagerInstaller | Runtime/Monster/Breeding/ | HatchingManager |
TradeManagerInstaller | Runtime/Monster/Trade/ | TradeManager |
InputManagerInstaller | Runtime/Input/ | IInputManager |
ConfigurationManagerInstaller | Runtime/Configuration/ | IConfigurationManager |
AudioSettingsInstaller | Runtime/Audio/ | Audio settings manager |
RenderingInstaller | Runtime/Rendering/ | RenderingManager |
UIElementsInstaller | Runtime/UI/ | UI element references |
MapSceneLauncherInstaller | Runtime/UI/Map/ | Map scene launcher |
GlobalGridManagerInstaller | Runtime/World/ | GlobalGridManager |
GridInstaller | Runtime/World/ | GridController |
SceneInfoInstaller | Runtime/World/ | SceneInfo |
TileDataInstaller | Runtime/World/ | TileData |
WorldDatabaseInstaller | Runtime/World/ | WorldDatabase |
Foundation Installers
| Installer | Location | What It Binds |
|---|---|---|
AudioInstaller | Foundation/Runtime/Audio/ | IAudioManager |
LocalizationInstaller | Foundation/Runtime/Localization/ | ILocalizer |
SceneManagerInstaller | Foundation/Runtime/SceneManagement/ | ISceneManager |
ConfigurationManagerInstaller | Foundation/Runtime/Configuration/ | Foundation-level config |
EventBusInstaller | Foundation/Runtime/Events/ | Event bus system |
Injection Patterns
Pattern 1: [Inject] on Construct() (MonoBehaviours)
This is the preferred pattern for MonoBehaviours. Zenject calls Construct() automatically after the object is created:
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));
}
Why Construct() over field injection:
- Explicit dependencies -- You can see everything the class needs in one method signature.
- Immutability -- Store injected references in
private readonlyfields; they cannot be accidentally reassigned. - Testability -- You can call
Construct()directly in unit tests with mock objects.
Pattern 2: [Inject] Field Injection (ScriptableObjects)
ScriptableObjects that are QueueForInject'd use field injection since they do not have a Construct() lifecycle:
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;
}
The installer must call Container.QueueForInject(reference) before binding for this to work.
Pattern 3: Factory Pattern (Runtime Instantiation)
For prefabs that need to be instantiated at runtime with full DI, MCE uses Zenject's PlaceholderFactory:
// In DialogManagerInstaller:
Container.BindFactory<DialogManager, DialogManager.Factory>()
.FromComponentInNewPrefab(Prefab);
// In PlayerCharacterInstaller:
Container.BindFactory<PlayerCharacter, PlayerCharacter.Factory>()
.FromComponentInNewPrefab(Prefab)
.AsSingle()
.Lazy();
Any class can then inject the factory and create instances:
[Inject]
public void Construct(DialogManager.Factory dialogManagerFactory)
{
DialogManager dialog = dialogManagerFactory.Create();
// 'dialog' has all its [Inject] fields resolved automatically
}
Pattern 4: Conditional Injection with WhenInjectedInto
PlayerDataInstaller restricts player data bindings to classes that implement IPlayerDataReceiver:
Container.Bind<Roster>()
.FromInstance(PlayerRoster)
.AsSingle()
.WhenInjectedInto<IPlayerDataReceiver>()
.Lazy();
This prevents accidental injection of player-specific data into systems that should not depend on it.
Pattern 5: Interface Binding
When systems have an abstraction layer, the installer binds the interface to the concrete instance:
// BattleLauncherInstaller
Container.Bind<IBattleLauncher>().FromInstance(Reference).AsSingle().Lazy();
Consumers inject the interface, not the concrete class, enabling substitution for testing or online play.
Creating Your Own Installer
Step 1: Create the ScriptableObject Installer
using UnityEngine;
using OpenMon.Foundation.DependencyInjection;
[CreateAssetMenu(menuName = "MCE/Dependency Injection/MySystemInstaller",
fileName = "MySystemInstaller")]
public class MySystemInstaller : LazySingletonScriptableInstaller<MySystem>
{
// If your system needs additional bindings, override InstallBindings:
public override void InstallBindings()
{
base.InstallBindings(); // Binds MySystem as singleton
// Add extra bindings here
Container.Bind<IMyService>().To<MyServiceImpl>().AsSingle().Lazy();
}
}
Step 2: Create the Installer Asset
Right-click in the Project window and use your CreateAssetMenu path to create the asset. Assign the ScriptableObject references in the Inspector.
Step 3: Register on ProjectContext
- Open
Assets/Resources/ProjectContext.prefab. - Select the
ProjectContextcomponent. - Add your installer asset to the
ScriptableObjectInstallerslist.
Step 4: Inject in Your Classes
public class MyConsumer : OpenMonBehaviour<MyConsumer>
{
[Inject]
public void Construct(MySystem mySystem, IMyService myService)
{
// Ready to use
}
}
Common Pitfalls
| Problem | Cause | Solution |
|---|---|---|
ZenjectException: Unable to resolve | Binding missing or installer not on ProjectContext | Check the installer is added to ProjectContext and the type matches exactly |
NullReferenceException on [Inject] field | QueueForInject not called for the ScriptableObject | Add Container.QueueForInject(reference) before the .Bind() call |
| Object injected too early | System uses injected dependency in Awake() | Move logic to Construct() or Start(); Zenject injects after Awake() |
| Duplicate binding error | Same type bound in two installers | Use .WhenInjectedInto<>() to scope, or bind to an interface |
| Factory creates objects without injection | Used Instantiate() directly | Use the Zenject factory pattern (BindFactory + Factory.Create()) |
Testing with Mock Containers
For unit tests that need DI, create a DiContainer manually:
[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>();
// Assert behavior
}
For integration tests that need the full MCE container, use [UnityTest] with a scene that has the ProjectContext configured.