安装器(Installer)
通常,每个子系统都有一些相关绑定的集合,因此将这些绑定组合成一个可重用的对象是有意义的。在Zenject中,这个可重用的对象称为“安装器”。您可以按如下方式定义新安装器:
public class FooInstaller : MonoInstaller
{
public override void InstallBindings()
{
Container.Bind<Bar>().AsSingle();
Container.BindInterfacesTo<Foo>().AsSingle();
// 等等...
}
}
您可以通过重写InstallBindings方法来添加绑定,该方法由添加了安装器的上下文(Context)调用(通常是场景上下文(SceneContext))。MonoInstaller是一个MonoBehaviour,因此您可以通过将其附加到游戏对象上来添加FooInstaller。由于它是游戏对象,您还可以向其添加公共成员,以便在Unity检视面板中配置安装器。这允许您在场景中添加引用,引用资源或简单地调整数据(有关调整数据的更多信息,请参见“使用Unity检视面板配置设置”章节)。
请注意,为了触发安装器,必须将其添加到到SceneContext对象的Installers(暂且称为安装器集合)属性中。安装器按SceneContext给定的顺序依次安装(首先是scriptable对象安装器,然后是Mono安装器,然后是预设体安装器),但是这个顺序通常不重要(因为在安装过程中不应该实例化任何内容)。
大部分情况下,安装器最好派生自MonoInstaller,因为这样可以在检视面板中对其设置。还有一个简单的基类Installer
,您可以在不需要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
。它也以这种方式设计以支持运行时参数(如下所述)。
我们在每个场景中使用安装器集合而不是将所有的绑定全部一次声明的主要原因之一是可以重用它们。这对于Installer<>
类型的安装器来说不是问题,因为你可以在需要的场景中像上面的例子一样调用FooInstaller.Install
实现重用,但在多个场景中我们如何重用MonoInstaller类型的安装器呢?
有以下3种方式:
- 在场景中添加预设体实例。将MonoInstaller类型安装器添加到场景中的游戏对象后,您可以创建一个该对象的预制体。这种方式允许您跨场景共享您在MonoInstaller类型安装器的检视面板中完成的任何配置(如果需要,还可以按场景修改配置)。在场景中添加该预设体后,您可以将其拖放到一个上下文(Context)的Installers属性中。
- 预设体。你也可以直接从工程面板中将预设体拖放到场景上下文(SceneContext)的InstallerPrefabs属性中。注意,在这种情况下无法像第一种方式一样可以修改配置,但这种方式可以很好地避免场景中的混乱。
-
Resources文件夹中的预制件。您还可以将安装器预设体放在Resoures文件夹下,并使用Resources路径直接从代码中安装它们。有关用法的详细信息请见“安装器运行时参数(Runtime Parameters For Installers)”章节。
除了MonoInstaller
和Installer<>
之外还可以使用ScriptableObjectInstaller
,这种方式有一些独特的优势(尤其在设置上),详细信息见“(Scriptable Object Installer)”章节。
ITickable
在某些情况下,使用MonoBehaviour会产生额外消耗,因而最好使用c#的普通类。通过zenject提供的接口你可以轻松实现该目的。这些接口镜像了MonoBehaviour的部分功能。
例如,如果您有需要每帧运行的代码,那么您可以实现ITickable接口:
public class Ship : ITickable
{
public void Tick()
{
//执行需要每帧执行的任务
}
}
然后将其连接到安装器上
Container.Bind<ITickable>().To<Ship>().AsSingle();
如果您不想总是记住您的类实现了哪些接口,您可以使用简单方式(详见后续“BindInterfacesTo 和 BindInterfacesAndSelfTo”章节)
注意,所有ITickable的Tick()方法的调用顺序也是可配置的,见后续“Update / Initialization 顺序”章节
另注意,ILateTickable 和IFixedTickable接口分别镜像了Unity的LateUpdate和FixedUpdated方法。
IInitializable
如果你需要在给定对象上进行一些初始化操作,你可以将代码添加到构造函数中。然而这意味着初始化逻辑将在创建对象图的过程中发生,所以,这并不是理想方式。
一种更好的方式是实现IInitializable
接口,然后在Initialize()
方法中执行初始化逻辑。
然后将其连接到安装器上:
Container.Bind<IInitializable>().To<Foo>().AsSingle();
如果您不想总是记住您的类实现了哪些接口,您可以使用简单方式(详见后续“BindInterfacesTo 和BindInterfacesAndSelfTo”章节)
Foo.Initialize
方法将在对象图构造完成及所有构造函数调用之后被调用。
注意,整个对象图的构造函数在Unity的Awake方法中被调用,IInitializable.Initialize
在Unity的Start方法中被调用。因此,使用IInitializable而不是构造函数更符合Unity自己的建议,即:使用Awake阶段来设置对象引用,并将Start阶段用于更复杂的初始化逻辑。
这种方式也比使用构造函数或[Inject]方法更好,因为初始化顺序可以以类似定义ITickable的顺序的方式自定义。如下例:
public class Ship : IInitializable
{
public void Initialize()
{
// 在这里初始化对象
}
}
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();
}
}
然后,包含到安装器中:
Container.Bind(typeof(Logger), typeof(IInitializable), typeof(IDisposable)).To<Logger>().AsSingle();
或者您可以使用BindInterfaces方式:
Container.BindInterfacesAndSelfTo<Logger>().AsSingle();
这是有效的,因为当场景发生变化或应用程序关闭时,Unity的所有MonoBehaviour都将触发OnDestroy()事件,包括SceneContext类,然后该类会触发绑定到IDisposable的所有对象的Dispose()方法。
您还可以实现ILateDisposable,该接口类似于ILateTickable,在所有IDisposable触发后调用。但是,对于大多数情况,如果对执行顺序有要求,您最好设置一个明确的执行顺序。
网友评论