说明
这是一篇学习笔记,整理了我在学习过程中看到的文章,引用了其中大量的观点并结合自己的理解。在文章中可能存在一定的错误或者理解不到位,希望指出。
关于ECS属于架构还是模式,在各种的贴子里对ECS的说法都不统一,本文将ECS称为架构而不是一种模式,因为我倾向与将设计模式归纳为代码中的定式,技巧。如单例模式、观察者模式、桥接模式之类的,而对于MVC,ECS我倾向归类为一种思想,所以我将其归类为一种架构。
什么是ECS设计模式?
ECS,即E-Entity-实体、C-Component-组件、S-System-系统。在ECS架构中,组件只保存状态而不具有行为,实体中挂载着组件,最后由系统负责获取相关组件完成对应运算,其核心思想就是对数据与运算的分离。对于系统只有方法,对于组件只有成员变量。
(图片无法上传-此处基本结构图)
** ECS从何而来?**
在具体了解ECS是什么之前,我需要先知道为什么会有ECS这种设计模式,或者说传统的开发中遇到了什么问题而需要提出一种新的模式。
在GDC上有一期来自暴雪开发者分享的名为《守望先锋的游戏架构和网络代码》的讲座,分享中认为ECS架构能够管理一个快速增长的代码库中的复杂度。可以感觉的到,守望先锋团队选择ECS架构很大的一部分原因就是在传统的模式下,游戏对象间的耦合变的难以处理。
在传统的OOP编程里,我们会先对游戏对象进行抽象出一个基类,将有共同属性的对象继承到到同一父类或接口中。所以在开发中,基类往往是难以修改的,而一旦要对逻辑做出修改,要么重写,要么继承基类进行覆盖,要么盲目的扩大基类的规模随着开发的进行,就有可能出现大量不必要的子类出现。
另一方面,传统的OOP随着工程的臃肿随之而来的就是效率的低下,而ECS模式强调面向数据编程(DOP思想)保证了数据空间上的连续,利用了CPU Cache而提高了效率。
这也是随着OOP(面向对象)的发展必然会面对的问题,可以说ECS基于组合优于基于继承的原则在一定程度上规避的系统中不必要的耦合,提高了效率,使其有了存在的必要性。
数据与行为的分离
数据与行为的分离是ECS模式的最主要的设计原则,在ECS中,实体不是最先被构造出来的,我们通过生活经验也能发现,不同的系统对同一个对象是会有不同的表达的,比如对于一个未成年来说,老师需要对他进行指导,他的家长需要对他的生活起居负责,当他去了一家店铺,店铺的老板需要知道他的需求。所以同样的,我们需要构建一个系统的话,我们需要先明白这个系统负责了哪些功能,通过这些功能我们来抽象出对应的组件,还是对于上述例子来说,老师需要根据他的成绩来决定他的指导方式,家长需要知道他的肚子饿了没,他有没有住够的日常开销费用,对于商家需要知道他身上的现金能否进行消费。
所以在对观察者进行抽象之后,我们可以很自然的给他挂载上诸如成绩,饥饿,现金等属性,老师会选择他的成绩组件,家长会选择他饥饿组件与现金组件,商家会选择他的现金组件,他的组件就自然的被不同系统选择并且产生了不同的表达。
总而言之,不同系统系统面对同一个对象会进行不同的表达,通过这样的抽象能更容易的完整表达一个对象,并且保持良好的拓展性。
为什么高效?
ECS为什么高效,前面已经简单的提到ECS是基于DOP的,ECS性能上的优势也基本基于此。
减少cache miss
基本上,我们可以将CPU访问数据的过程理解为,数据逐级的寻找CPU Cache,如果没有就把数据拷贝进缓存中,而对于ECS数据的处理是以System为单位,这使得CPU往往能一次性写入尽可能多的数据在缓存中,而减少了CPU对缓存访问时的Miss。
Unity中的ECS
Unity对ECS的支持并不友好,甚至于2018版本之前,ECS都不能正常使用。而在之后的更新中,Unity推出了DOTS即面向数据的技术堆栈,核心的部分为JobSystem(多线程处理复杂计算)与Burst(代码生成器和编译器)但仍然不是十分完善。
什么是DOP思想?
最后来记录下DOP的思想,其实这一部分应该放在开头的部分。ECS是基于DOP的思想而不是OOP的思想,是一种面向数据的编程思想。与传统的“类-继承”的关系不同,DOP的思想有些类似组件式的编程,这样的好处是在上手之后能降低团队的协作成本,因为每个人都只用关心自己负责的模块,这也就是解耦之后带来的好处。
另一点,在认识ECS之前,我都没有想过面向对象之外的设计思想,这种突破边界,打破常规的过程也十分重要。