一个小例子
“Hello World”
“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;
}
如果某物具有位置和速度,我们可以通过可移动组找到它。然后,我们可以遍历所有可移动物体,并将可移动物体的位置替换为其先前位置加上速度。
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.position.x + e.velocity.x, e.position.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();
}
}
}
}
现在,我们可以简化减速系统,通过删除速度检查并仅用减小的速度替换当前速度。
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”_的主要原因。