一个简单实体框架
一个简单实体框架
是什么?
我们经常会听到ECS这个名词,最近Unity也在推动他的ECS框架。那到底什么呢?
我们从字面上来解释就是Entity-Component-System,也就是实体-组件-系统。
分别解释一下:
- Entity :一个对象
- Component :存储数据的容器
- System :处理对象数据的运行器
我们可以从《游戏编程模式》中看到一个叫作组件模式,我认为ECS就是该模式的变种。
为什么用ECS
解耦
我们在游戏里所有不同的系统被杂糅进一团乱麻的代码,如我们设计一个人物类:
if (collidingWithFloor() && (getRenderState() != INVISIIBLE))
{
playSound(HIT_FLOOR);
}
这样耦合的设计在任何游戏中都是一种糟糕的设计,并且在使用并发性时会更加的不好。好的并发性是希望把游戏的各个模块在各自的线程运行,如AI计算在一个核中完成,生效在另一个核,渲染在第三个核等。
如果想要修改以上代码,就需要程序员了解物理、图像以及声音相关知识。
我们将这个人物类根据域边界切分成相互独立的部分。如,我们将所有用户的输入的代码放到一个单独的类InputComponent
中。完成切分后,我们几乎把这个人物类中所有的东西都清空了,这样这个人物类就只剩下一个空壳,那么这个空壳我们就叫Entity
。
轻量对象
当我们使用面向对象编程的时候,继承总是最实用的工具。它被视为代码重用的终极武器。然而我们发现这个武器很多时候是块绊脚石,继承有它的用途,但是对某些对象中的父类逻辑,该对象可能永远不会用到,这会使这个设计存在冗余。
简单的设计
我们先来看看整个的框架的流程,如下图:
我们在系统模块中去搜索我们需要处理的实体,搜索条件是拥有或者是不拥有某个特定的组件。然后遍历找到的所有实体,检测数据或者修改数据,再做特定的行为。这里的数据就是组件。
实体组件管理
我们在设一个 EntityContext
类来管理所有的实际与组件,其中使用两个数组来管理所有的实体与组件,使用索引来做每个实体与组件的映射。使用这种方式,我们需要固定每个实体的组件数量。具体的映射图如下:
这样设计才使得我们的实体是真正意义上是一个空壳。
我们这样设计就需要为每个组件设置固定的索引去映射到组件数组中去,所以我们在ECS启动时,为每个组件分配唯一的索引,也可以叫做组件ID。
这样我们可以通过实体的索引与组件的ID来获取到某个组件,计算如下:
int componentIndex = entity.index * maxComponentsPerEntity + Component.ID;
Component com = components[componentIndex];
索引池
这里我们就需要设计一个索引池来维护实体的索引,其实实体的索引也可以认为就是实体的ID,索引与ID都需要是唯一的。
我们申请一个数组来作为索引池,以这个数组的索引来最为实体的索引也就是ID。
实体筛选
我们需要获取特定的实例,比如我们需要获取所有含有PositionComponent
这个组件的实体,按照正常的想法就是对所有的实体遍历,判断每一个实体是否拥有该组件。
再具体的做法就是获得该组件的ID,根据实体的索引获得该组件的对象,判断其是否为空就可以了。
这里我们可以不去关注组件对象本身,我们可以使用一个更为直观简单高效的方式来判断一个实体是否拥有一个组件。
使用一个数字的二进制的每一位 0/1
表示是否有当前位数的组件,我们的组件的ID是动态分配的,从0开始,我们可以把ID看做数字的二进制的多少位。这样我们可以把这个问题转为这个数字的第多少位是否是1
。如果是1
,说明实体拥有该组件。
但是这样做会有一个弊端就是越界,我们数字的长度是有限的,当一个数字不足时,我们可以使用两个数字来存储,一个数字左移,一个数字右移。
但是这个都不是万全之策。但是从我们的实际使用来看设计一个可以存储128
为的数字即可,使用两个Int64
数字组合。
Entity
- Entity :我们的实体就是一个空壳,里面只需要一个
ID
字段即可。 - EntityType :实体类型的接口来区别表现层与逻辑层的实体。
Component
- ComponentID : 组件的ID,需要在系统开始时注册到一个集中的地方,然后分别动态分配数值。
- ComponentBase : 注册与销毁组件ID。
System
- ProcessBase :逻辑处理器,一般一个组件对应一个处理器。
- EntityProcess :含有所有逻辑处理器且驱动它们。