« 分组功能在挂接系统上的使用 | 返回首页 | 重构数学库 »

ECS 系统中 Entity 的生命期管理

我们的游戏场景是由若干场景节点构成的,每个场景节点是一个 Entity 的 Component 。而一个复杂的场景可以在编辑器中生成一个预制件 Prefab,像搭建乐高积木那样堆砌已经做好的部件。关于预制件的设计,之前有过两篇 blog 讨论。分别是:游戏引擎中预制件的设计预制件和对象集的管理

就目前的使用经验来看,几乎所有游戏中的 Entity 都是从 Prefab 实例化得来的。一个 Prefab 会实例化出 n 个 Entity ,但这 n 个 Entity 的生命期管理却很麻烦。

最自然的想法是:所有的 Entity 都必须是场景树上的节点(拥有场景组件),当我们删除一个场景节点时,它所有的子孙都一起移除。

这个做法有两个问题:

  1. 如果一个 Entity 被间接移除(祖先被删除),持有它的引用该如何处理?传统的方法是,只能对 Entity 保持弱引用。一般用 Entity ID 作弱引用。我们在对 Entity ID 解引用时,应检查引用是否有效。

  2. 有些 Entity 未必是场景节点,但也需要合理的自动化生命期管理怎么办?传统的方法要么让所有的东西都必须是场景节点,要么把这个东西变成场景某个节点的一个组件附着在场景节点上。

我不喜欢上面提到的传统解决方案。因为:生命期管理和场景管理其实是两件事。场景管理是按树结构组织的,而生命期管理更适合用集合这种结构。所有需要被管理的东西不应该都必须在场景树上。

动态给 Entity 增加 Component 的能力看似有用(我们的 ECS 系统刻意不支持它),但只是为了把一个不相干对象的生命期和另一个对象绑定起来,就强行把它作为 Component 和场景的 Component 组合在一起,我觉得也是对 ECS 的滥用。

回头来看 Prefab 。我们应当把它看成是对 Entity 组合的持久化,它持久化了 Entity 本身的数据,以及 Entity 之间的关系。然后,我们可以在运行时将持久化的模板实例化。

既然,prefab 的 instancing 是创建出一组预先设计好的 Entity 及其相互关系的主要方法;那么,销毁它们的过程也应该是对称的。即,如果我们通过 prefab 一起创建出 n 个 entity ,那么也应该让这 n 个 Entity 一起销毁。应该避免只销毁其中的一部分。

在实践中,我们销毁部分 Entity 往往是因为拿出了部分 Entity 挂接到已有的场景树上;或是让其它 Entity 挂接在它的挂接点上。前者拆分了生命期管理集,后者扩大了生命期管理集。

我最近重构了引擎中挂接这个特性。避免了场景节点间的任意挂接行为。挂接变成了某种弱引用,而不改变原本固有(在 Prefab 中预设)的场景树形态。就避免了这个问题。所以,理所当然的,就可以实施更简单的生命期管理了。

现在的方案是,当 prefab instancing 时,它在创建一组 Entity 的同时,还会返回一个 instance id ,用来引用这组 Entity 的生命期。这个 instance id 只用于一件事,就是日后销毁这组 Entity ,而不能做任何其它事情。控制这些 Entity 还是要通过 Entity ID 或 select 特定的 Component 完成。

这样,我们的系统的每个 Entity 都具有两个 ID ,一个是唯一的 EntityID ,另一个是被 prefab instancing 时赋予的 instance ID 。前者用于引用具体的 Entity ,后者用来在上层管理生命期。底层既然有通过 Entity ID 销毁特定 Entity 的接口;但上层不可以使用这个接口,而只能通过 Instance ID 销毁一组 Entity 。

在实现的时候,用一个小技巧就可以把两个 ID 合二为一。

用一个 64bit 数字作为 Entity ID ,即使 Entity 被销毁,新的 Entity 也不会复用旧 ID 。这样可以通过 ID 实现弱引用。

EntityID 是在 prefab instancing 的过程被赋予的。我们采用 48+16 的形式。有一个单调递增的 48bit 内部 ID ,每次 prefab instancing 时加一。当一次 instancing 的 Entity 新建数目少于 64K 时,剩下的 16bit 可以完美表示每个 Entity ,两者合并就得到最终的 Entity ID 。如果有巨大的 Prefab ,它会构建超过 64K 的 Entity 也没有关系,继续递增那个 48bit 内部 ID ,就可以有新的 64K 集合可用。当 prefab instancing 结束,我们看一共用了多少个 64K 集合,就能生成唯一的 Instancing ID 了。

比如,某次 instancing 过程生成了100K 个 Entity ,内部 ID 一开始是 42 ,那么这个过程就用掉了 42 和 43 两个内部 ID ,表示 64K + 36K 两组 Entity 。所以 Entity ID 是从 42 << 16 开始,到 (43 << 16) + 36K 结束。

最终的 Instance ID 就是 42 << 16 | 2 ,表示这个分组是从 42 开始的连续两组。Entity 身上不必专门记录 Instance ID ,只需要取 Entity ID 的高 48bit ,是 42 和 43 的都属于这个分组。

Comments

记得在当年,北京写程序圈子里,有个著名的笑话,世贸大厦被飞机撞塌了以后,帝国大厦的老板暗中窃喜,心想,这下帝国大厦又是北美第一个高楼了,哈哈。
做程序也是,自己不去优化自己的代码,暗中期盼别人犯错务,这种心态也不适合搞技术,是个搞政治。

游戏成品最好是越华丽越炫最好,但是做技术最忌讳华而不实,徒有虚名,上一次听到这个慢1000倍,还是当年JavaSE2的年代(2001)

那你认为mono慢的根源是什么?在内存饥饿(内存接近最低操作系统的配置)的电脑上,mono这个运行的速度比C++编写的代码至少慢500~1000倍,那你认为,mono慢的原因是什么?

之前U3D群里讨论一些用了ECS得人就认为prefab上挂组件不对,尽量不用mono,但是我觉得挂组件并没有错,ECS也是挂组件,u3d得精髓就是组件化开发,mono慢并不是挂组件得问题,挂组件只是帮你做了序列化不用自己来拼对象得一个过程,明明有工具还要手工挫航母得话完全没有必要。

云大什么时候准备开源引擎呢

Post a comment

非这个主题相关的留言请到:留言本