« Windows 下重定向当前进程的 stdout 到网络连接 | 返回首页 | ECS 的 entity 集合维护 »

Lua 下的 ECS 框架

前段时间,我写了一篇 浅谈《守望先锋》中的 ECS 构架 。最近想试试在 Lua 中实现一个简单的 ECS 框架,又仔细琢磨了一下。

我思考后的结论是:ECS 并不是一个新概念,它的提出其实是和语言相关的。ECS 概念的诞生起于游戏行业,相关框架基本都是基于 C++ 来开发的。它其实是对 C++ 对象模型的一个反思。ECS 针对组件组合对象,而反对 C++ 固有的基于继承的对象模型。对象模型才是 ECS 的设计核心理念。而离开 C++ 的对象模型,ECS 并不是什么新鲜的东西。

我的这个观点也不新鲜,在 ECS 的 Wikipedia 页上也有类似的说法:

In the original talk at GDC Scott Bilas compares C++ object system and his new Custom component system. This is consistent with a traditional use of this term in general Systems engineering with Common Lisp Object System and Type system as examples. Therefore, the ideas of "Systems" as a first-class element is a personal opinion essay. Overall, ECS is a mixed personal reflection of orthogonal well-established ideas in general Computer science and Programming language theory. For example, components can be seen as a mixin idiom in various programming languages. Alternatively, components are just a small case under the general Delegation (object-oriented programming) approach and Meta-object protocol. I.e. any complete component object system can be expressed with templates and empathy model within The Orlando Treaty vision of Object-oriented programming,

抛开理论不谈,如果要在 Lua 中实践,我们到底可以做点什么呢?

我认为需要有这几个方面:

首先应该对 Lua 加强类型系统。Lua 的动态性天然支持把不同的组件聚合在一起,我们把不同的 Component 放在一张表里组合成 Entity 就足够了。但如果 Component 分的很细的话,用很多的表组合成一个 Entity 对象的额外开销不小。不像 C++ ,结构体聚合的额外开销几乎为零。我们完全可以把不同 Component 的数据直接平坦放在一个 table 中,只要键值不冲突即可。但是我们需要额外的类型信息方便运行时从 Entity 中萃取出 Component 来。另外,如果是 C / Lua 混合设计的话,某些 Component 还应该可以是 userdata 。

从节省空间及方便遍历的角度讲,我们甚至可以把同类的 C Component 聚合在一大块内存中,然后在 Entity 的 table 中只保留一个 lightuserdata 即可。ECS 的 System 最重要的操作就是遍历处理同类 Component ,这样天然就可以分为 C System 和 Lua System 。数据的内聚性很高,可以直接区分开 C data 和 Lua Data 。

然后、就是方便的遍历。ECS 的 System 需要做的就是筛选出它关心的 Entity ,针对其中的 Component 做操作。如果需要筛选结果大大少于全体 Entity 数量,遍历逐个判断就会效率很低。好在在 Lua 中,我们可以非常容易地做出 cache ,只需要遍历筛选一次,在监控新的 Component 的诞生就可以方便的维护遍历用的集合了。


我写了一个初步的版本。打算等到实际使用起来再慢慢完善。

它可以实现成一个纯 Lua 版,但我特地尝试把里面的两个函数编写了 C 的等价版,看起来可以提高不少性能。

这里的 API 中, Entity 全部使用唯一数字 id 标识,而不主张直接引用 entity 的 table 。这也是一般 ECS 框架的通用做法。数字 id 可以提高健壮性,还可以避免对已经销毁的 Entity 错误的引用。如果需要 C / Lua 混合编程的话,在 C 中引用 id 也方便的多。

类型系统是这样设计的:

每个 Component 有一个 16bit 的唯一类型 id ,每个 Entity 是由若干 Component 组合而成,我将它们的类型 id 升序排列成一个字符串,作为整个 Entity 的动态类型。然后 Lua 中实现了一个 cache ,可以从这个类型字符串转换成易用的类型对象。类型对象用来对 Entity 做筛选,从里面分离出 Component 。这个类型字符串的拼接我实现的是一个 C 版本,虽然 Lua 也能实现的出来,但是效率会低很多。

大部分 Component 我主张直接平坦的放在 Entity 对象表中,但若需要用 C 结构来承载,或单独用一个子表,也提供了 Component 类型注册方法,可以在 new component 时定义一个专门的构造函数(以及 delete 时的析构函数)。

这里还提供了遍历包含特定 Component 的 Entity 集合 的迭代器。它由一个弱表实现的 cache 来管理。在第一次遍历时,会创建一个集合,收集 Entity 全体集合中符合要求的部分,把筛选出来的 id 记录下来。一旦遍历集合创建好,它还会跟踪新的 Component 的创建,自动加进来。

这里的迭代器,我同时实现了 Lua 版本和 C 版本。C 版本的性能会高一些,我认为这个 C 版本在遍历相关集合时,性能表现不会差于用 C/C++ 实现的原生容器。


以上是对 Entity 和 Component 的支持。System 相关的方法,我还没有想好可以做点什么。等用到了再完善。

Comments

System的部分,可以做2种:

一种普通的System,提供Update、CleanUp方法
另一种是ReactSystem,可以做2种监视,一种是Entity持有的Component发生变化,一种是Component的内容发生变化

unity2018也推出ECS了,Component也是平坦放的,提高了不少缓存命中,System可并行充分利用多核cpu.
https://github.com/Unity-Technologies/EntityComponentSystemSamples

“我写了一个初步的版本”url无效的?

@thewind
类型安全这个词有一个定义。看来我们对这个定义认识不一致。
我说某种写法放弃了类型安全,并不是指责这种写法不好,只是说这种写法不符合类型安全的定义而已。

至于你说的:
> 这需求又是在静态类型之外
这可能不对。

即使是Java之类的不够现代化的语言也可以用Object Algebras以类型安全的方式解决Expression Problem。

@杨博 你说类型安全的component,是指scala等语言的 trait吧.
确实,不支持trait的语言,通过运行时的component来模拟trait.
但我们仍有需求在运行时修改一个对象trait list,这需求又是在静态类型之外,(能够运行时构造类型是另一个话题)
但你不能说是这非类型安全(如果我们指得是编译时安全的话),get component得到的nullable(option),原则上可以进行编译期检查的(虽然java, c#并没有区别是否nullable的object)

编辑器的话,需要额外的元信息很有必要。

主要是我看原文这句话误解了用意。

> 但是我们需要额外的类型信息方便运行时从 Entity 中萃取出 Component 来。

我以为是类似COM的IUnknown.QueryInterface那种,在每次函数调用前要先查找接口的用法。

用 lua 实现 ECS 框架,必须额外描述 component 的类型信息,这对于数据驱动的游戏引擎来说是很重要的。

因为(类 Unity)编辑器工具依赖 Component 的类型信息做序列化,以及在运行时把需要的数据反射到调试器中。

每个 component 的数据条目,都有有类型信息来定义:是否能被调试器观测到,以及是否需要持久化。

我不同意这句话:
> 但是我们需要额外的类型信息方便运行时从 Entity 中萃取出 Component 来。

我觉得用Lua很容易实现ECS,只要把数据表和__index表分别平坦的拼出来就行了。根本不需要“查找”Component。

因为Lua相比C#有一个好处,__index表也可以动态创建啊。只要在System上把启用Component的各种组合的__index表分别缓存一下就行了。

我琢磨一下,刚才回复里可能写得不对。更正一下,以下才是正确的和ECS的对应关系:


这个页面里的Plugin1、Plugin2、Plugin3大致对应System,INDArrayLayer、INDArrayWeight大概对应Entity。而Component是个内部实现细节,隐藏在PluginX.XxxApi这样的类型里面。因为实际上造出来的INDArrayLayer会继承Plugin1.INDArrayLayerApi、Plugin2.INDArrayLayerApi、Plugin3.INDArrayLayerApi,而INDArrayWeight会继承Plugin1.INDArrayWeightApi、Plugin2.INDArrayWeightApi、Plugin3.INDArrayWeightApi,所以自动就有这些Component上的功能了。

这套系统除了ECS之外,在创建hyperparameters时还有额外的配置功能,当然也是类型安全的。

我最近刚好在完全不同的领域做了一套类型安全的ECS架构。我把类似的功能用在深度学习框架上。

http://deeplearning.thoughtworks.school/plugins

这个页面里的hyperparameters大概跟ECS的S差不多,Plugin1、Plugin2、Plugin3大致对应Component,INDArrayLayer、INDArrayWeight大概对应Entity。

因为Scala支持path-dependent type,所以可以用hyperparameters.INDArrayLayer这种类型:其中hyperparameters是个值,INDArrayLayer却是个类型。如果值不同,类型上的功能就不同。

@thewind
这里类型安全是说可以在编译时检查知道Entity上有哪些功能,比如说启用了不用Component的Entity有不用的类型,里面有不同的函数,没启用的Component上的函数就类型检查不过。

如果写个if语句在运行时检查有没有启用某个Component,不算类型安全。

Expression Problem的定义就是需要类型安全啊。当然实践中有没有必要搞这么严格的类型安全是另外一回事。

http://t-machine.org/index.php/2007/09/03/entity-systems-are-the-future-of-mmog-development-part-1/

这里讨论了在服务器 (MMOG) 里使用 ecs 。

话说,动态语言没有什么是不能加入静态类型系统的,typescript证明了这一点。

@杨博
这里说的Entity,component也是unity那种component模式吗,那c#实现的component并没有放弃类型安全,仅需要一定反射机制。

云风有见过在服务端用ECS么?我感觉游戏副本用ECS挺合适的。

这种思想很新奇吗?不觉的啊?居然还整个ECS的名词出来好搞笑。搞个什么类似h264算法拿出来说道说道,搞点专利费什么的感觉还有点价值,这种类似的设计模式没必要显摆了。真心感觉没啥,就一个感觉,你的逻辑写的还是太少了,游戏逻辑不积累到一定的代码量是不容易有深科的感悟的,况且这是和逻辑息息相关的架构。

客户端并发需求不多,并发本身并不能降低时间复杂度。

如果需要并发,只需要用 C 结构来实现关键 Component ,支持 C 里面迭代就可以了。

用同一lua虚拟机不容易支持System并发啊

像 Lua 这种语言用闭包维护状态比在对象中维护爽。

ECS这个问题在学术界叫做Expression Problem。

这在动态类型语言中实现起来毫无难度;静态类型语言里如果放弃类型安全,动态查找Component,也没什么难度。

倒是C++那种基于多重继承,想要保证类型安全同时又要考虑Component之间的依赖不产生冲突,挺难的。

Post a comment

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