欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程

程序员文章站 2022-06-01 13:09:27
...

Scriptable Object Installer

如何实现自己的 Installer ?以前的文章中介绍过从 MonoInstaller 派生或者 Installer 派生,今天我们讲解一个新的实现方式,从 ScriptableObjectInstaller 类进行派生。这种方法通常用于存储游戏设置。

下面我们将陈述使用ScriptableObjectInstaller的优势:

  • 在运行模式调整的参数,结束运行后,都可以被保留下来。这对我们调参十分有用,其他的Installer或者场景中挂载的Mono脚本,一旦结束运行,在运行时调整的参数全部会被强制恢复到运行之前(对Inspector面板进行的操作全部撤销),经过千辛万苦才调试出的参数丢失是谁都不愿意面对的。但是使用ScriptableObjectInstaller 也需要慎重,一旦数据改变就永久性的改变了,并没有还原点。因此,如果走这条路线,应该将所有的设置对象都视为 readonly 。(个人认为:这种方式非常适合游戏开发初期进行人物移动数值,伤害数值、关卡参数的调试)
  • 使用这种方式可以快速的更换实例,加载另一个运行参数模板,比如将游戏分为 简单难度、中等难度、地狱难度,只需要切换参数模板即可。

案例:

  • 打开Unity
  • 在Project窗口的某个位置右击 Create -> Zenject -> ScriptableObjectInstaller
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  • 将其命名为 GameSettingsInstaller(自己使用时可以自定义命名)
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  • 在Project窗口的某个位置右击 Create -> Installers -> GameSettingsInstaller
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  • 将下列脚本替换到GameSettingsInstaller中
public class GameSettings : ScriptableObjectInstaller
{
    public Player.Settings Player;
    public override void InstallBindings()
    {
        Container.BindInstances(Player);
        Container.Bind<ITickable>().To<Player>().AsSingle().NonLazy();
    }
    
}

public class Player : ITickable
{
    readonly Settings _settings;
    Vector3 _position;

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

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

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

这里进行一个简单的场景测试:

  1. 查看我们创建的设置实例
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
    2.将实例拖拽到SceneContext对应的位置
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  2. 现在,您应该能够运行游戏并在运行时调整GameSettingsInstaller资产上的Speed值,并将该更改永久保存,我们实时打印speed的数值,并在运行模式下修改。
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程

详解Installers 参数

当在一个 Installer 中调用其他的 Installer 时,是可以传递参数的。
使用non-MonoBehaviour Installer的案例:

public class FooInstaller : Installer<string, FooInstaller>
{
    string _value;

    public FooInstaller(string value)
    {
        _value = value;
    }

    public override void InstallBindings()
    {
        ...

        Container.BindInstance(_value).WhenInjectedInto<Foo>();
    }
}

public class MainInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        FooInstaller.Install(Container, "asdf");
    }
}

使用 MonoInstaller 预制体的案例:

public class FooInstaller : MonoInstaller<string, FooInstaller>
{
    string _value;

    // 注意在这种情况下我们无法使用构造函数,使用方法注入
    [Inject]
    public void Construct(string value)
    {
        _value = value;
    }

    public override void InstallBindings()
    {
        ...
        Container.BindInstance(_value).WhenInjectedInto<Foo>();
    }
}

public class MainInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        //这个案例能起作用前提是在“Resources/My/Custom/ResourcePath.prefab”路径下具有一个Prefab并且挂载了FooInstaller 
        FooInstaller.InstallFromResource("My/Custom/ResourcePath", Container, "asdf")
        // 如果未提供资源路径,则假定它存在于“'Resources/Installers/FooInstaller'”资源路径中
        // 可以这么写:
        // FooInstaller.InstallFromResource(Container, "asdf");
    }
}

ScriptableObjectInstaller与MonoInstaller的工作原理相同。

在Untiy外部或DLL中使用Zenject

如果您希望将一些代码Build成DLL,但仍然在Unity中使用他们,那么依旧可以在Installers中为这些类添加绑定,唯一的限定是必须使用构造函数注入。如果非要使用其他方式注入(例如成员变量注入和方法注入),需要将项目的引用添加到Zenject-Usage.dll中,可以在Zenject\Source\Usage directory 目录中找到它,该目录还包含ITickable,IInitializable等标准接口,因此您也可以使用它们。
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
您可以下载 Zenject-NonUnity.zip,在非unity环境中使用Zenject。下载地址

最后,如果您尝试使用Unity生成的解决方案在Unity外部运行单元测试,则Zenject代码在尝试访问Unity API时可能会遇到运行时错误。 您可以通过在生成的解决方案中添加ZEN_TESTS_OUTSIDE_UNITY的定义来禁用此行为。

Zenject Settings

通过ProjectContext的settings属性可以设置Zenject的许多默认行为。这包括:
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
**Ensure Deterministic Destruction Order On Application Quit ** :当设置为True时,这表明在关闭应用程序时,所有的游戏物体和IDisposables 都以可预测的顺序进行销毁。默认设置为false,因为启用可能带来一些不良影响。
**Display Warning When Resolving During Install **:这个值用来控制 Install 阶段(Resolve过程或Instantiate 过程)发生的警告是否输出到控制台上。

Zenject Warning: It is bad practice to call Inject/Resolve/Instantiate before all the Installers have completed!  This is important to ensure that all bindings have properly been installed in case they are needed when injecting/instantiating/resolving.  Detected when operating on type 'Foo'.

如果您经常遇到此警告,并且知道所执行操作的含义,则可以将此值设置为false来禁止显示。
Validation Root Resolve Method:当对目标场景进行验证时,DiContainer将进行“空运行”并假装实例化场景中 Installers 定义的整个object graph。但是,默认情况下,它将仅验证object graph的“root”-即“ NonLazy”绑定或注入到“ NonLazy”绑定中的绑定。您可以选择将此行为更改为“All”,这将验证所有绑定,即使当前未使用的绑定也会被验证到。
Validation Error Response此值控制zenject遇到验证错误时触发的行为。 可以将其设置为“ Log”或“ Throw”。 此处的区别在于,当设置为“ Log”时,每次运行验证时都会打印多个验证错误,而如果设置为“ Throw”,则只会将第一个验证错误输出到控制台。 取消设置时,默认值为“ Log”。 如果在单元测试中运行验证,“Throw”有时也很有用。

也可以通过更改DiContainer.Settings属性,在每个DiContainer的基础上配置这些设置。 更改此属性将影响给定的Container以及该Container的subContainer。

Update / Initialization 自定义执行顺序

在大多数项目中,尤其是小项目中,类 Update / Initialization 的顺序无关紧要。在大项目中 类 Update / Initialization 的顺序可能十分关键。在unity中这个问题格外明显,因为我们很难预测Start(),Awake()或Update()方法的调用顺序。Unity中没有简单的方法进行控制(Edit -> Project Settings -> Script Execution Order 能起到一定作用,但这并不简便)。
因此推荐您使用Zenject中的解决方案,默认情况下,ITickable和IInitializables根据添加顺序被依次调用,针对于 update /initialization 顺序很重要的情况,还可以通过代码来指定优先级:

public class AsteroidsInstaller : MonoInstaller
{
    ...
    void InitExecutionOrder()
    {
        // 在大多数情况下,您不必担心执行顺序
        // 如果我们想要确保AsteroidManager.Initialize优先于 GameController.Initialize(Tick同理)执行
        //可编写如下代码
        Container.BindExecutionOrder<AsteroidManager>(-10);
        Container.BindExecutionOrder<GameController>(-20);
    }
    ...
    public override void InstallBindings()
    {
        ...
        InitExecutionOrder();
        ...
    }
}

明确了调用执行的顺序,就不会发生不可预知的依赖性错误。
请注意,这里给BindExecutionOrder的值将应用于ITickable / IInitializable和IDisposable(IDisposable的执行顺序相反)。
还可以根据具体的接口定制顺序:

Container.BindInitializableExecutionOrder<Foo>(-10);
Container.BindInitializableExecutionOrder<Bar>(-20);

Container.BindTickableExecutionOrder<Foo>(10);
Container.BindTickableExecutionOrder<Bar>(-80);

未分配优先级的所有ITickable,IInitializable或IDisposable将自动被赋予 0 优先级。 这使您可以在未指定的类之前或之后执行具有显式优先级的类。 例如,上面的代码将导致Foo.Initialize在Bar.Initialize之前被调用。

Zenject 生命周期详解

使用了Zenject的场景在每个阶段究竟发生了什么改变,随后将进行详细的讲解,这将有助于进一步了解Zenject的工作原理。
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程

Unity Awake() 阶段:

  • SceneContext.Awake() 方法被调用。这是在运行你的场景时,默认要做的第一件事。
  • Project Context 初始化. (注意每次运行程序只发生一次,如果你之前的场景加载过程中已经完成了这一步,新的场景加载时则跳过此步骤)
  1. ProjectContext prefab上所有的的可注入MonoBehaviour都通过DiContainer.QueueForInject传递到container
  2. ProjectContext遍历通过Unity Inspector添加到其Prefab中的所有 Installers ,运行他们上面的注入,并调用每个Installer 的 InstallBindings() 。每个Installer 都会在DiContainer上调用一定数量的Bind方法。
  3. ProjectContext构造所有 non-lazy 根对象,这包括从ITickable / IInitializable或IDisposable派生的任何类,以及添加了NonLazy()绑定的那些类。
  4. 注入通过DiContainer.QueueForInject添加的所有实例
  • Scene Context 初始化
  1. 场景中所有可以注入的Monebehaviours都通过DiContainer.QueueForInject传递到Container中。
  2. SceneContext遍历通过Unity Inspector添加到它的所有Installers ,运行他们上面的注入,并调用每个Installer 的 InstallBindings() 方法。每个Installer 都会在DiContainer上调用一定数量的Bind方法。
  3. SceneContext构造所有 non-lazy 根对象,这包括从ITickable / IInitializable或IDisposable派生的任何类,以及添加了NonLazy()绑定的那些类。
  4. 注入通过DiContainer.QueueForInject添加的所有实例
  • 如果仍有依赖项无法解决,抛出 ZenjectResolveException

Unity Start() 阶段

  • 调用ProjectKernel.Start()方法。这个方法将触发 ProjectContext installers上所有 IInitializable对象(实现了IInitializable接口的对象) 的Initialize()方法。(Initialize()执行顺序参看“Update / Initialization 自定义执行顺序”)
  • 调用SceneKernel.Start()方法.这个方法将触发 SceneContext installers上所有 IInitializable对象(实现了IInitializable接口的对象) 的Initialize()方法。(Initialize()执行顺序参看“Update / Initialization 自定义执行顺序”)
  • 场景中继承了MonoBehaviour的脚本调用他们的Start()方法。

Unity Update()阶段:该顺序同样适用于LateUpdate 和 FixedUpdate

  • 调用ProjectKernel.Update() 方法。这个方法将触发 ProjectContext installers上所有 ITickable 对象(实现了ITickable 接口的对象) 的Tick()方法。(Tick() 执行顺序参看“Update / Initialization 自定义执行顺序”)
  • 调用SceneKernel.Update() 方法。这个方法将触发 SceneContext installers上所有 ITickable 对象(实现了ITickable 接口的对象) 的Tick()方法。(Tick() 执行顺序参看“Update / Initialization 自定义执行顺序”)
  • 场景中继承了MonoBehaviour的脚本调用他们的Update() 方法。

Unity scene 卸载阶段

  • GameObjectContext上所有实现了IDisposable 接口的对象调用 Dispose()方法
  • SceneContext 上所有实现了IDisposable 接口的对象调用 Dispose()方法

应用程序关闭阶段

  • ProjectContext 上所有实现了IDisposable 接口的对象调用 Dispose()方法

场景切换时进行数据注入

很多时候我们需要将参数从一个场景传递到另一个场景。在不使用Zenject的时候有两种解决方案,第一种是创建一个Gameobject,并设置为DontDestroyOnLoad(),让这个游戏物体在所有场景都保持活跃。第二种时使用全局静态类来临时存储数据。

首先我们要知道Zenject提供了一个加载场景的类ZenjectSceneLoader ,其中包含了加载场景的多种方法,我们下面列举出两种。
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程

//第一种 同步加载
         public void LoadScene(
            string sceneName,
            LoadSceneMode loadMode = LoadSceneMode.Single,
            Action<DiContainer> extraBindings = null,
            LoadSceneRelationship containerMode = LoadSceneRelationship.None,
            Action<DiContainer> extraBindingsLate = null)
        {
            PrepareForLoadScene(loadMode, extraBindings, extraBindingsLate, containerMode);
            Assert.That(Application.CanStreamedLevelBeLoaded(sceneName),
                "Unable to load scene '{0}'", sceneName);
            SceneManager.LoadScene(sceneName, loadMode);
        }
//第二种 异步加载场景
            public AsyncOperation LoadSceneAsync(
            string sceneName,
            LoadSceneMode loadMode = LoadSceneMode.Single,
            Action<DiContainer> extraBindings = null,
            LoadSceneRelationship containerMode = LoadSceneRelationship.None,
            Action<DiContainer> extraBindingsLate = null)
        {
            PrepareForLoadScene(loadMode, extraBindings, extraBindingsLate, containerMode);

            Assert.That(Application.CanStreamedLevelBeLoaded(sceneName),
                "Unable to load scene '{0}'", sceneName);

            return SceneManager.LoadSceneAsync(sceneName, loadMode);
        }
           
            

假设您要为下一个场景指定一个“关卡”字符串。

public class LevelHandler : IInitializable
{
    readonly string _startLevel;

    public LevelHandler(
        [InjectOptional]
        string startLevel)
    {
        if (startLevel == null)
        {
            _startLevel = "default_level";
        }
        else
        {
            _startLevel = startLevel;
        }
    }

    public void Initialize()
    {
        ...
        [Load level]
        ...
    }
}

你可以使用以下代码加载指定的包含LevelHandler 的场景:

public class Foo
{
    readonly ZenjectSceneLoader _sceneLoader;

    public Foo(ZenjectSceneLoader sceneLoader)
    {
        _sceneLoader = sceneLoader;
    }

    public void AdvanceScene()
    {
        _sceneLoader.LoadScene("NameOfSceneToLoad", LoadSceneMode.Single, (container) =>
            {
                container.BindInstance("custom_level").WhenInjectedInto<LevelHandler>();
            });
    }
}

上述在Lambda表达式中绑定到Container的代码和 新场景中 installer中进行绑定一样有效。
仍然可以直接运行场景,在这种情况下,默认情况下将使用“ default_level”。因为使用了InjectOptional特性。
实现此目的的另一种方法(可能是更简洁的方法)是自定义Installer本身,而不是自定义LevelHandler类。 在这种情况下,我们可以这样编写我们的LevelHandler类(不带[InjectOptional]标志)。

public class LevelHandler : IInitializable
{
    readonly string _startLevel;

    public LevelHandler(string startLevel)
    {
        _startLevel = startLevel;
    }

    public void Initialize()
    {
        ...
        [Load level]
        ...
    }
}

此时在installer中需要这样写:

public class GameInstaller : Installer
{
    [InjectOptional]
    public string LevelName = "default_level";
    ...

    public override void InstallBindings()
    {
        ...
        Container.BindInstance(LevelName).WhenInjectedInto<LevelHandler>();
        ...
    }
}

相比于注入到LevelHandler ,我们可以直接注入到installer 。

public class Foo
{
    readonly ZenjectSceneLoader _sceneLoader;

    public Foo(ZenjectSceneLoader sceneLoader)
    {
        _sceneLoader = sceneLoader;
    }

    public void AdvanceScene()
    {
        _sceneLoader.LoadScene("NameOfSceneToLoad", (container) =>
            {
                container.BindInstance("custom_level").WhenInjectedInto<GameInstaller>();
            });
    }
}

ZenjectSceneLoader类还允许更复杂的场景,例如将场景加载为当前场景的“子级”,这将导致新场景继承当前场景中的所有依赖关系。 但是,通常最好使用场景 的Contract Name。

使用 Contract Names确定场景父子关系

将绑定放到ProjectContext中是一种快速且简单的方式,绑定的依赖所有的场景都可以使用。但是如果我们希望数据仅注入到我们指定的场景而不是所有场景呢,ProjectContext技术还适用吗?下面我们将介绍一种被称为 ‘Scene Contract Names’的功能。
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
例如,假设我们正在做一个太空飞船游戏,我们想要创建一个场景作为环境(涉及行星,小行星,恒星等),并且我们想要创建另一个场景来表示飞船所在的位置。我们还希望飞船场景中的所有类都能够引用环境场景中声明的绑定。另外,我们希望能够定义飞船场景和环境场景的多个不同版本。
这首先要用到Unity多场景编辑的知识,将我们的 环境场景 和我们的 船舶场景 拖到“Scene Hierarchy”选项卡中。然后,我们将在环境场景中选择SceneContext并添加一个“Contract Name”。 我们称之为“Environment”。 然后,我们现在要做的就是在舰船场景中选择SceneContext并将其“Parent Contract Name”设置为相同的值(“Environment”)。 现在,如果我们按play键,则船舶场景中的所有类都可以访问环境场景中声明的绑定。
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
我们在这里使用 字段 而不是使用场景名称的原因是为了支持将各种环境场景的更换。 在此示例中,我们可以定义几个不同的环境,全部使用相同的合同名称“Environment”,这样,我们只需将我们想要的场景拖入场景层次结构然后进行播放,就可以轻松地将它们与不同的船舶场景混合并匹配。
之所以称为’Contract Name’ ,是因为预期所有环境场景和飞船场景都将遵循某个“Contract ”。例如,舰船场景可能要求无论加载哪个环境场景,“ AsteroidManager”都有一个绑定,其中包含舰船必须避免的小行星列表。
请注意,您无需同时加载环境场景和船舶场景,此功能就可以工作。例如,您可能想要在环境中嵌入一个菜单,以允许用户在开始之前选择他们的船。因此,您可以创建一个菜单场景并将其加载到环境场景之后。然后,一旦用户选择了自己的船,就可以通过调用统一方法SceneManager.LoadScene(确保使用LoadSceneMode.Additive)来加载关联的船场景。
Unity当前没有内置的方法来保存和还原多场景设置。为此,我们使用了一个简单的编辑器脚本,有兴趣的话可以在这里找到。

ZenAutoInjecter

工厂部分的介绍我们了解到,需要动态创建的任何对象都必须通过zenject创建才能被注入。 您不能简单地执行GameObject.Instantiate(prefab)或调用 new Foo()。
但是,在使用其他第三方库时可能出现错误。 例如,某些网络库通过自动实例化 prefabs 跨客户端同步状态。 在这些情况下,仍然希望执行zenject注入。
因此,为解决这些情况,Zenject添加了一个辅助工具MonoBehaviour,称为ZenAutoInjecter。 如果将此MonoBehaviour添加到prefabs中,则可以调用GameObject.Instantiate并自动进行注入。
Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
’Container Source’

  • SearchInHiearchy :这将通过搜索实例化Prefab所在的 scene hierarchy 来找到要使用的Container。 因此,如果Prefab在GameObjectContext下实例化,它将使用与GameObjectContext关联的Container。 如果在DontDestroyOnLoad对象下实例化它,则它将使用ProjectContext容器。 否则,将使用SceneContextContainer。(笔者:自动搜索,根据一定的规则匹配Container,将Container中的数据注入到我们的Prefab中)
  • SceneContext :不用搜索指定使用当前场景的SceneContext
  • ProjectContext :不用搜索指定使用ProjectContext

Scene Decorators

除了上文提到的scene parenting,Scene Decorators提供了另一种方法来使用多场景。区别在于,使用Scene Decorators时多场景共享一个Container,所有场景都可以访问其他场景中的绑定。scene parenting中只能Child访问parent 的绑定。
scene decorators也可以视为一种切换场景时注入数据的方式,可以在不修改场景中Installer的情况下,将行为添加到另一个场景。
通常,当您要根据某些条件为给定场景自定义不同的行为时,可以在MonoInstallers上使用布尔值或枚举属性,然后根据设置的值将其用于添加不同的绑定。 但是,Scene Decorators方法更加便捷,因为它更改主场景。
例如,假设我们要在主场景中添加一些特殊的键盘快捷键来进行测试。使用Scene Decorators的方法如下所示:

  1. 打开主场景
  2. 添加新场景
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  3. 拖动场景使其位于主场景上
  4. 在新场景上右击添加 Zenject -> Decorator Context
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  5. “Decorated Contract Name”字段设置为“Main”
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  6. 在主场景中选择SceneContext并添加具有相同的contract name (“ Main”)
    Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程
  7. 使用以下代码创建一个新的C#脚本,然后将此MonoBehaviour作为gameObject添加到您的装饰器场景中,然后将其拖动到SceneDecoratorContext的Installers属性中
public class ExampleDecoratorInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<ITickable>().To<TestHotKeysAdder>().AsSingle();
    }
}

public class TestHotKeysAdder : ITickable
{
    public void Tick()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("Hotkey triggered!");
        }
    }
}

Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程

注意事项:

  1. 如果运行场景,则除了decorator installer安装程序中的新增功能外,它现在的行为应与主场景完全相同。 另请注意,虽然此处未显示,但两个场景都可以访问彼此的绑定,就像所有内容都在同一场景中一样。
  2. 验证命令(CTRL + ALT + V)可用于快速验证不同的多场景设置。
  3. 装饰器场景必须在装饰场景之前加载。
  4. Unity当前没有内置的方法来保存和还原多场景设置。 为此,我们使用了一个简单的编辑器脚本,有兴趣的话可以在这里找到。
  5. 最后,如果您想节省一些时间,可以为上面使用的 contract name 添加默认场景

Just-In-Time Resolving Using LazyInject<>

在一些场景中,有些依赖项的创建发生在启动之后。此时可以使用LazyInject<>构造。
例如:

public class Foo
{
    public void Run()
    {
        ...
    }
}

public class Bar
{
    Foo _foo;

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

    public void Run()
    {
        _foo.Run();
    }
}

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

通过上面的代码我们可以知道我们只有在使用Foo(调用Bar.Run方法时)的时候才想创建它,但实际是哪怕我们没有调用Bar.Run,我们现在仍然会创建一个Foo,可以通过下面的代码进行优化:

public class Bar
{
    LazyInject<Foo> _foo;
    public Bar(LazyInject<Foo> foo)
    {
        _foo = foo;
    }
    public void Run()
    {
        _foo.Value.Run();
    }
}

现在,通过使用LazyInject <>,直到第一次调用Bar.Run时才创建Foo类。 之后,它将使用Foo的相同实例。
请注意,两种情况下的 installer 均相同。 只需将其包装在LazyInject <>中,就可以使任何注入的依赖项变成懒汉模式。

开放的泛型

Zenject还具有一项功能,可让您在注入过程中使用泛型。 例如:

public class Bar<T>
{
    public int Value
    {
        get; set;
    }
}

public class Foo
{
    public Foo(Bar<int> bar)
    {
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind(typeof(Bar<>)).AsSingle();
        Container.Bind<Foo>().AsSingle().NonLazy();
    }
}

注意:在使用开放泛型参数的时候,必须使用Band方法的non generic 版本,如示例所示,绑定开放的泛型类型时,它将注入匹配的参数/字段/属性。 您也可以像这样将一个开放的通用类型绑定到另一种开放的通用类型:

public interface IBar<T>
{
    int Value
    {
        get; set;
    }
}

public class Bar<T> : IBar<T>
{
    public int Value
    {
        get; set;
    }
}

public class Foo
{
    public Foo(IBar<int> bar)
    {
    }
}

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.Bind(typeof(IBar<>)).To(typeof(Bar<>)).AsSingle();
        Container.Bind<Foo>().AsSingle().NonLazy();
    }
}

APP销毁顺序

如果绑定的类实现了IDispose接口,通过execution order就可以设置他们的调用顺序,但是这对于场景中的Gameobject并不适用。Unity自身有一个“script execution order”的概念,但是这个顺寻不会影响销毁的顺序,root的物体可以按照任意的顺序销毁,这其中就包括了SceneContext。使销毁顺序可控一些的行为是将所有的Gameobject都放在SenceContext的下面,这可能带来一些帮助,因为它至少保证了绑定了的IDisposables对象优先于场景中的其他物体被销毁。还可以在SceneContext的"Parent New Objects Under Scene Context"中进行勾选,来保证所有动态创建的对象都是SceneContext的子物体。

Unity高性能依赖注入框架Extenject(Zenject)-----进阶教程

有时在销毁顺序方面可能会出现的另一个问题是场景的卸载顺序以及DontDestroyOnLoad对象(包括ProjectContext)的卸载顺序。不幸的是,在这种情况下,Unity也没法保证确定性的销毁顺序,并且您会发现有时退出应用程序时,DontDestroyOnLoad对象实际上在场景之前被销毁,或者您会发现首先加载的场景也是 首先被销毁,这通常不是您想要的。
如果销毁顺序对您来说十分重要,那么可以考虑将ZenjectSetting中“Ensure Deterministic Destruction Order On Application Quit”设置为True,此时OnApplicationQuit事件期间使用比默认情况下更统一的顺序销毁所有场景。(以加载的相反顺序销毁,最后销毁DontDestroyOnLoad对象)
此设置默认情况下未设置为true的原因是,这可能会导致Android崩溃。

UniRx 联合使用

UniRx是一个将Reactive Extensions带到Unity的库。 通过将类之间的某种通信视为数据的“流”,可以大大简化您的代码。
默认情况下,UniRx的Zenject是禁用的。启用方式:必须将定义ZEN_SIGNALS_ADD_UNIRX添加到项目, Edit -> Project Settings -> Player,将ZEN_SIGNALS_ADD_UNIRX添加进“Scripting Define Symbols"
对于zenject版本7.0.0,您还必须将Zenject.asmdef文件更改为以下内容:

{
    "name": "Zenject",
    "references": [
        "UniRx"
    ]
}

启用ZEN_SIGNALS_ADD_UNIRX后,您可以按照Signal文档中的说明通过UniRx流观察zenject信号,还可以观察TickableManager类上的zenject事件,例如Tick,LateTick和FixedTick等。 一种示例用法是确保某些事件每帧最多只能处理一次:

public class User
{
    public string Username;
}

public class UserManager
{
    readonly List<User> _users = new List<User>();
    readonly Subject<User> _userAddedStream = new Subject<User>();

    public IReadOnlyList<User> Users
    {
        get { return _users; }
    }

    public IObservableRx<User> UserAddedStream
    {
        get { return _userAddedStream; }
    }

    public void AddUser(User user)
    {
        _users.Add(user);
        _userAddedStream.OnNext(user);
    }
}

public class UserDisplayWindow : IInitializable, IDisposable
{
    readonly TickableManager _tickManager;
    readonly CompositeDisposable _disposables = new CompositeDisposable();
    readonly UserManager _userManager;

    public UserDisplayWindow(
        UserManager userManager,
        TickableManager tickManager)
    {
        _tickManager = tickManager;
        _userManager = userManager;
    }

    public void Initialize()
    {
        _userManager.UserAddedStream.Sample(_tickManager.TickStream)
            .Subscribe(x => SortView()).AddTo(_disposables);
    }

    void SortView()
    {
        // Sort the displayed user list
    }

    public void Dispose()
    {
        _disposables.Dispose();
    }
}

使用Zenject创建编辑器窗口

如果您需要添加自己的Unity插件,并且要创建自己的EditorWindow派生类,则可以考虑使用Zenject来帮助管理此代码。 让我们来看一个如何执行此操作的示例:

  1. 创建窗口 Create -> Zenject -> Editor Window 吗,命名为TimerWindow
  2. 打开窗口 Window -> TimerWindow
  3. 用下面的代码进行替换
public class TimerWindow : ZenjectEditorWindow
{
    TimerController.State _timerState = new TimerController.State();

    [MenuItem("Window/TimerWindow")]
    public static TimerWindow GetOrCreateWindow()
    {
        var window = EditorWindow.GetWindow<TimerWindow>();
        window.titleContent = new GUIContent("TimerWindow");
        return window;
    }

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

class TimerController : IGuiRenderable, ITickable, IInitializable
{
    readonly State _state;

    public TimerController(State state)
    {
        _state = state;
    }

    public void Initialize()
    {
        Debug.Log("TimerController initialized");
    }

    public void GuiRender()
    {
        GUI.Label(new Rect(25, 25, 200, 200), "Tick Count: " + _state.TickCount);

        if (GUI.Button(new Rect(25, 50, 200, 50), "Restart"))
        {
            _state.TickCount = 0;
        }
    }

    public void Tick()
    {
        _state.TickCount++;
    }

    [Serializable]
    public class State
    {
        public int TickCount;
    }
}

在ZenjectEditorWindow的InstallBindings方法中,您可以像在场景中一样添加IInitializable,ITickable和IDisposable绑定。 还有一个名为IGuiRenderable的新接口,您可以使用它通过使用Unity的即时模式gui将内容绘制到窗口。
请注意,每次在Unity中再次编译代码时,都会重新加载编辑器窗口。 再次调用InstallBindings,并从头开始创建所有类。 这意味着您可能存储在成员变量中的任何状态信息都将被重置。 但是,EditorWindow派生类本身中的成员字段已序列化,因此您可以利用此优点使状态在重新编译期间保持不变。 在上面的示例中,通过将其包装在Serializable类中并将其包括在EditorWindow中,可以使当前的Tick计数保持不变。
还有一点需要注意的是,ITickable.Tick方法的触发速率可以根据您所关注的内容而变化。 如果您运行我们的计时器窗口,然后选择Unity以外的其他窗口,您将明白我的意思。 (Tick Count 的增加要慢得多)