一般指南/建议/陷阱/提示和技巧
-
需要注入依赖项的物体不要用
GameObject.Instantiate
生成- 如果想要在运行时实例化预设体并使其身上的Monobehaviour自动被注入,我们推荐使用工厂模式。你也可以使用DiContainer调用实例化预设体的方法直接实例化预设体(详见后续“DiContainer.Instantiate”章节)。使用这些方法而不是
GameObject.Instantiate
将确保所有使用[Inject]属性标记的的字段被正确注入,以及预设体上使用[Inject]标记的方法已经被调用。
- 如果想要在运行时实例化预设体并使其身上的Monobehaviour自动被注入,我们推荐使用工厂模式。你也可以使用DiContainer调用实例化预设体的方法直接实例化预设体(详见后续“DiContainer.Instantiate”章节)。使用这些方法而不是
-
DI的最佳实践是仅在组合的根层级上引用容器
- 请注意,工厂是根层的一部分,并且可以在工厂中引用容器(这在运行时创建对象所必需的)。有关详细信息,请参见后续“使用工厂动态创建对象”章节。
-
不要将IInitializable,ITickable和IDisposable用于动态创建的对象上
-
IInitializable
类型的对象只会在启动时在Unity的Start方法中初始化一次,如果你使用工厂创建IInitializable
类型的对象,Initialize()
方法不会被调用,这种情况下,你应该在创建对象后使用[Inject]标记的方法明确的调用Initialize()
。 - 这同样适用于
ITickable
andIDisposable
。除非它们是启动时创建的原始对象图的一部分,否则它们不会执行任何操作。 - 如果动态创建的对象包含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”并确认这些类位于顶部。
- 这可能是因为Zenject的
游戏物体绑定方法(Game Object Bind Methods)
对于创建新游戏对象的绑定(例如FromComponentInNewPrefab
,FromNewComponentOnNewGameObject
等),还有两个额外的绑定方法。
- 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上的设置不同)。因此,如果您使用这种方式,应该将所有设置对象视为只读,以避免这种情况发生。
您可以非常轻松地交换同一安装器的多个实例。例如下面的示例,您可能有一个名为
GameSettingsEasy
的GameSettingsInstaller
的实例,以及另一个名为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
上的速度值,并保存该更改