最近看到同事有在用ECS的思想写框架,抽时间整理了下网上的资料,大致理解这个ECS框架,写下这篇博客做下工作随笔
从最简单的游戏模型说起
最简单的游戏模型一般是实体Entity(unity内的GameObject)的行为和他们之间的交互和管理Manager
但一般游戏不可能这么简单,在unity里 你会在你的GameObject上挂上各种组件(Component),组件里面有各种逻辑,逻辑拆分组成各种不同的组件(Component),同时你的Manager也不可能只有一个,你会写出各种Manager,什么SoundManager,ResManager等等
最后一般都会成为这样一种状态
同时根据OOP的思想,你在编写Component时,抽象,封装,继承,等等一系列操作,但也会带来一些问题
从abstract说起
对对象的抽象是整理代码的重点,继承是一种非常常见的抽象,描述一个对象“是”什么
其中可能包含了对象拥有的属性和对象拥有的方法
在简单情况下继承是一种非常易用易懂的抽象
BUT,在复杂情况下却有各种各样的问题
- (1) 深层继承树(理解一个类,可能要向上翻看非常多的类)
- (2) 强耦合(修改基类会影响整颗继承树)
- (3) 繁重的父类(子类方法不断提取抽象,会导致父类过度膨胀)
这些问题会随着代码量的不断扩大会产生相互的影响和和恶性循环
interface
于是人们便尝试简化模型,描述一个叫接口(interface)的抽象,描述一个对象“能”干什么,其中包含了对象拥有的方法,不再含有数据,同时隐藏了大部分细节,然而高层次的抽象导致CPU更难以理解代码
ECS的产生
相类似的,在面对大量的对象种类,人们描述了一种组件的抽象,描述了一个对象“有”什么
对象(Entity)或者unity内的GameObject本身不在拥有代码或者数据,带来了优越的动态性,对象完全又其所拥有的组件决定
对于对象(Entity)本身,其实已经不必承载多少信息,在不同的数据结构里,甚至可以是一个id,一个int值,用途也仅限于和其他对象区分而已
同时组件内的逻辑移除(用于描述对象“有”什么,仅需含有数据),而当组件不包含逻辑,只含有数据时,组件作为一个大的对象的一小部分,通常可以是一个小结构体
这样做的好处
ECS相对传统的OOP
oop:面向对象,我是什么我有属性,我有方法,我要继承后重写成自己独有
ecs:面向组件,我有什么,我有组件便有这个功能,我由很多组件组成
在传统的OOP的设计中,游戏里的每一个“thing”,无论是坦克、子弹、灯光还是音效,都是由它自己的类定义的。当然,有些类之间有很多相似之处,因此它们可以从更抽象的类继承公共行为。例如,一辆坦克和一辆汽车是非常相似的:它们都在周围行驶,受到物理的影响,有一些视觉形式,偶尔也会发出声音。这种行为对所有车辆都很常见,因此可以从抽象车辆类继承。
这非常直观,相对有效,并且允许程序员通过继承车辆的相同行为并将其分配给卡车模型来添加卡车。当然,车辆和弹丸都受到物理的影响,所以两个抽象类都可以继承一个更抽象的物理对象的属性和行为,等等。
面向对象游戏的设计需要这个类层次结构的详细设计,每个具体的游戏内对象都继承自一个越来越抽象的类树,如果这个层次结构在实现之前就已经计划好了,那么很可能构建一个利用OOP继承的大型复杂游戏。毕竟,大多数游戏都是这样构建的。
然而,类层次结构越深,它就越脆弱。如果在实现开始后需求发生变化,反映代码中的这些变化通常需要向那些抽象类中添加或者删除功能。这些更改将继续影响所有子类,不管这些子类是否需要更改。结果是混乱的代码添加被推到(倒过来的)树的根上,以便为更多的子类提供更改。
在ECS的架构中, 只对具体的“东西”感兴趣——坦克和汽车,而不是抽象的“车辆”,你拥有的任何游戏对象层次结构都应该是几乎完全平坦的。
实体(Entity)代表世界上一个具体的“东西”,比如坦克。然而,Entity没有特定于坦克的逻辑;实际上,一个Entity几乎没有任何逻辑,而且仅仅是一个ID。
组件(Component)是功能模块,如果你愿意,可以称它为属性(attribute)。Component是Entity具有的东西,比如位置、速度、RenderableMesh和物理体。Entity只不过是一袋Components, 它是各部分的总和。重要的是,Entity不清楚它包含哪些部分,这意味着所有Entity都可以被游戏的其他部分以同样的方式对待。
这是可能的,因为组Component自己负责,而不管它们属于哪个Entity。例如,RenderableMesh组件包含呈现3D模型的功能。模型被分配给Component,Component被分配给实体,所以Entity不需要知道模型是什么样子。Entity所需要做的就是对它的每一个Component,每一帧调用一个通用的更新函数,每个Component都会做自己的事情。例如,RenderableMesh就可以把自己绘制到显示器上。
这样一个系统的优点是Component是通用的,它们只执行一个角色,不管它们的父Entity是什么,它们都以相同的方式执行。因此,坦克物体的RenderableMesh会像汽车物体的RenderableMesh那样画自己,唯一的区别就是分配给每个部件的网格的形状。因此,通过将不同的可重用Component插入到空Entity中,可以很容易地制造出不同类型的Entity。
对于在开发期间和之后保持灵活性来说,这是一个好消息!对Entity的更改通常涉及隔离地更改一个或两个Component,而不更改任何不相关的Component或污染其他Entity。新功能可以通过独立添加新Component来添加。
同时传统游戏引擎多是基于面向对象设计,游戏对象会有一个update的方法,unity内还有fixedupdate,lateupdate等,unity会遍历所有继承MonoBehaviour对象,在每一帧的不同时机去掉用, 同时MonoBehaviour还有复杂的继承关系
资料参考
https://zhuanlan.zhihu.com/p/32787878
https://blog.csdn.net/u010019717/article/details/80378385
https://blog.csdn.net/yuliying/article/details/48625257
https://zhuanlan.zhihu.com/p/41652478