转自: http://www.bakachu.cn/archives/222
所谓的ECS模式全称就是Entity-Component-System模式。很早就听说过unity等引擎中广泛地使用了这样的模式,没有细查。今天看了几篇文章之后有了些许了解,故记叙此文作为笔记。
一、问题提出
之前在写STG框架的时候遇到了这样的问题,以面向对象的思想对游戏对象进行抽象,那么可以实现一个基类GameObject。之后包括敌机、自机、子弹在内的所有对象都继承这个基类并进行实现。
于是很容易设计出这样一个基类:
classGameObject
{
private:
// === 基础属性 ===
fcyVec2 m_Position; // 位置
fcyVec2 m_Rotation; // 方向
fcyVec2 m_Scale; // 缩放
// === 碰撞属性 ===
fcyVec2 m_Size; // 大小
public:
voidUpdate(floatdelta);
voidRender(floatdelta);
};
之后,继承这个基类,分别实现GameBullet、GameEnemy……
这样带来的问题就是游戏对象的逻辑往往在超类中被定死了,而一旦需要对逻辑作出修改,要么重写实现,要么继承基类进行覆盖。此外,在C++中使用对象池优化时就会造成灾难性的后果——一种类型一个池(尽管可以用通用的内存分配器,但是这样还要考虑内存碎片等杂七杂八的问题)。
对于传统的设计思路,在游戏开发上就会导致“类灾难”。于是ECS模式被提了出来,用于解决继承带来的问题。
二、ECS的解决之道
使用继承去表述游戏对象和逻辑会造成逻辑混杂、维护扩展困难的问题。
既然继承出了问题,我们就用组合来解决吧。
于是在2002年的Game Dungeon Siege上,ECS模式被提了出来。
ECS全写即“实例-组件-系统”的设计模式。简言之,实例就是一个游戏对象实体,一个实体拥有众多的组件,而游戏系统则负责依据组件对实例做出更新。
举个例子,如果对象A需要实现碰撞和渲染,那么我们就给它加一个碰撞组件和一个渲染组件;如果对象B只需要渲染不需要碰撞,那么我们就给它加一个渲染组件即可。而在游戏循环中,每一个系统都会遍历一次对象,当渲染系统发现对象持有一个渲染组件时,就会根据渲染组件的数据来执行相应的渲染过程。同样的碰撞系统也是如此。
也就是说游戏对象需要什么就会给自己加一个组件。而系统会依据游戏对象增加了哪些组件来做出行为。换言之实例只需要持有必要的数据,由系统负责逻辑就行了。这也就是ECS模式能和数据驱动很好结合的一个原因。
于是在上述问题中所有的派生类都消失了,只留下了GameObject。
三、没有什么是完美的
虽然ECS模式可以让维护和扩展的成本降低——必要的时候你只要给对象增加组件、为游戏逻辑增加系统就可以扩展了。这种设计降低了耦合,也提高了可复用性。
但是很明显的,ECS带来了两个缺陷:
1、数据共享
比如渲染组件需要对象的位置信息、碰撞组件也要对象的位置信息,那么我们怎么解决这里的数据共享问题?
一种解决方法是把位置信息作为组件抽象出来,但是这样带来的问题就是效率会降低,在处理渲染和碰撞的时候都会再去存取一次位置信息。
另一种解决方法是使用引用(比如使用C++中的智能指针)在构建对象的时候分配同一位置对象,然后传递引用给其他组件。
2、遍历次数
当游戏中数量巨大并且系统数量也增加时,很明显整个算法的复杂度将不低于O(n*m),遍历对象的次数将变得巨大。
比如碰撞检测系统若是两两对象进行逻辑处理那么速度上的损失将是十分巨大的。
这里的一种解决方法是分集合处理,不再赘述。
四、参考文档
http://blog.lmorchard.com/2013/11/27/entity-component-system
http://piemaster.net/2011/07/entity-component-primer/
http://www.richardlord.net/blog/what-is-an-entity-framework
http://en.wikipedia.org/wiki/Entity_component_system
http://blog.csdn.net/i_dovelemon/article/details/27230719
【完】