System 系统
ECS或是面向数据设计最主要的目标就是从行为中分理出状态(State from behaviour)。System是我们定义行为的地方。我们可以在system种利用代码来创建新的状态,改变或是删除已有的状态。
在Entitas-CSharp中,我们有几个接口需要实现以标记一个类是system。 ISystem
是最基础的接口,我们不需要实现。他只是一个用于内部的标记作用(所谓的幽灵协议)。
如果我们需要需要一个持续执行的system,我们需要实现IExecuteSystem
。这个接口只有一个方法void Execute();
。这是我们写需要每tick都需要执行的代码的地方。
另外一个systm也是持续执行的是ICleanupSystem
。这个system是为了我们在执行完所有的IExecuteSystem
之后执行的逻辑所设立的。像他名字建议的一样,你应该放清理(clean up)的代码到它的void Cleanup();
方法中去。他们都只是接口,你可以有用一个类同时实现这些协议,有时候这样做从游戏逻辑上来说非常有意义。
Setup and Teardown 建立与销毁
通常来说,当我们开始一个游戏,我们需要先创建初始状态。这就是为什么在Entitas-CSharp中我们会有IInitializeSystem
接口了。它有一个void Initialize();
方法用于存储你游戏中的初始化逻辑,基本上来说就是创建所有开始游戏所需要的Entitas和其他状态。
与IInitializeSystem
相对应的是ITearDownSystem
。这个接口拥有void TearDown();
方法。我们可以将游戏/关卡/场景(任何符合你使用场景的地方)结束后执行的逻辑放到这里。
Composing systems 组合系统
知道现在为止,我在这个章节中描述的所有东西都是为了更方便的分离行为代码的接口。我看见过有些人在项目中使用Entitas而不用Systems。他们实现了自己的命令模式(Command Pattern)。但是如果你想要使用systems这种方式的话,你可能想要将systems组合成一个明确的层级。为了做到这一点,我们提供了一个Sysetms
类,实现了 IInitializeSystem, IExecuteSystem, ICleanupSystem, ITearDownSystem
接口。我们可以Add
一个system到Systems
的实例中,然后当我们调用Systems上的Execute(), Cleanup(), Initialize(), TearDown()
方法,它会调用所有添加了的system的对应方法。Systems
是一个在组合模式中典型的 Parent Node(父节点)。
当我们再MatchOne的例子时,会发现其实我们其实不会直接使用Systems
类:
public class MatchOneSystems : Feature {
public MatchOneSystems(Contexts contexts) {
// Input
Add(new InputSystems(contexts));
// Update
Add(new GameBoardSystems(contexts));
Add(new GameStateSystems(contexts));
// Render
Add(new ViewSystems(contexts));
// Destroy
Add(new DestroySystem(contexts));
}
}
在这里,我们扩展了一个Feature
类。根据我们选择是否开启 visual debugging ,会生成一个Feature 类集成Systems
或者DebugSystems
类。VIsual debugging消耗非常多的资源,所以当你在移动设备上运行或者制作产品Build的时候,你应该将VIsual debugging关闭掉。所以Feature就是为了让我们的代码更加简单而创造出来的类。
从上面代码片段我们可以得知的另一件事是,我们传入了一个Contexts
类。Contexts
类是另一个为了方便我们引用不同的Context实例而在ntitas-CSharp 中创建出来的类。这个生成的代码用于每种context类型的实例的获取器(在appliance章节会有更多关于code generator的内容)。
How do we execute the systems 我们怎么执行Systems
当我们实现完这些系统并且将他们组合成层级结构之后,我们需要在某处调用 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();
}
}
持续性的systems是否应该在FixedUpdate
而不是Update
中执行是一个经常出现的问题。这个问题一般是需要你自己做决定。我一般会将system放在Update
中,如果你的的情况需要放在 FixedUpdate
或者甚至LateUpdate
中,最终怎么做都是取决于你。你甚至还可以更疯狂一些,拥有多个system层级,有些在Update
中执行,有些在FixedUpdate
中执行,但我不确定这是不是一个好的想法。
How do I implement a typical execute system? 我怎么实现一个典型的执行系统?
一个执行system会周期性的运行,所以我们通常会做的是:设定一个或多个groups在sysem的构造函数里,然后在Execute
中我们会迭代这些group中的entity,改变他们或者是创造新的Entity。
总的来说说,我们会从context拉去数据并且针对这些数据做一些事情。我们将会在下一个章节了解到在Entitas-CSharp中,还有另一种方式去处理这些数据。