跳到主要内容

Dependency Injection (Zenject)

仅限 Source 和 Studio 版

此页面记录的内部引擎代码仅供 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();
}
}

这做了三件事:

  1. 将 ScriptableObject 加入注入队列,以便其自身的 [Inject] 字段得到解析。
  2. 将其绑定为单例,使任何声明 [Inject] private T myRef; 的类都获得相同的实例。
  3. 将绑定标记为 .Lazy(),使得该对象不会在容器构建时立即解析。

还有一个用于基于 MonoBehaviour 的系统的 LazySingletonMonoInstaller<T> 变体。

.AsSingle().Lazy() 约定​

每个 MCE 绑定都使用 .AsSingle().Lazy()。原因如下:

  • .AsSingle() — 确保每个容器只有一个实例。这防止了意外的重复管理器。
  • .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 入口)

在线层还多了一个,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() 而不是字段注入:

  • 显式依赖——在一个方法签名中即可看到类所需的一切。
  • 不可变性——将注入的引用存储在 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 上注册​

  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] 字段上的 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]。