同一 Entity 包含多个同类 Component 的问题

ECS 中,同一个 Entity 是否可以由多个同类型的 Component 构成?在 Unity 中,答案是可以。我们的引擎在设计之初也是可以的。 当时有一个问题:在 Lua 中,如何访问同类型的 Component ?如果有多个同类 Component ,最自然的方式是把它们放在一个数组里。但是、绝大多数情况下我们用不上这个特性,每次访问 Component 都加一次 [1] 或 [0] 的数组索引显得画蛇添足。若单个 Component 不用数组,多个才用数组,写起来又有极大的心智负担。因为这样做,它们就成了两个不同的类型。 后来,我们干脆利用 Lua 的特性,把数组和 Component 本身放在一个 table 中。如果有多个 Component 就把这个数组直接放在第一个 Component 的 table 内。就这样用了一段时间后,最后还是受不了这个脏技巧。等到用 C 编写 luaecs 后,就砍掉了这个特性。 ECS 不是万能灵药。如果需要让相同的 Component 聚合在一起,那么就使用额外的数据结构,或是不只使用一个 world 。这是我们目前实践给出的答案。去年在 luaecs 的 issue 9 也讨论过类似问题。 最近,我们在不同的地方又碰到类似的问题。我觉得这个问题的本质是,ECS 虽然提供了一个高效的、不同于 OOP 中的对象的数据模型,但它对数据结构的实现有很大限制(才能换取高效)。我们应该怎样在这种限制下设计出更有弹性的数据结构。 其中一个问题出在我们引擎的渲染模块上。 通常当我们需要一个弹性数据结构,在 luaecs 中,申明一个 lua table 作为 Component 就可以了。table 内的数据结构可以随心所欲。但如果想从 C Side 访问这个 lua table ,性能势必大打折扣。这对于渲染底层显然有不可接受的性能代价。 所以,我们的渲染层用到的 Component 结构都是定义好的 C 结构。用 C side 访问无额外开销,反之在 Lua side 访问比原生 Lua 结构多一个间接层。但我们只在 Lua side 处理这个结构的初始化和其它的一些低频操作,这是可以接受的。换得的好处是在 C side 直接编写 system 原生的处理渲染过程,这让我们可以让一个基于 Lua 架构的渲染模块可以获得 C/C++ 原生代码编写的渲染模块 90% 以上的性能。 在材质部分,我们遇到了同一个可渲染对象需要多个材质的问题:比如,一个可渲染对象,在普通的渲染流程用到的材质肯定和渲染它的阴影时用到的不同。一些特殊的渲染效果,也会用到不用的材质。 解决方法是,在 C 结构中,直接定义出材质数组。它们是一体的,为同一个 Component 。这样,同一个可渲染对象能同时拥有的不同材质数量就有一个上限。在实践中,我们发现,这个上限可大可小,不同的项目有不同的需求。如果我们把自定义效果做成插件的话,组合过多效果就可以需要更大的上限;若只用基本的渲染模块,又不需要太大。 对于 C/C++ 项目,我们会用动态数组实现这种弹性需求,如果不需要弹性,则在编译期确定数组大小。而对于动态语言,没有也不能在编译期固定数据结构,一切都是动态运行时再确定的。而我们这种混合使用的方式,引入了第三种数据结构的形态:初始化阶段动态构造数据类型,而运行过程中不再变化。这时,C side 看到的是一个材质数组,但数组的长度不是编译期确定的,而是在运行时传入。该长度在程序初始化阶段就确定好了,它取决于我们加载了多少自定义材质的插件。 如果需要在 C Side 处理更有弹性的数据结构怎么办?我和同事讨论过这个问题。我建议用一个 C/C++ 模块管理这种数据类型,以 id 来索引它们。而 luaecs 中只保存 id 。定期比对 ecs 中的 id 集合和 C/C++ 模块中的集合,就能找出已被销毁的那些 id 对应的对象。

评论 抢沙发

表情