关于ECS
OO和ECS的模式,并不需要分的太开。总体上,OO的模式,偏向于人类和生物对世界的概念及其创造物,偏高层偏对外偏应用实现。ECS的模式,偏向于无机世界及低级有机世界,偏底层偏对内偏系统实现。一般情况下,两者皆有,只是多少的问题
传统方式
- 从”是”到”能”再到”有”
对对象的抽象是整理代码的要点,继承是一种比较古老并常见的抽象,其描述了一个对象**”是”**什么,其中包含了对象拥有的属性和对象拥有的方法,在简单情况下,继承是一种非常易用易懂的抽象,然而在更复杂的情况下,继承引入的的问题渐渐浮现出来,使得它不再那么易用。
以下列举几个例子:
- 深层次继承树(要理解一个类,需要往上翻看非常多的类)。
- 强耦合(修改基类会影响到整棵子继承树)。
- 菱形继承(祖父的数据重复,方法产生二义性)。
- 繁重的父类(子类的方法被不断提取到父类,导致父类过度膨胀,某 UE4)。
- 而这些问题又相互影响产生恶性循环,使得项目的后期开发和优化变得无比困难。
于是,大家便尝试简化模型,并描述了一种叫做接口的抽象,其描述了一个对象”能”干什么,其中包含了对象拥有的方法(不再包含数据),接口隐藏了对象的大部分细节,使得对象变成一个黑箱,且展平了类结构(不再是树状),然而接口(这里指运行时接口而非泛型)作为一种非常高层次的抽象,这种抽象层次似乎有时会过高,导致CPU更难以理解代码,这点在稍后会讨论到。
类似的,在游戏开发中,面对大量的对象种类,大家又描述了一种组件的抽象,如 UE4 中的 Actor Component 模型和 Unity 中的 Entity Component 模型,其描述了一个对象**”有”**什么部分,其中对象本身不再拥有代码或数据(但其实 Unity 和 UE4 之类的并没有做到这么纯粹,对象本身依然带有大量”基础”功能,这导致了代码量和内存占用的双重膨胀)。组件的方式带来了优越的动态性,对象的状态完全由其拥有的组件决定(同样,一般没这么纯粹),甚至可以动态的改变。并且这让我们可以排列组合以少量的组件组合出巨量的对象(当然,有效组合往往没那么多)。有趣的是,从展平对象结构的角度看起来和组件和接口有着微妙的相似性。不过这种抽象也带来出了一些歧义性
- “有”和”能”和实现
在组件模型中,对象由组件组成,所以其行为也由组件主导,例如一个对象拥有[Movement] 和 [Location],则我们可以认为它能够移动,这在整体上是十分和谐自然的,但当我们仔细考量,这个**”能”是由于什么呢,是因为 [Movement]吗,是因为[Location]吗,还是同时因为 [Movement] 和 [Location]?当然是同时(这里便揭示出了组件和接口的展平对象方式是正交的),那移动的逻辑放到哪呢?答案是放在这个“切片“上。但在实际项目中会看到把逻辑放在 [Movement] 上的做法,这两种方式都是可取的,后一种拥有较为简单的实现并被广泛采用,而前一种拥有更精准的语义,更好的抽象**(后一种种方式中 [Movement] 去访问并修改了 [Location] 的数据,这破坏了一定的封闭性,且形成了耦合,当然这种耦合也有一定的好处,如避免只添加了 [Movement] 这种无意义的情况发生)。
ECS
关于组件模式,可以通过组件管理器来实现管理组件功能的代码,而使用“管理器”实现的方式,拥有更精准的语义和更好的抽象,组件之间被彻底解耦,而这个“管理器”我们称之为系统(System)。即系统负责管理特定的组件的组合,而组件则不再负责逻辑。
System
对象耦合于接口,而这里系统则耦合于对象。这意味着组件不变的情况下,系统的任何修改都不会对程序的其余部分造成影响。这给代码带来了出色的内聚性,让 culling 和 plugin 都变得更轻松,并且系统本身拥有很好的纯度,我们完全可以把系统看做是”输入上一帧的数据,输出下一帧的数据“。也就是系统本身贴合了函数式的思想,根据前面的叙述,函数式在并行上有天生的优势,这在系统上也体现了出来:系统负责管理组件的信息是透明的,于是我们对系统对组件的读写便一目了然 - 注意结构体之间没有任何依赖,系统与系统之间的冲突也一目了然。更进一步,在通常情况下,系统是一个白箱,运作系统的代码将不会经过虚函数,不管是效率还是可测试性都是极好的。甚至对于系统的执行调度也完全暴露了出来,这在实现网络同步之类的框架的时候能提供很大的便捷性。
Component / Entity
对于对象本身,其实已经不必要承载多少信息了,激进一点说,对象甚至只是一个唯一的ID,用于和其他对象区分而已,这让我们有机会去除那些”基础”功能的依赖(例如 Transform),使得内存和代码进一步压缩。而组件不包含逻辑,就只有数据,作为一个大的对象的分割的属性,通常为小结构体。对于每一种组件,我们可以使用紧密的数组来储存它,而这也意味着我们可以轻松的池化这个数组。在系统管理组件的时候,并不关心特定 Entity,而是在组件数据的切片上批量的连续的进行处理,这在理想情况下能大大的减少 Cache Miss 的情况。作为额外的好处,纯数据的组件对序列化,表格化有着极强的适应性,毕竟对象天生就是一个填着组件的表格,对网络、编辑、存档等都十分的友好。(这里也可以引入很多数据库相关的知识)
ECS & OOP
举个编程的例子,游戏引擎用于构建一个底层虚拟世界,其实现就便于用ECS模式,而GUI之类的,是上层与玩家交互信息工具和语言,其实现就是用OO模式,整体上是树状的,就算现在流行的Vue和React前端框架,在我看来也是OO的变体。