系统(System)
ECS 或数据导向设计的主要目标是将状态与行为分离。系统是我们定义行为的地方。在系统中,我们可以编写创建新状态、更改给定状态或销毁状态的代码。
在 Entitas-CSharp 中,我们有多个接口,我们必须实现这些接口才能将一个类标记为系统。ISystem
接口是一个基础接口,我们不需要自己实现它。它只是一个被标记的接口(所谓的 ghost protocol),在内部使用。
如果我们想要一个应该周期性地执行的系统,我们需要实现 IExecuteSystem
。这个接口只有一个方法 void Execute();
。这是我们在每个时钟周期中执行代码的地方。
另一种周期性执行的系统类型是 ICleanupSystem
。这个接口用于在所有 IExecuteSystem
运行后执行的逻辑。顾名思义,您应该将清理代码放在其 void Cleanup();
方法中。由于这些仅仅是接口,我们可以有一个类同时实现这两个协议。有时从游戏逻辑的角度来看,这是完全有意义的。
设置和拆卸
通常情况下,在我们启动游戏时,我们需要首先创建初始状态。这就是为什么在 Entitas-CSharp 中我们有 IInitializeSystem
接口。它有一个 void Initialize();
方法,该方法应该包含您的游戏初始化逻辑 - 基本上是创建您需要开始玩的所有实体和其他状态。
IInitializeSystem
的对应物是 ITearDownSystem
。这个接口有一个 void TearDown();
方法,在我们关闭游戏/关卡/场景之前(适用于您的用例)执行代码。
组合系统
到目前为止,在本章中我所描述的一切都只是接口,这些接口反映了我们用于拆分行为代码的惯例。我见过一些项目,其中的人们在没有系统的情况下使用了 Entitas。他们实现了自己的命令模式。但是,如果您想要遵循系统方法,您可能需要在某种层次结构中将系统组合在一起。为此,我们提供了一个 Systems
类,它实现了 IInitializeSystem, IExecuteSystem, ICleanupSystem, ITearDownSystem
接口。我们可以将一个系统添加到 Systems
类的实例中。这样,当我们在此实例上调用 Execute()
、Cleanup()
、Initialize()
、TearDown()
方法时,它将在添加的系统上调用这些方法。Systems
类在 组合模式 意义上是一个典型的父节点。
当我们看 MatchOne 示例时,可以看到我们不直接使用 Systems
类:
public class MatchOneSystems : Feature {
public MatchOneSystems(Contexts contexts) {
// 输入
Add(new InputSystems(contexts));
// 更新
Add(new GameBoardSystems(contexts));
Add(new GameStateSystems(contexts));
// 渲染
Add(new ViewSystems(contexts));
// 销毁
Add(new DestroySystem(contexts));
}
}
在这里,我们扩展了 Feature
类。Feature
类是一个生成的类,它扩展了 Systems
类或 DebugSystems
类,具体取决于是否要启用可视化调试。可视化调试会消耗大量资源,不应在进行生产构建或在移动设备上运行游戏时启用。这就是为什么生成 Feature
类是为了使我们的实际代码更简单。
从上面的代码片段中,您可能还注意到我们传入了一个 Contexts
类。Contexts
类是在 Entitas-CSharp 中生成的另一个方便类,我们可以在其中引用不同的上下文实例。生成的代码包含每个上下文类型的实例的 getter(有关代码生成器的更多信息,请阅读工具章节)。
如何执行系统
在实现系统并将它们组合成层次结构后,我们需要在某个地方调用 Execute()
、Cleanup()
、Initialize()
、TearDown()
方法。为此,通常我们会创建一个 MonoBehaviour
来执行。如果您不使用 Unity3D,您需要自己决定在何处触发这些方法。我想再次使用 MatchOne 来展示这样的 MonoBehaviour
类可能会是什么样子:
using Entitas;
using UnityEngine;
public class GameController : MonoBehaviour {
Systems
_systems;
void Start() {
Random.InitState(42);
var contexts = Contexts.sharedInstance;
_systems = new MatchOneSystems(contexts);
_systems.Initialize();
}
void Update() {
_systems.Execute();
_systems.Cleanup();
}
void OnDestroy() {
_systems.TearDown();
}
}
一个经常出现的问题是,周期性系统是否应该在 FixedUpdate
而不是 Update
上运行。这通常是您个人的决定。我通常在 Update
上安排系统,如果在您的情况下在 FixedUpdate
或甚至 LateUpdate
上安排很重要,那么这是您的决定。您甚至可以在多个系统层次中进行切换,一个在 Update
上执行,另一个在 FixedUpdate
上执行,虽然我不确定这是否是个好主意。
如何实现典型的执行系统?
执行系统定期运行,因此我们通常要做的是,在系统构造函数中设置一个或多个组,然后在 Execute
中迭代这些组中的实体并对其进行更改或创建新实体。
一般来说,我们从上下文中拉取数据并对其进行操作。在 Entitas-CSharp 中,还有另一种处理数据的方法,您将在下一章中详细了解。