Zenject框架(九)

一般指南/建议/陷阱/提示和技巧

  • 需要注入依赖项的物体不要用GameObject.Instantiate生成
    • 如果想要在运行时实例化预设体并使其身上的Monobehaviour自动被注入,我们推荐使用工厂模式。你也可以使用DiContainer调用实例化预设体的方法直接实例化预设体(详见后续“DiContainer.Instantiate”章节)。使用这些方法而不是GameObject.Instantiate将确保所有使用[Inject]属性标记的的字段被正确注入,以及预设体上使用[Inject]标记的方法已经被调用。
  • DI的最佳实践是仅在组合的根层级上引用容器
    • 请注意,工厂是根层的一部分,并且可以在工厂中引用容器(这在运行时创建对象所必需的)。有关详细信息,请参见后续“使用工厂动态创建对象”章节。
  • 不要将IInitializableITickableIDisposable用于动态创建的对象上
    • IInitializable类型的对象只会在启动时在Unity的Start方法中初始化一次,如果你使用工厂创建IInitializable类型的对象,Initialize()方法不会被调用,这种情况下,你应该在创建对象后使用[Inject]标记的方法明确的调用Initialize()
    • 这同样适用于ITickable and IDisposable。除非它们是启动时创建的原始对象图的一部分,否则它们不会执行任何操作。
    • 如果动态创建的对象包含Update()方法,最好在高级别的类中手动调用Update()方法,比如通常会有的Manager类。对于动态的对象,如果你更喜欢使用ITickable,你也可以在TickManager中声明依赖项然后明确的添加或者删除它们。
  • 使用多个构造函数
    • 你可以使用多个构造函数,但必须使用[Inject]属性修饰其中一个以确保Zenject知道要使用哪一个。如果存在多个构造函数但均没有使用[Inject]标记,zenject将默认使用参数最少的构造函数。
  • 延迟实例化对象和对象图
    • Zenject不会立即实例化所有在安装器中绑定的对象,只会实例化使用[NonLazy]标记的对象。其他的对象只会在需要时生成。 所有的[NonLazy]对象以及它们的依赖项构成了程序的“初始对象图”。请注意,这会自动包含所有实现了IInitializable, ITickable, IDisposable,等的类型。因此,如果您有一个绑定未被创建是因为初始对象图中没有任何内容引用它,您可以通过添加NonLazy到绑定来使其显式化
  • 仅将绑定命令的使用限制为“组合根”。 换句话说,就是在安装阶段完成之后不要调用Container.Bind, Container.Rebind, 和 Container.Unbind,这很重要,因为在安装完成后会立即构建应用程序的初始对象图,并且需要访问完整的绑定集。
  • 事情发生的顺序是错误的,比如注入发生的太迟,或者Initialize()没有在正确的时间被调用等等
    • 这可能是因为Zenject的 ProjectKernel类或SceneKernel类或SceneContext类执行顺序不正确。这几个类应始终最早或接近最早被执行。这应该已经默认设置好的(因为此设置包含在这些类的cs.meta文件中)。但是,如果您需要自己编译Zenject或者有一个需要确认的特殊的配置,您可以通过转到“Edit -> Project Settings -> Script Execution Order”并确认这些类位于顶部。

游戏物体绑定方法(Game Object Bind Methods)

对于创建新游戏对象的绑定(例如FromComponentInNewPrefabFromNewComponentOnNewGameObject等),还有两个额外的绑定方法。

  • WithGameObjectName =用于提供与此绑定关联的新游戏对象的名称。
Container.Bind<Foo>().FromComponentInNewPrefabResource("Some/Path/Foo").WithGameObjectName("Foo1");
Container.Bind<Foo>().FromNewComponentOnNewGameObject().WithGameObjectName("Foo1");
  • UnderTransformGroup(string) =用于放置新游戏对象的transform group的名称。这对于可用于创建很多预制体副本的工厂特别有用,因此让它们自动在场景的层级面板中组合是很好的。
Container.BindFactory<Bullet, Bullet.Factory>()
    .FromComponentInNewPrefab(BulletPrefab)
    .UnderTransformGroup("Bullets");
  • UnderTransform(Transform) = 放置新游戏对象的实际transform
Container.BindFactory<Bullet, Bullet.Factory>()
    .FromComponentInNewPrefab(BulletPrefab)
    .UnderTransform(BulletTransform);
  • UnderTransform(Method) = 提供可用的transform的方法
Container.BindFactory<Foo, Foo.Factory>()
    .FromComponentInNewGameObject()
    .UnderTransform(GetParent);

Transform GetParent(InjectContext context)
{
    if (context.ObjectInstance is Component)
    {
        return ((Component)context.ObjectInstance).transform;
    }

    return null;
}

此示例会自动将Foo游戏对象置于其注入的游戏对象下,除非被注入的对象不是MonoBehaviour,在这种情况下,Foo游戏对象会保留在场景层次面板的根部。

可选的绑定(Optional Binding)

您可以将某些依赖项声明为可选,如下所示:

public class Bar
{
    public Bar(
        [InjectOptional]
        IFoo foo)
    {
        ...
    }
}
...

// You can comment this out and it will still work
Container.Bind<IFoo>().AsSingle();

或者加一个标识符:

public class Bar
{
    public Bar(
        [Inject(Optional = true, Id = "foo1")]
        IFoo foo)
    {
        ...
    }
}

如果未在任何安装器中绑定可选依赖项,则它将被注入为null。
如果依赖关系是基本类型(例如int, float,struct),那么它将被注入其默认值(例如,0用于整数)。
您还可以使用标准C#方式分配显式默认值,例如:

public class Bar
{
    public Bar(int foo = 5)
    {
        ...
    }
}
...

// Can comment this out and 5 will be used instead
Container.BindInstance(1);

另请注意,[InjectOptional]在这种情况下不需要的,因为默认值指明了值。
或者,您可以将基本参数定义为可空,并根据是否提供值执行逻辑,例如:

public class Bar
{
    int _foo;

    public Bar(
        [InjectOptional]
        int? foo)
    {
        if (foo == null)
        {
            // 如果没有指定则使用5
            _foo = 5;
        }
        else
        {
            _foo = foo.Value;
        }
    }
}

...

// Can comment this out and it will use 5 instead
Container.BindInstance(1);

条件绑定(Conditional Bindings)

在许多情况下,您需要限制注入给定依赖项的位置。您可以使用以下语法执行此操作:

Container.Bind<IFoo>().To<Foo1>().AsSingle().WhenInjectedInto<Bar1>();
Container.Bind<IFoo>().To<Foo2>().AsSingle().WhenInjectedInto<Bar2>();

请注意,WhenInjectedInto是以下的简写,下面的语法使用了更通用的When()方法:

Container.Bind<IFoo>().To<Foo>().AsSingle().When(context => context.ObjectType == typeof(Bar));

InjectContext类(在上面被作为context参数进行传递)包含您可以作为条件使用信息,如下:

  • Type ObjectType

列表绑定(List Bindings)

当Zenject找到同一类型的多个绑定时,它会将其解释为列表。因此,在下面的示例代码中,Bar将获得包含新实例Foo1,Foo2,Foo3的列表:

// In an installer somewhere
Container.Bind<IFoo>().To<Foo1>().AsSingle();
Container.Bind<IFoo>().To<Foo2>().AsSingle();
Container.Bind<IFoo>().To<Foo3>().AsSingle();

...

public class Bar
{
    public Bar(List<IFoo> foos)
    {
    }
}

列表的顺序与使用Bind方法添加顺序的顺序相同。唯一的例外是当您使用子容器时,因为在这种情况下,列表将首先由关联的子容器排序,第一组实例从最底层的子容器中获取,然后是父级,然后是祖父级,等等。

使用工程长下文的全局绑定(Global Bindings Using Project Context)

如果有一些依赖项需要在所有的场景中长久保存,该怎么办?在Zenject中,您可以通过向ProjectContext对象添加安装器来完成此操作。

为此,首先需要为ProjectContext创建一个预制体,然后您可以为其添加安装器。您可以通过选择Edit -> Zenject -> Create Project Context菜单项轻松完成此操作。然后,您应该可以在Assets/Resources看到名为“ProjectContext”的新资产。或者,您可以在工程面板中右击某个位置并选择Create -> Zenject -> ProjectContext

如果单击此项,可以看到它与SceneContext的检视面板几乎完全相同。配置此预制体的最简单方法是暂时将其添加到场景中,向其中添加安装器,然后单击“apply”将其保存回预制体,然后再从场景中删除它。除了安装器,您还可以直接将自己的自定义MonoBehaviour类添加到ProjectContext对象。

然后,当您启动任何包含SceneContext的任何场景时,ProjectContext对象都会首先初始化。您在此处添加的所有安装器都将被执行,在安装器中添加的绑定将可用于项目中的所有场景。该ProjectContext游戏对象被设置为DontDestroyOnLoad因此更改场景时也不会被销毁。

另请注意,这只发生一次。如果从第一个场景加载另一个场景,ProjectContext则不会再次调用,并且之前添加的绑定将保留在新场景中。您可以使用与场景安装器相同的方式在项目上下文安装器中声明ITickable/ IInitializable/ IDisposable对象,其结果是IInitializable.Initialize只在每次运行时执行一次,IDisposable.Dispose只在应用程序完全停止时调用一次。

您添加到全局安装器的所有绑定都可用于每个场景中的所有类的原因是因为每个场景中容器都会使用的ProjectContext的容器作为父对象 。有关嵌套容器的更多信息,请参见后续。

ProjectContext是一个放置跨场景保留的对象的非常方便的地方。但是,它对每个场景都是完全全局的这一事实可能导致一些意想不到的行为。例如,这意味着即使您编写了一个使用Zenject框架的简单测试场景,它也会加载ProjectContext,这可能不是您想要。为了解决这些问题,通常最好使用Scene Parenting,因为这种方法允许您根据哪些场景继承相同的公共绑定来选择。有关该方法的更多详细信息,请参见“Scene Parenting Using Contract Names”章节

另请注意,默认情况下,在ProjectContext中实例化的任何游戏对象都默认成为其子级。如果您希望将每个新实例化的对象放置在场景层次面板的的根目录中(但仍标记为DontDestroyOnLoad),则可以通过在ProjectContext的检视面板中取消选中“Parent New Objects Under Context”。

标识符(Identifiers)

如果您需要为相同类型设置不同的绑定,而不仅仅是将其保存在列表中,你可以为绑定添加标识符。例如:

Container.Bind<IFoo>().WithId("foo").To<Foo1>().AsSingle();
Container.Bind<IFoo>().To<Foo2>().AsSingle();

...

public class Bar1
{
    [Inject(Id = "foo")]
    IFoo _foo;
}

public class Bar2
{
    [Inject]
    IFoo _foo;
}

在该例中,Bar1类将会得到Foo1的实例,而Bar2类会得到IFoo绑定的默认版本也就是Foo2的实例。
另请注意,您也可以对构造函数/注入方法参数执行相同的操作:

public class Bar
{
    Foo _foo;

    public Bar(
        [Inject(Id = "foo")] 
        Foo foo)
    {
    }
}

在很多情况下,标识符是字符类型的,但实际上可以是任意类型的,在下面的例子中使用了枚举作为标识符:

enum Cameras
{
    Main,
    Player,
}

Container.Bind<Camera>().WithId(Cameras.Main).FromInstance(MyMainCamera);
Container.Bind<Camera>().WithId(Cameras.Player).FromInstance(MyPlayerCamera);

你也可以使用自定义的类型,只要它们实现Equals运算符即可。

可编辑对象安装器(Scriptable Object Installer)

自定义的安装器除了可以派生自MonoInstaller或 Installer,也可以派生自ScriptableObjectInstaller。这最常用于存储游戏设置。这种方法具有以下优点:

  • 停止运行后,对安装器属性所做的任何更改都将保留。这在运行时调整参数非常有用。对于场景中的其他类型的安装器以及MonoBehaviour,在停止运行后,所有在运行时对属性的修改都将消失。但是,有一个“问题”需要注意:代码中对这些设置的任何更改也将被持久保存(与在MonoInstaller上的设置不同)。因此,如果您使用这种方式,应该将所有设置对象视为只读,以避免这种情况发生。

  • 您可以非常轻松地交换同一安装器的多个实例。例如下面的示例,您可能有一个名为GameSettingsEasyGameSettingsInstaller的实例,以及另一个名为GameSettingsHard的实例,等等。

例子:

  • 打开Unity
  • 右键单击工程面板中的某个位置并选择 Create -> Zenject -> ScriptableObjectInstaller
  • 将其命名为GameSettingsInstaller
  • 再次右键单击同一位置
  • 选择新添加的菜单项 Create -> Installers -> GameSettingsInstaller
  • 按照此处列出的设置方法,您可以将其替换为以下内容:
public class GameSettings : ScriptableObjectInstaller
{
    public Player.Settings Player;
    public SomethingElse.Settings SomethingElse;
    // ... etc.

    public override void InstallBindings()
    {
        Container.BindInstances(Player, SomethingElse, etc.);
    }
}

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

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

    public void Tick()
    {
        _position += Vector3.forward * _settings.Speed;
    }

    [Serializable]
    public class Settings
    {
        public float Speed;
    }
}
  • 现在,您应该能够运行游戏时修改GameSettingsInstaller上的速度值,并保存该更改
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容