英文原文:https://github.com/mzaks/EntitasCookBook
“Hello world”就是一个典型的例子,用来说明手头技术的主要好处。对于编程语言,人们使用一个将 Hello World 输出到控制台的小程序。对于我看到的大多数 ECS 实现,从 MoveSystem 开始。
移动系统
移动系统是移动实体的系统。为了移动某个东西,这个东西需要有一个位置和一个速度:
using Entitas;
[Game]
public sealed class PositionComponent : IComponent {
public int x;
public int y;
}
[Game]
public sealed class VelocityComponent : IComponent {
public int x;
public int y;
}
如果某物有位置和速度,我们可以通过可移动Group找到它。比我们可以遍历所有可移动物体并将可移动物体的位置替换为其先前位置加上速度。
using Entitas;
public sealed class MoveSystem : IExecuteSystem {
Group movableGroup;
public MoveSystem(Contexts contexts) {
var matcher = Matcher.AllOf(GameMatcher.Velocity, GameMatcher.Position);
movableGroup = contexts.game.GetGroup(matcher);
}
public void Execute() {
foreach(var e in movableGroup) {
e.ReplacePosition(e.postion.x + e.velocity.x, e.postion.y + e.velocity.y);
}
}
}
这就是我们可以使用 Entitas 移动事物的方式。这是一个非常简单的示例,但它说明了 ECS 的优势。这个系统可以找到每个相关实体并移动它。我们不关心这个实体附加了什么样的其他组件,它可以是汽车、人类、狗或房子,我们不关心。我们刚刚定义,如果某物有位置和速度,它将被移动。
减速系统
使用给定的移动系统,我们将以恒定的速度移动物体,直到我们停止游戏/应用程序。然而,在大多数情况下,我们希望减速。让我们在一个单独的系统中进行。
using Entitas;
public sealed class DecelerateSystem : IExecuteSystem {
Group velocityGroup;
public DecelerateSystem(Contexts contexts) {
velocityGroup = contexts.game.GetGroup(GameMatcher.Velocity);
}
public void Execute() {
foreach(var e in velocityGroup) {
var velocityX = e.velocity.x / 2;
var velocityY = e.velocity.y / 2;
if (velocityX == 0 && velocityY == 0) {
e.RemoveVelocity();
} else {
e.ReplaceVelocity(velocityX, velocityY);
}
}
}
}
上面的实现非常简单,它只是将速度减半,所以速度会很快变为零向量,当它变为零时,我们将其全部删除。我不希望你在真实的代码中写出这样的东西,但它足以展示 ECS 的一个关键概念。使系统遵循单一职责原则是一个好主意。移动系统只关心可移动的实体。减速系统与降低速度有关。我们甚至可以说减速系统的当前实现有两个职责。它会降低速度并消除它。我们甚至可以引入第三个停止系统。它可以是一个响应式系统,当它变为零向量时移除速度。
using Entitas;
public sealed class StopSystem : ReactiveSystem<GameEntity> {
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context) {
return context.CreateCollector(GameMatcher.Velocity);
}
protected override bool Filter(GameEntity entity) {
return entity.hasVelocity;
}
protected override void Execute(List<GameEntity> entities) {
foreach (var e in entities) {
if (e.velocity.x == 0 && e.velocity.y == 0) {
e.RemoveVelocity();
}
}
}
}
现在我们可以通过取消速度检查并用降低的速度替换当前速度来简化减速系统。
使用 Entitas;
using Entitas;
public sealed class DecelerateSystem : IExecuteSystem {
Group velocityGroup;
public DecelerateSystem(Contexts contexts) {
velocityGroup = contexts.game.GetGroup(GameMatcher.Velocity);
}
public void Execute() {
foreach(var e in velocityGroup) {
var velocityX = e.velocity.x / 2;
var velocityY = e.velocity.y / 2;
e.ReplaceVelocity(velocityX, velocityY);
}
}
}
启示
这一切都很有趣,但我仍然没有解释我的主要观点。我的主要观点是系统不直接相互通信。在面向对象编程、函数式编程甚至过程式编程中——对象、函数或过程直接且同步地相互通信。在 ECS 系统中,仅查询状态和更改状态。它们彼此分离。或者至少他们不知道彼此的存在。您可能会争辩说事物是通过组件类型耦合的,但组件类型是数据驱动的。我们定义它们是为了反映我们想要模拟的“世界”,因此数据成为我们的 API。
这是一种非常独特的方法来处理我称之为 ECS 的第一条规则的事情 - 将状态与行为分开。这就是为什么我认为它值得成为Hello World。