Dependency Injection (Zenject)
此页面记录的内部引擎代码仅供 Source 和 Studio 版许可证持有者使用。
Zenject 跨越这些模块边界进行装配,并且只朝一个方向:Core 中的安装器永远不会从 Online 中解析任何东西。
MCE 使用 Zenject(通过 Extenject 分支)进行依赖注入。本页涵盖约定、模式以及引擎的所有安装器,以便您可以用自己的系统扩展 DI 图。
为什么选择 Zenject?
MCE 是一个大型模块化引擎,其战斗、怪物、存档、音频和 UI 等系统需要在不硬耦合的情况下相互引用。Zenject 提供:
- MonoBehaviour 的构造函数注入(通过
Construct()上的[Inject]),无需依赖FindObjectOfType或单例。 - 基于 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();
}
}
这做了三件事:
- 将 ScriptableObject 加入注入队列,以便其自身的
[Inject]字段得到解析。 - 将其绑定为单例,使任何声明
[Inject] private T myRef;的类都获得相同的实例。 - 将绑定标记为
.Lazy(),使得该对象不会在容器构建时立即解析。
还有一个用于基于 MonoBehaviour 的系统的 LazySingletonMonoInstaller<T> 变体。
.AsSingle().Lazy() 约定
每个 MCE 绑定都使用 .AsSingle().Lazy()。原因如下:
.AsSingle()— 确保每个容器只有一个实例。这防止了意外的重复管理器。.Lazy()— 将解析推迟到首次使用。由于 MCE 有 20 多个安装器,立即解析会导致初始化顺序问题:一个系统试图使用另一个尚未完成设置的系统。
所有 MCE 安装器
以下是引擎中安装器的完整列表:
核心运行时安装器
| 安装器 | 位置 | 绑定的内容 |
|---|---|---|
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(通过 BattleLauncher) |
SinglePlayerPlayerControlManagerInstaller | Runtime/Battle/PlayerControl/ | 战斗玩家控制管理器 |
SavegameInstaller | Runtime/Saves/ | SavegameManager, ISerializer<string>(通过 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/ | 音频设置管理器 |
RenderingInstaller | Runtime/Rendering/ | RenderingManager |
UIElementsInstaller | Runtime/UI/ | UI 元素引用 |
MapSceneLauncherInstaller | Runtime/UI/Map/ | 地图场景启动器 |
GlobalGridManagerInstaller | Runtime/World/ | GlobalGridManager |
GridInstaller | Runtime/World/ | GridController |
SceneInfoInstaller | Runtime/World/ | SceneInfo |
TileDataInstaller | Runtime/World/ | TileData |
WorldDatabaseInstaller | Runtime/World/ | WorldDatabase |
SDKInstaller | Runtime/SDK/ | IMonsterCaptureEngine(公开 SDK 入口) |
在线层还多了一个,OnlineBattleInstaller(MCE.Online.Runtime,位于 Online/Runtime/Battle/ 下)。Foundation 的 Events/ 文件夹(EventChannel ScriptableObject)不需要安装器。
Foundation 安装器
| 安装器 | 位置 | 绑定的内容 |
|---|---|---|
AudioInstaller | Foundation/Runtime/Audio/ | IAudioManager |
LocalizationInstaller | Foundation/Runtime/Localization/ | ILocalizer |
SceneManagerInstaller | Foundation/Runtime/SceneManagement/ | ISceneManager |
ConfigurationManagerInstaller | Foundation/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() 而不是字段注入:
- 显式依赖——在一个方法签名中即可看到类所需的一切。
- 不可变性——将注入的引用存储在
private readonly字段中;它们不会被意外重新赋值。 - 可测试性——您可以在单元测试中直接调用
Construct()并传入模拟对象。
模式 2:[Inject] 字段注入(ScriptableObjects)
通过 QueueForInject 处理的 ScriptableObject 使用字段注入,因为它们没有 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 上注册
- 打开
Assets/Resources/ProjectContext.prefab。 - 选择
ProjectContext组件。 - 将您的安装器资源添加到
ScriptableObjectInstallers列表中。
步骤 4:在类中注入
public class MyConsumer : OpenMonBehaviour<MyConsumer>
{
[Inject]
public void Construct(MySystem mySystem, IMyService myService)
{
// 可以开始使用
}
}
常见陷阱
| 问题 | 原因 | 解决方案 |
|---|---|---|
ZenjectException: Unable to resolve | 绑定缺失或安装器未添加到 ProjectContext | 检查安装器是否已添加到 ProjectContext 且类型完全匹配 |
[Inject] 字段上的 NullReferenceException | 未为 ScriptableObject 调用 QueueForInject | 在 .Bind() 调用前添加 Container.QueueForInject(reference) |
| 对象注入过早 | 系统在 Awake() 中使用注入的依赖 | 将逻辑移到 Construct() 或 Start();Zenject 在 Awake() 之后注入 |
| 重复绑定错误 | 同一类型在两个安装器中被绑定 | 使用 .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]。