游戏编程模式之享元模式
“使用共享以高效地支持大量的细粒度对象”(摘自《游戏编程模式》第三章)
- 从这一句话可以看出,享元模式重要的目的是关于代码的高效性。而一般提升效率的本质则是减小计算量、工作量。例如:
- LOD:层次细节技术,它降低远处的渲染质量来减小计算机的工作量
- 比较的二分法:利用规律减少比较次数
- 理解上述问题是比较简单的。而享元模式,它减小计算量的手段是:通过对一个集合的公共部分的抽离,让公共部分共享一份数据模型,避免了工作的重复,以提高效率
引例:GPU对森林的渲染
- 关于森林中的每一棵树,我们都有如下性质并封装成类:
class Tree { private: Mesh m_Mesh; Texture m_Bark; Texture m_Leaves; Vector m_Position; double m_Height; double m_Thickness; Color m_BartTint; Color m_LeaveTint; }
- 在渲染中,如果我们将上述类实例化作为对象,则需要向GPU传输对象的数据。在一帧内传输至GPU是不太可行的。(Mesh和Texture数据量很大)
- 我们可以发现,一个树其实是有公共部分的:它们分别是:Mesh、Bark、Leaves。因此,在类的设计时,我们可以将公共部分抽离成一个类,并采用C++组合的关系。
class TreeModel { private: Mesh m_Mesh; Texture m_Bark; Texture m_Leaves; } class Tree { private: TreeModel* m_Model; Vector m_Position; double m_Height; double m_Thickness; Color m_BartTint; Color m_LeaveTint; }
在C++中,类与类关系主要有两种,一个是继承,一个是组合。继承是拷贝,即有size(子类) ≈ size(父类)+ size(差异)。而对于组合,是在类中定义一个类的指针,以达到have one 的效果。size(组合类)=size(差异)+size(ptr) (ptr为对象指针)
- 因此,在本次渲染工作中,仅需要提供两组数据。一组为通用数据:TreeModel,第二组就是实例列表以及差异化参数。(关于这次渲染涉及到实例绘制,这里仅给出名词)
享元模式
- 享元模式通过将对象数据切分为两种类型来解决问题。第一类为不属于单一对象实例并且能够被对象共享的数据,称为内部状态(the intrinsic state);其他数据称为外部状态(the extrinsic state)
这两个词或许只是GoF小组(书的作者团队)所命名
拓展
-
游戏中将使用基于瓦片(Title-base)的技术来构建地面
-
例子中,地面的属性有:
- 移动开销
- 是否是一片能够行驶船只的水域的标志位
-
第一种解决方案:枚举作为返回标志
enum Terrain { TERRAIN_GRASS, TERRAIN_HILL, TERRAIN_RIVER }; class World { public: int getMovementCost(int x,int y); bool isWater(int x,int y); private: Terrain m_Tiles[WIDTH][HEIGHT]; //瓦片二维数组 } int Terrain::getMovementCost(int x,int y) { switch(m_Tiles[x][y]) { case TERRAIN_GRASS: return grassCost; reak; case TERRAIN_HILL: break; } } ……
- 这一方法的缺陷在于:代码在组织上比较简陋,因为地形和判断方法没有组织在一起。一个整体被分成了两个部分。
-
如同上述的森林渲染一样,设计一个类,每一个地形为一个类,它保证了整体完整。但这又回到了引例中的问题。
class Terrain { public: Terrain(int movementCost,bool isWater,Texture texture): int getMoveCost() const {return m_MoveCost;} bool isWater() const {return m_IsWater; } const Texture& getTexture() const { return m_Texture; } private: int m_MoveConst; bool m_IsWater; Texture m_Texture; }
-
我们学习引例的做法:将数据拆分成内部数据和外部数据。例子中,三个数据均为内部数据。并对复用数据进行 have one的设计
class World { private: Terrain* m_Tiles[WIDTH][HEIGHT]; //地形对象的网格指针 public World() : m_GrassTerrain(1,fase,GRASS_TEXTURE),m_HillTerrain(2,fasle,HILL_TEXTURE),m_RiverTerrain(3,true,RIVER_TEXTURE){} //初始化瓦片 bool generateTerrain() { m_Tiles[1][1]=&m_RiverTerrain; …… } //have one 设计 Terrain m_GrassTerrain; Terrain m_HillTerrain; Terrain m_RiverTerrain; }
总结
- 享元模式关键字在于享,把一个对象的公共数据资源抽离成一个实例,因此所有的对象都可以公用这一个实例,减少了计算量。
- 如果你发现自己创建了一个枚举,并使用switch,那么你可以考虑使用这一模式。
- 继承和组合是类的两种关系,我们要学习在特定情况下如何选择。
知识点全部源于《Game Programming Patterns》,中文名《游戏编程模式》,图片来源于网络