ECS 中的 Entity
我认为 ECS 框架针对的问题是传统面向对象框架中,对象数量很多而对象的特性非常繁杂,而针对对象的不同方面 aspect 编写处理逻辑会非常繁杂。每个针对特定的方面执行业务,都需要从众多对象中挑选出能够操作的子集,这样性能低下,且不相关的特性间耦合度很高。
所以 ECS 框架改变了数据组织方式,把同类数据聚合在一起,并用专门的业务处理流程只针对特定数据进行处理。这就是 C 和 S 的概念:Component 就是对象的一个方面 aspect 的数据集,而 System 就是针对特定一个或几个 aspect 处理方法。
那么,Entity 是什么呢?
我认为 Entity 主要解决了三个问题。
生命期管理。如果我们把对象的不同方面分拆到了不同的 Component 中,同类的 Component 群聚在一起。这样就达到了 System 间的解耦。但是这些 Component 依然在生命期上属于一个整体,换句话说,它们还是属于一个对象。所以 Entity 就是联系同一对象上不同组件生命期的东西。我们可以通过删除一个 Entity 来从世界中剔除一组相关的 Component 。
组件关联性。有些系统只处理单一类型的 Component ,那么它们可以无视对象身上的其它方面。但很多系统必须处理一个对象所属的多个方面 ,例如你需要把物理系统针对碰撞体计算出来的结果反馈给对象的空间位置信息。碰撞体组件 A 和空间变换组件 B就发生了联系。整个世界中有众多的 A 和众多的 B ,Entity 就是负责把特定的 A 和特定的 B 关联在一起。
跨世界的对象关联。这个需求多见于 C/S 结构的网络游戏。服务器和客户端有两个世界,但是两个世界中的对象是互为镜像,它们是有关联的。我们需要有一个手段知道服务器上的某个对象到底对应着是客户端上哪个对象。这里的对象就指 Entity ,两个世界中的互为镜像的 Entity 有两个独立的数据拷贝,所以,我们需要用一个 id 来指代它们其实是同一个东西。这就是为什么 ECS 框架中,Entity 为何用 id 来表示的原因之一。
我们一开始开发 ECS 框架时,限制了一个 Entity 上只能由不同类型的 Component 构成,不允许多个同类的 Component 加到同一个 Entity 上。
现实需求总是复杂的。往往我们会碰到更复杂的数据组织需求。例如,一个玩家身上有多个 buf ;一个场景物件需要由多个几何体来描述碰撞体。这都涉及多个同类 Component 组合成一个 Entity 。
Unity 采取的方案是支持同类 Component 组合。例如你可以给一个对象添加多个碰撞体。但我认为并非很好的解决方案。
因为,从 Entity 索引特定的 Component 变得复杂,遍历世界中所有同类 Component 接口不仅需要关心每个 Component 隶属于哪个 Entity ,还需要关心是 Entity 上多个同类 Component 中的哪一个。很难简单的把同类 Component 置于一个简单集合中。
另外需要把同类子物件摊平为单个组件。就以碰撞体为例,它原本可分为集合形状和空间位置两部分,或者你还可以给碰撞体添加颜色(用于开发调试)、分层等等其它属性,但由于我们需要把多个碰撞体加在物件上,不得不把碰撞体的所有属性都加到同一个叫碰撞体的特定类型 Component 上。
这样是不利于 ECS 框架原有希望做到的数据解耦:例如空间剔除器只关心空间位置和包围盒,并不关心形状、阻挡属性等等;而物理系统则关心几何体的外形描述,质量等等其它方面。
事实上,Unity 也做了场景树这种不同 Entity 间的组合。可以把一个 Entity 挂接到场景树上,让空间关系受另一个 Entity 影响。我认为场景树和 Component 组合在功能上有重叠。
我现在的想法是,应该抽象出另外两类 Component ,一个是 subset ,就是一个 entity id 集合,表示一组 Entity 是一体的,满足前面所说的生命期管理需求和关联性需求。
另一个是 owner ,指向一个 entity id ,表示自身被另一个 Entity 管理。
subset 和 owner 作为可选项,可以随意添加到 Entity 上。根据 System 的实现需求,有时仅用其中之一即可。
subset 的实现可以是一个简单的 table ,但针对 3d engine 来说,我们开发了名为 hierarchy 的数据结构,可以表示一棵树。也就是可以用一个 set 包容下整个树的节点,并储存下针对根节点的空间变换信息。
以场景组织为例,一个场景物件可以有多个碰撞体来描述碰撞信息。这些碰撞体会被物理系统处理。但在场景编辑过程中,我们在场景中挪动物件,则会改变相关碰撞体组的空间位置。所以我们可以把多个碰撞体加入场景物件的 subset 中,储存在一个 hierarchy 数据结构里。当编辑场景时,一个特定的 system 会负责从物件的空间位置更新碰撞体在空间中的位置,方便物理碰撞系统去处理。