原文链接(英文): https://godotengine.org/article/why-isnt-godot-ecs-based-game-engine
以下为中文翻译
作者: Juan Linietsky 2月26 2021
关于
关于Godot为什么不使⽤ECS的话题时常出现,所以这篇⽂章将解释其背后的设计决策,同时也会对Godot的⼯作原理进⾏⼀些说明。
注意:为了避免不必要的误解,我对⽂章做了⼀些修改,以便强调这并不是⼀篇关于继承和组合谁更好或者⼀篇反ECS的引战⽂,它只是解释了Godot采⽤⽬前架构的理由。⽽且我不认为有⼀个普遍更好的⽅法来做这些事,所以如果你更喜欢⽤其他的⽅法或者对你来说更合适的架构,那很好。当然希望你阅读此⽂是因为有兴趣理解Godot架构背后的原因,⽽不是为了想要知道哪种⽅法更好。
什么是ECS?
ECS是⼀种在视频游戏领域常使⽤的设计模式(尽管在软件⾏业的其他领域并不常⽤),它包括有⼀个基础的实体(⼀个容器对象)以及可以在其上添加的组件。组件提供数据和整个世界的交互⼿段。最后,系统独⽴⼯作,并对每⼀个相似的组件进⾏操作
这种设计在2010年代初的游戏引擎和库中变得很常⻅。主要的吸引⼒(除了架构)是组件数据可以放在连续的内存中,改善了缓存访问。这是⼀种常⻅的⾯向数据的优化。
从架构上看,ECS的⽬标是通过偏重于组合来取代继承,类似于OOP中接⼝或多重继承的⼯作⽅式。ECS的关键优势在于组件是动态的(可以在运⾏时添加或删除)。
为什么Godot不使用ECS?
Godot通过提供包含有数据和逻辑的节点以更传统的OOP⽅式来使⽤,同时节点也⼤量使⽤了继承。不过节点依然能够做组合,只不过是以⼀种更⾼的层级来实现(构建的节点通常⽐传统的ECS组件具有更⾼的层级)
作为⼀个区别两者的例⼦,在典型的ECS中,⼀个Button实体可以有如下组件:
- 变换(Transform)
- 渲染器(Renderer)
- 事件处理器(EventHandler)
- 按钮(Button)
- ⾏为(Behavior)
当然为了让它更简单以及避免⼀些问题,⼀些实现会在添加其他组件时强制⼀些组件存在。
在Godot中,⼀个Button⾃身内则具有完整的继承链:
Node -> CanvasItem -> Control -> Button -> Behavior Script
⼀个更复杂的例⼦是⼀个带有精灵图的刚体,在典型的ECS中,这被构建成⼀个包含有如下组件的实体:
- 变换(Transform)
- 刚体(RigidBody)
- 碰撞体(Collider)
- 精灵图(Sprite)
在Godot中,则会更加复杂。你需要3个节点及层级:
RigidBody (Node -> Node2D -> PhysicsBody2D -> RigidBody2D)
Sprite (Node -> Node2D -> Sprite)
CollisionShape (Node -> Node2D -> CollisionShape)
所以,乍⼀看Godot的做法似乎⽐较浪费,但真的是这样吗?
- 节点是轻量的,类似于组件。
- Node2D包含了2D变换(2D Transform),类似于ECS中的变换组件。在ECS中只需⼀个组件就⾜够了然⽽(在Godot)中却需要三个节点,这看起来似乎挺浪费,但真的是这样吗?在实体中,碰撞体和精灵图很可能不会居中,仍然需要偏移和旋转属性,所以最后其实没有什么区别。
- 在Godot中添加更多的这些节点(多个精灵图和碰撞体)算是挺⾃由的,变换偏移会很⾃然的发⽣。在ECS中,则需要特殊的逻辑来处理这种情况。
由此可⻅,在Godot中,继承和组合其实可以共存的,并且也讲的通。
主要的区别可以总结为:
相⽐较于传统ECS,Godot是在更⾼层级上做组合
这在架构和性能上带来两个根本性的区别
架构
从架构上看,这将导致ECS的⼯作⽅式发⽣重⼤变化:
继承更加明确
由于倾向于继承(相较于ECS中组件间不明确的关系),现在这些关系在继承链中更加明确。
这使得(对Godot⽤户来说)只需看⼀下继承树,就能⾮常容易地理解场景节点是如何组织的,以及它们能做什么。
场景作⽤更加明确
由于组合发⽣在⼀个更⾼的层级(节点层级),Godot⽤户也很容易通过观察场景树来了解⼀个场景的具体作⽤。
可重复使⽤性得到提⾼
由于所有的东⻄都是节点(⽽不是带有组件的实体),因此尽可能地重⽤和组合所有的东⻄变得更加容易。这就是为什么Godot没有prefabs的概念以及它和场景间的区别。
此外,由于拥有更⾼层级(概念)的节点,Godot没有 “场景设置 “的概念,因为这些也是通过节点来完成的。
由此的结果便是⽐传统的ECS⽅式具有更好的做组合的能⼒。
ECS通常的的组合形式:Components -> Entity -> Prefab -> Scene -> World
Godot则只有节点,⽽场景不过是被保存成⽂件的⼀连串节点组成的树。(⽽且)场景可以被实例化和继承,灵活性要⽐其他⼤多数⼯具⾼。
优化
抛开架构上的偏好,这在优化⽅⾯似乎是个问题。ECS最⼤的优势之⼀是系统(⾯向数据)部分,它允许在线性内存中组织运⾏很多相似组件的数据。这⽐Godot的节点⼯作⽅式带来了巨⼤的性能提升。
再⼀次,这⾥的关键是,去理解在Godot中,相较于传统的ECS实现,场景和节点在⼀个更⾼层级⾯上在操作。
这可以通过分别研究引擎和游戏逻辑部分来理解。
引擎部分
Godot在物理、渲染、⾳频等⽅⾯使⽤了⼤量⾯向数据的优化。然⽽,它们是独⽴的系统,完全隔离的。
⼤多数(如果不是全部的话)利⽤ECS的技术都是在核⼼引擎层⾯上进⾏的,并且作为基础架构,在其上构建其他⼀切(物理、渲染、⾳频等)。
⽽Godot则是将这些⼦系统全部独⽴隔离(并装在Servers⾥⾯)。我发现这使得代码更简单,更容易维护和优化(⼀个证明是,与其他游戏引擎相⽐,Godot的代码库虽然相当的⼩,却依然提供了相似⽔准的功能)。
与传统的ECS系统相⽐,Godot中的场景系统(节点串)⼀般具有更⾼的层级。⼤部分的事情都是通过信号回调来发⽣的(⽐如,物体碰撞了,某个东⻄需要重新绘制,按钮被按下了等等)。在Godot中,⽤户端每⼀帧都需要处理某件事情的情况⾮常少⻅,因为引擎会在内部进⾏管理,使得⽤户从这些复杂性中脱身。
简单来说,节点只是Servers内部处理实际数据的接⼝,⽽在ECS中,实际的实体才是被系统处理的东⻄。
换句话说,Godot作为⼀个引擎,试图将处理的负担从⽤户身上移开,⽽将重点放在决定事件发⽣时该怎么做。这确保了⽤户不需要过多关注优化从⽽能写出更多的游戏代码,也是Godot试图传达的关于应该构成⼀个易于使⽤的游戏引擎的愿景的⼀部分。
游戏逻辑
不过(虽然,到⽬前为⽌,还不是⼤多数)有些类型的游戏在游戏逻辑端使⽤ECS后,会有性能上的提升。
这类游戏⼀般都需要在⼏⼗上千个对象上处理游戏逻辑,在这种情况下,⾯向数据的优化成为必须,因为移动到CPU缓存中的⻚⾯数量会增加⼏个数量级,严重影响性能(以及移动设备上电池的使⽤情况)。
我再次强调⼀点,虽然这种⽤例很少(相⽐之下,⼤多数游戏⼀般最多只有⼏百个对象,对于这些游戏来说,如果不进⾏缓存优化,内存的访问速度会远远不够),但需要进⾏缓存优化的游戏是真实存在的,⽽且确实存在。
这类游戏的例⼦有:
- 城市建设类游戏(有很多事情要做)。
- 沙盒(每⼀帧都需要处理很多微⼩的事情)。
- ⼀些策略游戏(虽然不是⼤多数,但有些会同时使⽤⼏千或⼗⼏万个游戏单元)。
- 其他⼀些有很多内容在进⾏的3A游戏
那么,这是否意味着这些类型的游戏不能⽤Godot制作呢?
答案是,你仍然可以做任何你想做的游戏,但你需要⽤不同的⽅式来做。
使⽤巧妙的优化
在你开始迎头蛮⼲之前,很多时候你还可以依靠巧妙的优化。好奇过《模拟城市》是如何能够在 Commodore 64上跑起来的吗?其实它是通过交替处理每⼀帧中Tiles来实现的,所以它永远不需要同时处理成千上万的Tiles(显然对6502 CPU来说是不现实的)。
优化的⽅式有很多种,从只处理每帧中需要处理的,到更优先处理所有可⻅的内容(或接近摄像机的),通常情况下,这些优化会将需要处理的对象数量减少⼏个数量级,并且仍然⽐完全⾯向数据的⽅式表现更好。
Godot提供了诸如VisibilityEnabler这样的节点来帮助你实现这⼀点,⽽即将到来的Godot 4.0 禁⽤场景树中的部分内容有着更精细的控制。
不过,这些对你的游戏来说可能还是不够,或者太麻烦了。在这种情况下,你可能需要直接跳过场景优化:
直接使⽤Servers
如前所述,Godot将引擎⼤部分的⾼性能/底层级的部分放在了Servers中。Servers 的 API 则完全暴露在外,允许你在很低的层级上控制整个引擎。
⼤多数时候,即使通过GDScript、C#或者C++实现的GDNative(或模块)来使⽤Servers,对于上述类型的游戏来说也是绰绰有余的。虽然它们可能需要⾼性能的代码来实现核⼼游戏循环,但逻辑很少会复杂到需要⼀个完整的框架来实现。
这类似于⾃⼰⽤SDL/OpenGL来写游戏,只是引擎的其他部分仍然可以使⽤其他所有的⾮游戏专⻔的代码。(⽐如UI、IO、保存、⽹络等。换句话说,游戏剩下的90%的部分)。
使⽤通⽤计算
现今通⽤计算(GPGPU)⼏乎⾮常普遍了(被桌⾯和移动设备所⽀持),并且可以实现巨⼤的灵活性和性能优化。Godot 4.0将会包括易于使⽤的,⽤于⾼度并⾏任务计算的⽀持。
使⽤ECS
没有什么能阻⽌你在Godot中使⽤ECS解决⽅案。事实上,我强烈建议查看Andrea Catania 在 Godex 上的精彩⼯作,它的⽬的是带来⼀个⾼性能的ECS可插拔的实现。
未来
对于Godot的未来版本,我们正在评估(可选的以备在你真正需要的时候)以缓存友好的⽅式处理相似的节点,并以类似ECS处理系统的⽅式分离数据和逻辑,当然是以适应于Godot的⼯作⽅式。毕竟,这些类型的优化不是ECS独有的。
总结
虽然旨在同时解决⼏个问题的架构和设计模式对程序员来说是⾮常有诱惑⼒,但Godot是以⾮常务实的⽅式开发的。
虽然ECS确实给我们带来了优势,但能客观证明它对Godot的⽬标有益的案例⾮常有限,所以很难(⾄少对我们来说)证明架构的改变是合理的,因为它们并不普遍(⽽且通常可以通过其他更简单的⼿段实现优化)。
Godot选择了⼀条以⽤户友好和可扩展为优先的道路,⽽且我们明⽩,对于⼤多数情况,由于引擎的设计⽅式,需要极端游戏逻辑性能的情况⾮常少(因为引擎承担了⼤部分的重任)。不过如果有这种需求,Godot的设计还是会在这⽅⾯给你答案,⽽且随着时间的推移,更多的选择会不断出现。