[Unity] Zenject 中文文档 Version 9.2.0 [2]

Installers

  通常,每个子系统都有一些相关的绑定集合,因此将这些绑定组合到一个可重用的对象中是有意义的。在Zenject中,这种可重用的对象被称为 “installer”。你可以按以下方式定义一个新的安装器。

public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Bar>().AsSingle();
        Container.BindInterfacesTo<Foo>().AsSingle();
        // etc...
    }
}

  你通过覆盖InstallBindings方法来添加绑定,该方法由安装器被添加到的任何Context(通常是SceneContext)调用。MonoInstaller是一个MonoBehaviour,所以你可以通过将其附加到一个GameObject来添加FooInstaller。因为它是一个GameObject,你也可以向它添加公共成员,以便从Unity检查器中配置你的安装器。这允许你在场景中添加引用,对资产的引用,或者简单的调整数据(关于调整数据的更多信息请看这里)。

  注意,为了使你的安装程序被触发,它必须被附加到SceneContext对象的Installers属性。安装程序是按照SceneContext的顺序安装的(先是可脚本对象安装程序,然后是单体安装程序,然后是预制件安装程序),但是这个顺序通常不重要(因为在安装过程中不应该有任何实例化)。

  在很多情况下,你想让你的安装程序派生自MonoInstaller,这样你就可以有检查员的设置。还有一个基类叫做简单安装器,你可以在不需要它是一个MonoBehaviour的情况下使用。

  你也可以从另一个安装程序中调用一个安装程序。比如说:

public class BarInstaller : Installer<BarInstaller>
{
    public override void InstallBindings()
    {
        ...
    }
}

public class FooInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        BarInstaller.Install(Container);
    }
}

  注意在这种情况下,BarInstaller是Installer<>类型(注意通用参数),而不是MonoInstaller,这就是为什么我们可以简单地调用BarInstaller.Install(Container)而不要求BarInstaller已经被添加到我们的场景中。任何对BarInstaller.Install的调用将立即实例化一个临时的BarInstaller实例,然后对其调用InstallBindings。这将对这个安装器所安装的任何安装器重复进行。还要注意的是,当使用Installer<>基类时,我们总是必须传入我们自己作为Installer<>的通用参数。这是必要的,以便Installer<>基类可以定义静态方法BarInstaller.Install。这样的设计也是为了支持运行时参数(如下所述)。

  我们使用installers 而不是为每个场景一次性声明所有的绑定,其中一个主要原因是为了使它们可以重复使用。这对于Installer<>类型的安装器来说不是问题,因为你可以简单地对你想使用的每个场景调用FooInstaller.Install,但是我们如何在多个场景中重复使用一个MonoInstaller?

有三种方法可以做到这一点。

  1. 场景中的预制件实例。将MonoInstaller附加到场景中的游戏对象后,你就可以从它那里创建一个预制件。这很好,因为它允许你在不同的场景中分享你在检查器中对MonoInstaller所做的任何配置(如果你想的话,还可以对每个场景进行重写)。在你的场景中添加它之后,你可以把它拖放到上下文的Installers属性中。

  2. Prefabs。你也可以直接把你的Installer prefab从项目标签拖到SceneContext的InstallerPrefabs属性中。注意,在这种情况下,你不能像在你的场景中实例化prefab时那样有每个场景的覆盖,但可以很好地避免场景中的混乱。

  3. Resources文件夹中的Prefabs。你也可以把你的Installer prefab放在Resoures文件夹下,通过使用Resources路径直接从代码中安装它们。关于使用的细节,请看这里

  除了MonoInstaller和Installer<>之外,另一个选择是使用ScriptableObjectInstaller,它有一些独特的优势(尤其是在设置方面)–详情见这里

  当从其他Installer中调用Installer时,通常会希望向其传递参数。关于如何做到这一点的细节,请看这里

使用非MonoBehaviour类

ITickable

  在某些情况下,最好是避免MonoBehaviours的额外重量,而只使用普通的C#类。Zenject允许你更容易地做到这一点,它提供的接口反映了你通常需要使用MonoBehaviour的功能。

  例如,如果你有需要每帧运行的代码,那么你可以实现ITickable接口:

public class Ship : ITickable
{
    public void Tick()
    {
        // Perform per frame tasks
    }
}

  然后,要把它挂在一个Installer中:

Container.Bind<ITickable>().To<Ship>().AsSingle();

  或者如果你不想总是记住你的类实现了哪些接口,你可以使用这里描述的快捷方式

  请注意,所有ITickables的Tick()被调用的顺序也是可配置的,如这里所述。

  还要注意的是,有一些接口ILateTickable和IFixedTickable反映了Unity的LateUpdate和FixedUpdated方法。

IInitializable

  如果你有一些初始化需要发生在一个给定的对象上,你可以在构造函数中包含这段代码。然而,这意味着初始化逻辑将发生在构建对象图的中间,所以它可能不是很理想。

  一个更好的选择是实现IInitializable,然后在Initialize()方法中执行初始化逻辑。

  然后,要把它挂在一个Installer中:

Container.Bind<IInitializable>().To<Foo>().AsSingle();

  或者如果你不想总是记住你的类实现了哪些接口,你可以使用这里描述的快捷方式

  Foo.Initialize方法将在整个对象图构建完成且所有构造函数都被调用之后被调用。

  请注意,初始对象图的构造函数是在Unity的Awake事件中调用的,而IInitializable.Initialize方法是在Unity的Start事件中立即调用。因此,使用IInitializable而不是构造函数更符合Unity自己的建议,它建议使用Awake阶段来设置对象引用,而使用Start阶段来进行更多的初始化逻辑。

  这也可以比使用构造函数或[Inject]方法更好,因为初始化顺序是可定制的,与ITickable类似,在此解释。

public class Ship : IInitializable
{
    public void Initialize()
    {
        // Initialize your object here
    }
}

  IInitializable对于启动时的初始化很有效,但是对于通过工厂动态创建的对象,又该如何处理呢?我在这里指的是什么,请看这一节)。对于这些情况,你很可能想使用一个[Inject]方法或者一个显式的Initialize方法,在对象被创建后被调用。比如说:

public class Foo
{
    [Inject]
    IBar _bar;

    [Inject]
    public void Initialize()
    {
        ...
        _bar.DoStuff();
        ...
    }
}
IDisposable

  如果你有外部资源,你想在应用程序关闭、场景改变或因任何原因销毁上下文对象时将其清理掉,你可以像下面这样将你的类声明为IDisposable:

public class Logger : IInitializable, IDisposable
{
    FileStream _outStream;

    public void Initialize()
    {
        _outStream = File.Open("log.txt", FileMode.Open);
    }

    public void Log(string msg)
    {
        _outStream.WriteLine(msg);
    }

    public void Dispose()
    {
        _outStream.Close();
    }
}

  然后在你的Installer中,你可以包括:

Container.Bind(typeof(Logger), typeof(IInitializable), typeof(IDisposable)).To<Logger>().AsSingle();

  或者你可以使用BindInterfaces的快捷方式:

Container.BindInterfacesAndSelfTo<Logger>().AsSingle();

  这样做是因为当场景改变或你的unity应用程序关闭时,unity事件OnDestroy()会在所有MonoBehaviours上被调用,包括SceneContext类,然后会在所有与IDisposable绑定的对象上触发Dispose()。

  你也可以实现ILateDisposable接口,其工作原理与ILateTickable类似,它将在所有IDisposable对象被触发后被调用。然而,对于大多数情况,如果顺序是个问题,你可能最好设置一个显式的执行顺序来代替。

BindInterfacesTo 和 BindInterfacesAndSelfTo

  如果你最终使用了上述的ITickable、IInitializable和IDisposable接口,你往往会出现这样的代码:

Container.Bind(typeof(Foo), typeof(IInitializable), typeof(IDisposable)).To<Logger>().AsSingle();

  这有时会有点冗长。另外,这并不理想,因为如果我后来决定Foo不需要Tick()或Dispose(),那么我必须让Installer保持同步。

  一个更好的主意可能是,就像这样一直使用接口。

Container.Bind(new[] { typeof(Foo) }.Concat(typeof(Foo).GetInterfaces())).To<Foo>().AsSingle();

  这种模式非常有用,以至于Zenject为它包含了一个自定义的绑定方法。上面的代码相当于:

Container.BindInterfacesAndSelfTo<Foo>().AsSingle();

  现在,我们可以向/从Foo中添加和删除接口,安装程序保持不变。

  在某些情况下,你可能只想绑定接口,而让Foo对其他类隐藏起来。在这种情况下,你可以使用BindInterfacesTo方法来代替:

Container.BindInterfacesTo<Foo>().AsSingle()

在这种情况下,这将扩大到:

Container.Bind(typeof(IInitializable), typeof(IDisposable)).To<Foo>().AsSingle();
使用Unity Inspector来配置设置

  把大部分代码写成普通的C#类而不是MonoBehaviour的一个影响是,你失去了使用检查器对它们进行数据配置的能力。然而,你仍然可以通过使用以下模式在Zenject中利用这一点:

ublic class Foo : ITickable
{
    readonly Settings _settings;

    public Foo(Settings settings)
    {
        _settings = settings;
    }

    public void Tick()
    {
        Debug.Log("Speed: " + _settings.Speed);
    }

    [Serializable]
    public class Settings
    {
        public float Speed;
    }
}

然后,在一个Installer中:

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public Foo.Settings FooSettings;

    public override void InstallBindings()
    {
        Container.BindInstance(FooSettings);
        Container.BindInterfacesTo<Foo>().AsSingle();
    }
}

或者说,等同于:

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public Foo.Settings FooSettings;

    public override void InstallBindings()
    {
        Container.BindInterfacesTo<Foo>().AsSingle().WithArguments(FooSettings);
    }
}

  现在,如果我们运行我们的场景,我们可以改变速度值来实时调整Foo类。

  另一种(可以说是更好的)方法是使用ScriptableObjectInstaller而不是MonoInstaller,它的额外优势是你可以在运行时改变你的设置,并在播放模式停止时让这些改变自动持续下去。详情请看这里

对象图验证

概述

  使用DI框架设置绑定时,通常的工作流程是这样的:

  • 在代码中增加一些绑定的数量
  • 执行你的应用
  • 观察到一堆与DI相关的异常情况
  • 修改你的绑定来解决这个问题
  • 重复

  这对小项目来说是可行的,但随着你的项目的复杂性增加,这往往是一个乏味的过程。如果你的应用程序的启动时间特别糟糕,或者当异常只发生在工厂的运行时间的不同点时,问题就会变得更糟。如果有一些工具能够分析你的对象图,并准确地告诉你所有缺失的绑定在哪里,而不需要花费成本来启动你的整个应用程序,那就更好了。

  你可以在Zenject中通过执行菜单项Edit -> Zenject -> Validate Current Scene(验证当前场景)或在你想验证的场景打开时简单地按下SHIFT+ALT+V来进行验证。这将执行当前场景的所有安装程序,其结果是一个完全绑定的容器。然后,它将遍历对象图,并验证是否能找到所有的绑定(实际上并没有实例化任何绑定)。换句话说,它执行了正常启动程序的 “dry run”。在引擎盖下,这是通过在容器中存储假的对象来代替实际实例化你的类。

  另外,你可以执行菜单项 “Edit ->Zenject-> Validate Then Run”,或者直接点击CTRL+ALT+R。这将验证你所打开的场景,如果验证成功,它将开始播放模式。验证通常是相当快的,所以这可能是一个很好的替代方法,而不是总是点击播放,特别是如果你的游戏有一个昂贵的启动时间。

  请注意,这也将包括工厂和内存池,这特别有帮助,因为这些错误可能在启动后的某个时候才会被发现。

  有几件事情需要注意:

  • 没有实际的逻辑代码被执行 - 只有安装绑定被调用。这意味着,如果你在Installer内有绑定命令以外的逻辑,这些逻辑也将被执行,并可能在运行验证时引起问题(如果该逻辑要求容器返回实际值)。

  • 空值被注入到实际被实例化的依赖关系中,比如安装程序(不管绑定的是什么)。

  你可能想在验证模式下也注入一些类。在这种情况下,你可以用[ZenjectAllowDuringValidation]标记它们。

  还请注意,一些验证行为是可以在zenjectsettings中配置的。

自定义验证

  如果你想添加你自己的验证逻辑,你可以通过让你的一个类继承于IValidatable来实现。这样做之后,只要你的类被绑定在某个安装程序中,它就会在验证期间被实例化,然后它的Validate()方法就会被调用。但是请注意,它的任何依赖关系将被注入为空(除非用 [ZenjectAllowDuringValidation] 属性标记)。

  在Validate方法里面,如果你希望验证失败,你可以抛出异常,或者你可以直接把信息记录到控制台。在自定义验证器中,一个常见的情况是将那些本来不会被验证的类型实例化。通过在验证过程中实例化它们,可以确保它们所有的依赖关系都能被解决。

  例如,如果你创建了一个自定义工厂,使用Container.Instantiate()直接实例化一个类型,那么Foo将不会被验证,所以直到运行时你才会发现它是否缺少一些依赖性。但是你可以通过让你的工厂实现IValidatable并在Validate()方法中调用Container.Instantiate()来解决这个问题。

场景绑定

  在许多情况下,你有许多MonoBehaviours被添加到Unity编辑器中的场景中(即在编辑器时间而不是运行时间),你想让这些MonoBehaviours也被添加到Zenject容器中,以便它们可以被注入到其他类中。

  通常的做法是在你的Installer中添加对这些对象的公共引用,像这样:

public class Foo : MonoBehaviour
{
}

public class GameInstaller : MonoInstaller
{
    public Foo foo;

    public override void InstallBindings()
    {
        Container.BindInstance(foo);
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

public class GameRunner : IInitializable
{
    readonly Foo _foo;

    public GameRunner(Foo foo)
    {
        _foo = foo;
    }

    public void Initialize()
    {
        ...
    }
}

  这样做很好,但在某些情况下,这可能会变得很麻烦。例如,如果你想让艺术家在场景中添加任何数量的敌人对象,并且你也想把所有这些敌人对象添加到Zenject容器中。在这种情况下,你必须手动将每个对象拖到你的一个安装程序的检查器中。这很容易出错,因为很容易忘记一个,或者删除了敌人的游戏对象,但忘记删除Installer的检查器中的空引用,等等。

  另一种方法是像这样使用FromComponentInHierarchy的绑定方法:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>().FromComponentInHierarchy().AsTransient();
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

  现在,只要需要一个Foo类型的依赖,zenject就会在整个场景中搜索任何Foo类型的MonoBehaviours。这与每次你想查找某个依赖关系时使用Unity的FindObjectsOfType方法的功能非常相似。注意,因为这个方法可能是一个非常繁重的操作,你可能想把它标记为AsCached或AsSingle,而不是像这样:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<Foo>().FromComponentInHierarchy().AsCached();
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

  这样一来,你只需要在第一次需要搜索时产生一次性能冲击,而不是每次都注入到任何类中。还要注意的是,在我们预期有多个Foos的情况下,我们可以从FromComponentsInHierarchy(注意是复数)。

  然而,另一种方法是使用ZenjectBinding组件。你可以这样做,在你想自动加入Zenject容器的同一个游戏对象上添加ZenjectBinding MonoBehaviour。

  例如,如果你的场景中有一个Foo类型的MonoBehaviour,你可以直接在它旁边添加ZenjectBinding,然后把Foo组件拖到ZenjectBinding组件的Component属性中。
在这里插入图片描述
然后我们的Installer就变成了:

public class GameInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<IInitializable>().To<GameRunner>().AsSingle();
    }
}

ZenjectBinding组件有以下属性:

  • Bind Type - 这将决定使用什么 “合同类型”。它可以被设置为以下任何一个值:
  1. Self
    这相当于我们做的第一个例子:
Container.Bind<Foo>().FromInstance(_foo);

或者说,等同于:

Container.BindInstance(_foo);

  所以如果我们复制这个游戏对象来拥有多个带有Foo的游戏对象(以及ZenjectBinding),它们都将以这种方式被绑定到容器上。所以在这样做之后,我们必须改变上面的GameRunner来接受List,否则我们会得到Zenject的异常(关于列表绑定的信息请看这里)。

  1. AllInterfaces
      该绑定类型相当于以下内容:
Container.BindInterfacesTo(_foo.GetType()).FromInstance(_foo);

  但是请注意,在这种情况下,GameRunner必须在其构造函数中询问IFoo类型。如果我们让GameRunner询问Foo类型,那么Zenject就会出现异常,因为BindInterfaces方法只绑定了接口,而不是具体类型。如果你也想要具体的类型,那么你可以使用。

  1. AllInterfacesAndSelf
      该绑定类型相当于以下内容:
Container.BindInterfacesAndSelfTo(_foo.GetType()).FromInstance(_foo);

  这与AllInterfaces相同,只是我们可以使用Foo类型直接访问Foo,而不需要一个接口。

  1. BaseType
      该绑定类型相当于以下内容:
Container.Bind(_foo.GetType().BaseType()).FromInstance(_foo)
  • Identifier - 这个值在大多数情况下可以留空。它将决定用什么作为绑定的标识符。例如,当设置为 "Foo1 "时,它相当于做了以下事情:
Container.BindInstance(_foo).WithId("Foo1");
  • Use Scene Context - 这是可选的,但在你想将GameObjectContext中的依赖关系绑定到SceneContext的情况下很有用。你也可以把SceneContext适当地拖到Context上,但这个标志更容易一些。
    `
  • Context - 这是完全可选的,在大多数情况下应该不设置。这将决定将绑定应用到哪个上下文。如果不设置,它将使用游戏对象所在的任何上下文。在大多数情况下,这将是SceneContext,但是如果它在一个GameObjectContext中,它将被绑定到GameObjectContext容器中。这个字段的一个重要用例是允许将SceneContext拖入这个字段,用于组件在GameObjectContext中的情况。这允许你把这个MonoBehaviour当作GameObjectContext给定的整个子容器的一个Facade。

一般准则/建议/陷阱/技巧和窍门

  • 如果你希望你的对象被注入依赖关系,请不要使用GameObject.Instantiate
    • 如果你想在运行时实例化一个prefab并自动注入任何MonoBehaviour,我们建议使用一个工厂。你也可以通过调用InstantiatePrefab方法直接使用DiContainer来实例化预制件。使用这些方法而不是GameObject.Instantiate将确保任何标有[Inject]属性的字段被正确填写,并且预制体中的所有[Inject]方法都被调用。
       
  • DI的最佳做法是只在组合根 "层 "中引用容器。
    • 请注意,工厂是这一层的一部分,容器可以在那里被引用(这对于在运行时创建对象是必要的)。关于这方面的更多细节,请看这里
       
  • 不要对动态创建的对象使用IInitializable、ITickable和IDisposable。
    • 类型为IInitializable的对象只被初始化一次–在Unity的启动阶段。如果你通过工厂创建一个对象,并且它派生自IInitializable,Initialize()方法将不会被调用。在这种情况下,你应该使用[Inject]方法或者在调用Create后自己明确地调用Initialize()。
    • 这同样适用于ITickable和IDisposable。除非它们是启动时创建的原始对象图的一部分,否则从这些东西派生出来什么也做不了。
    • 如果你有动态创建的对象有一个Update()方法,通常最好是手动调用这些对象的Update(),而且通常有一个更高级别的类似管理器的类,从那里做这个是有意义的。然而,如果你喜欢使用ITickable来处理动态对象,你可以声明对TickableManager的依赖,并明确地添加/删除它。
       
  • 使用多个构造函数
    • 你可以有多个构造函数,但你必须用[Inject]属性标记其中一个,以便Zenject知道该使用哪一个。如果你有多个构造函数,但没有一个标记为[Inject],那么Zenject会猜测你要用的构造函数是参数最少的那个。
       
  • Lazily 实例化的对象和对象图
    • Zenject不会立即将你在安装程序中设置的绑定所定义的每个对象实例化。它只实例化那些被标记为NonLazy的绑定。所有其他的绑定只有在需要时才会被实例化。所有NonLazy对象以及它们的所有依赖关系构成了应用程序的 “初始对象图”。注意,这自动包括所有实现IInitializable、ITickable、IDisposable等的类型。因此,如果你有一个绑定没有被创建,因为初始对象图中没有任何东西引用它,那么你可以通过添加NonLazy到你的绑定中来明确这一点
       
  • 限制绑定命令的使用,只限于 “组成根”。
    • 换句话说,在Install阶段完成后,不要对Container.Bind、Container.Rebind或Container.Unbind进行调用。这一点很重要,因为在安装完成后,你的应用程序的初始对象图会立即被构建,并且需要访问全套的绑定。
       
  • 事情发生的顺序是错误的,比如注入发生得太晚,或者Initialize()事件没有在正确的时间调用,等等。
    • 这可能是因为Zenject的ProjectKernel或SceneKernel或SceneContext类的 "脚本执行顺序 "不正确。这些类应该总是具有最早或接近最早的执行顺序。这应该已经被默认设置了(因为这个设置包含在这些类的cs.meta文件中)。但是,如果你自己编译Zenject或者有一个独特的配置,你可能想确认一下,你可以通过进入 “Edit -> Project Settings -> Script Execution Order”,确认这些类在顶部,在默认时间之前。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值