Entity Index
首先来看几个概念:
- Entities 是运行时中独一无二的“东西”,通常与一个独一无二的整数标示。
- Type(或 Archetype)来区分 Entity 所拥有组件列表的异同,通常用一个数组标示。比如一个 FPS 游戏中 Player 对象拥有两个组件,分别是“位置”和“血量”,则它的 Type 就是
[Position, Health]
。
我们可以通过这两个概念来构造 ECS 中最基础的数据结构,即 Entity Index:
1 | using EntityId = uint64_t; |
通过 EntityId
我们可以很容易获取该 Entity 所拥有的组件列表,那么实现一个 HasComponent
便易如反掌:
1 | bool HasComponent(EntityId entity, ComponentId component) { |
针对 C++ 的特性,在添加组件时可以用模板的形式绑定就再好不过了:
1 | Player.add<Position>(); |
在这两个 add
操作之后,entityIndex
中就应该包含一项绑定 Player
与 [Position, Health]
的数组元素。当然,是以 ComponentId
,即 uint64_t
的形式定义的,例如:
1 | ComponentId Position_id = 1; |
如此一来,Player 这一 Entity 的 Type 便成了 [1, 2]
。
ECS 其中一种实现方法 SanderMertens/Flecs 提供了一种思路:由于 Entity 与 Component 均使用整形作为 id,可以将它俩统一存入 EntityIndex。虽然这样会降低些许可读性,但在实际开发时,考虑某个 Entity 拥有哪些组件也将会造成同样的问题。
1 | { |
“One of the biggest implications of this design is that we can add entities to entities” – Ajmmertens.
Hierarchies
上文已阐述了 Entity 与 Component 的储存方式,当我们将若干 Entity 载入场景中时,需要一个不可或缺的功能:Entity 层级。按照之前 Entity Index 的实现,我们可以将父节点以 Component 的形式绑定至 Entity 中:
1 | { |
当然这个写法是有问题的,我们无法区分 std::vector<uint_64>
中哪些是父对象,哪些是子对象:
1 | Player: [Position, Health, MyPlatoon] |
这时可以使用**类型标注(Type Flag)**来对数组元素进行标注,以描述其在数组中的作用。下例使用了一个 CHILDOF
标注修饰 MyPlatoon
,令其成为 Player
的父对象。
1 | Player: [Position, Health, CHILDOF | MyPlatoon] |
只需要进行一次或运算便能得到父对象:
1 | Type type = entity_index[Player]; |
层级可谓游戏设计中极为常用的功能,牺牲一些内存来储存层级信息优大于劣。当然现在我们只谈到了如何储存方式,如何在系统中运作就是另外一个浩大工程了。
Prefab
重用对象,又称预设(Prefab)这看起来很复杂的功能在游戏开发中也不可或缺。在 Flecs 中将对象扩展了一位信息用作声明实例间的关系,显式定义为INSTANCEOF
。利用这一位信息再添加一些结合语义,就能使对象共用一组 Component。
1 | { |
当我们想获取 InstanceA
或 InstanceB
的 Color
组件时,实际上获取到的是 Base
的 Color
。
1 | InstanceA.get<Color>(); // returns Base.Color |
这里说的共用组件(shared component)对应拥有组件(owned component)。下面看一段 Flecs 的应用代码,观察如何在 API 上去调用:
1 | auto BlueSquare = flecs::entity() |
将会生成如下 entity index:
1 | { |
继续优化:假设 SquareA
想要更新自己的 Color
而不去影响其他 BlueSquare
实例,便需要一个复写(override)的功能。就结论来说我们可以增加两条规则:
- Adding a component to an instance will override the base
- When overriding a shared component, the value of the base will be copied to the instance
1 | auto BlueSquare = flecs::entity() |
注意第 6 行并没有为 Color
组件赋初值。根据规则 2 的定义,新增的 Color
组件内容将拷贝于BlueSquare
。执行完上面的代码后将得到:
1 | { |
功能虽然有了,但每次创建实例时都需要将 Color
重新 add
着实麻烦。我们可以定义一个特殊的 Archetype 来代替这个操作:
1 | auto BlueSquarePrefab = flecs::entity() |
Summary
层级与实例均使用了 EntityId 中的一位来用于标识,即使是如此简单的数据结构,却能实现一个较为复杂的行为。
1 | // Entities, types and entity index |
希望这篇文章能给你提供一些关于如何设计 ECS 的灵感~