Dependency Injection (Zenject)
Esta página documenta código interno del motor disponible exclusivamente para licenciatarios de las ediciones Source y Studio.
Zenject conecta a través de esos límites de módulo, y solo en esa dirección: un instalador de Core nunca resuelve nada de Online.
MCE usa Zenject (a través del fork Extenject) para la inyección de dependencias. Esta página cubre las convenciones, patrones y todos los instaladores del motor para que puedas extender el grafo de DI con tus propios sistemas.
¿Por qué Zenject?
MCE es un motor grande y modular donde sistemas como batallas, monstruos, guardado, audio y UI necesitan referenciarse entre sí sin acoplamiento rígido. Zenject proporciona:
- Inyección por constructor para MonoBehaviours (vía
[Inject]enConstruct()) sin depender deFindObjectOfTypeo singletons. - Instaladores basados en ScriptableObject que pueden configurarse en el Inspector e intercambiarse entre proyectos.
- Patrón Factory para instanciación de prefabs en tiempo de ejecución con inyección automática.
- Contenedores con ámbito (ProjectContext vs SceneContext) para que los sistemas globales y locales por escena coexistan limpiamente.
ProjectContext
Los bindings globales de MCE viven en el prefab ProjectContext en Assets/Resources/ProjectContext.prefab. Zenject carga este prefab automáticamente antes de que se ejecute cualquier escena. Todos los assets ScriptableObjectInstaller referenciados en este prefab se instalan una vez y persisten durante toda la vida útil de la aplicación.
Aquí es donde se vinculan los sistemas centrales del motor: la base de datos de monstruos, sistema de guardado, audio, entrada, renderizado, lanzador de batallas, gestor de evolución, localización y más.
El Patrón Installer
MCE usa subclases de ScriptableObjectInstaller casi exclusivamente. Cada instalador es un asset ScriptableObject (creado mediante CreateAssetMenu) que mantiene referencias serializadas a los sistemas que instala.
Convención Base: LazySingletonScriptableInstaller
La mayoría de los instaladores de MCE extienden una clase base de conveniencia:
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();
}
}
Esto hace tres cosas:
- Encola el ScriptableObject para inyección para que sus propios campos
[Inject]se resuelvan. - Lo vincula como singleton para que cualquier clase que declare
[Inject] private T myRef;reciba la misma instancia. - Marca el binding como
.Lazy()para que el objeto no se resuelva anticipadamente al construir el contenedor.
También existe una variante LazySingletonMonoInstaller<T> para sistemas basados en MonoBehaviour.
La Convención .AsSingle().Lazy()
Cada binding de MCE usa .AsSingle().Lazy(). He aquí el porqué:
.AsSingle()— Garantiza que exista exactamente una instancia por contenedor. Esto evita managers duplicados accidentales..Lazy()— Difiere la resolución hasta el primer uso. Como MCE tiene más de 20 instaladores, la resolución anticipada causaría problemas de orden de inicialización donde un sistema intenta usar otro que aún no ha terminado de configurarse.
Todos los Instaladores de MCE
Aquí está la lista completa de instaladores del motor:
Instaladores del Runtime Principal
| Instalador | Ubicación | Qué Vincula |
|---|---|---|
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 |
SDKInstaller | Runtime/SDK/ | IMonsterCaptureEngine (el punto de entrada público del SDK) |
La capa online añade uno más, OnlineBattleInstaller (MCE.Online.Runtime, en Online/Runtime/Battle/). La carpeta Events/ de Foundation (los ScriptableObject EventChannel) no necesita instalador.
Instaladores de Foundation
| Instalador | Ubicación | Qué Vincula |
|---|---|---|
AudioInstaller | Foundation/Runtime/Audio/ | IAudioManager |
LocalizationInstaller | Foundation/Runtime/Localization/ | ILocalizer |
SceneManagerInstaller | Foundation/Runtime/SceneManagement/ | ISceneManager |
ConfigurationManagerInstaller | Foundation/Runtime/Configuration/ | Foundation-level config |
Patrones de Inyección
Patrón 1: [Inject] en Construct() (MonoBehaviours)
Este es el patrón preferido para MonoBehaviours. Zenject llama a Construct() automáticamente después de crear el objeto:
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));
}
Por qué Construct() en lugar de inyección por campo:
- Dependencias explícitas — Puedes ver todo lo que la clase necesita en una sola firma de método.
- Inmutabilidad — Almacena las referencias inyectadas en campos
private readonly; no pueden ser reasignadas accidentalmente. - Testeabilidad — Puedes llamar a
Construct()directamente en tests unitarios con objetos mock.
Patrón 2: Inyección por Campo [Inject] (ScriptableObjects)
Los ScriptableObjects que son QueueForInject'd usan inyección por campo ya que no tienen un ciclo de vida 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;
}
El instalador debe llamar a Container.QueueForInject(reference) antes del binding para que esto funcione.
Patrón 3: Patrón Factory (Instanciación en Tiempo de Ejecución)
Para prefabs que necesitan instanciarse en tiempo de ejecución con DI completa, MCE usa PlaceholderFactory de Zenject:
// En DialogManagerInstaller:
Container.BindFactory<DialogManager, DialogManager.Factory>()
.FromComponentInNewPrefab(Prefab);
// En PlayerCharacterInstaller:
Container.BindFactory<PlayerCharacter, PlayerCharacter.Factory>()
.FromComponentInNewPrefab(Prefab)
.AsSingle()
.Lazy();
Cualquier clase puede entonces inyectar la fábrica y crear instancias:
[Inject]
public void Construct(DialogManager.Factory dialogManagerFactory)
{
DialogManager dialog = dialogManagerFactory.Create();
// 'dialog' tiene todos sus campos [Inject] resueltos automáticamente
}
Patrón 4: Inyección Condicional con WhenInjectedInto
PlayerDataInstaller restringe los bindings de datos de jugador a clases que implementan IPlayerDataReceiver:
Container.Bind<Roster>()
.FromInstance(PlayerRoster)
.AsSingle()
.WhenInjectedInto<IPlayerDataReceiver>()
.Lazy();
Esto evita la inyección accidental de datos específicos del jugador en sistemas que no deberían depender de ellos.
Patrón 5: Vinculación por Interfaz
Cuando los sistemas tienen una capa de abstracción, el instalador vincula la interfaz a la instancia concreta:
// BattleLauncherInstaller
Container.Bind<IBattleLauncher>().FromInstance(Reference).AsSingle().Lazy();
Los consumidores inyectan la interfaz, no la clase concreta, permitiendo sustitución para pruebas o juego en línea.
Creando tu Propio Instalador
Paso 1: Crear el Instalador ScriptableObject
using UnityEngine;
using OpenMon.Foundation.DependencyInjection;
[CreateAssetMenu(menuName = "MCE/Dependency Injection/MySystemInstaller",
fileName = "MySystemInstaller")]
public class MySystemInstaller : LazySingletonScriptableInstaller<MySystem>
{
// Si tu sistema necesita bindings adicionales, sobrescribe InstallBindings:
public override void InstallBindings()
{
base.InstallBindings(); // Vincula MySystem como singleton
// Agrega bindings extra aquí
Container.Bind<IMyService>().To<MyServiceImpl>().AsSingle().Lazy();
}
}
Paso 2: Crear el Asset del Instalador
Haz clic derecho en la ventana Project y usa tu ruta de CreateAssetMenu para crear el asset. Asigna las referencias del ScriptableObject en el Inspector.
Paso 3: Registrar en ProjectContext
- Abre
Assets/Resources/ProjectContext.prefab. - Selecciona el componente
ProjectContext. - Agrega tu asset instalador a la lista
ScriptableObjectInstallers.
Paso 4: Inyectar en tus Clases
public class MyConsumer : OpenMonBehaviour<MyConsumer>
{
[Inject]
public void Construct(MySystem mySystem, IMyService myService)
{
// Listo para usar
}
}
Problemas Comunes
| Problema | Causa | Solución |
|---|---|---|
ZenjectException: Unable to resolve | Falta el binding o el instalador no está en ProjectContext | Verifica que el instalador esté agregado a ProjectContext y que el tipo coincida exactamente |
NullReferenceException en campo [Inject] | QueueForInject no fue llamado para el ScriptableObject | Agrega Container.QueueForInject(reference) antes de la llamada .Bind() |
| Objeto inyectado demasiado pronto | El sistema usa la dependencia inyectada en Awake() | Mueve la lógica a Construct() o Start(); Zenject inyecta después de Awake() |
| Error de binding duplicado | Mismo tipo vinculado en dos instaladores | Usa .WhenInjectedInto<>() para definir ámbito, o vincula a una interfaz |
| La fábrica crea objetos sin inyección | Se usó Instantiate() directamente | Usa el patrón factory de Zenject (BindFactory + Factory.Create()) |
Pruebas con Contenedores Mock
Para tests unitarios que necesitan DI, crea un DiContainer manualmente:
[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
}
Para tests de integración que necesitan el contenedor completo de MCE, usa [UnityTest] con una escena que tenga el ProjectContext configurado.