游戏编程模式--享元模式


享元模式

  享元模式是把数据分为两种类型,一种是不属于单一对象而是为所有对象共享的数据,GoF将其称为内部状态;而另一种数据则为单一对象独有的。比如我们要渲染很多的草和树,草和树的形状是共享的,每棵树的位置和大小等数据是对象唯一的。通过共享数据的使用来节省内存。

地形

  我们使用一个地形的例子来解释享元模式是如何工作的。假设在我们的虚拟世界中由三种地形:草地、山地、河流,而我们使用基于瓦片(tile-based)的技术来构建地形,每个瓦片由一种地形覆盖。对于每一种地形,都有一些影响着游戏玩法的属性:

  •   移动开销决定角色穿过这个地形所使用的时间
  •   用来决定是否是一片能够行驶船只的水域的标志位
  •   纹理,用来渲染地形

  先来看一种常用的实现方法,游戏开发人员为了高性能,通常不会为每一个瓦片保存状态,为每一种地形创建一个枚举类型,每个瓦片都拥有一种地形:

enum Terrain
{
    TERRAIN_GRASS,
    TERRAIN_HILL,
    TERRAIN_RIVER,
    //other terrains....
};

class world
{
private:
    Terrain tiles_[WIDTH][HEIGHT];
};

  这样,如果想获得瓦片的数据则使用这样的方式:

int World::getMovementCost(int x, int y)
{
    switch(tiles_[x][y])
    {
        case TERRAIN_GRASS:
            return 1;
        case TERRAIN_HILL:
            return 3;
        case TERRAIN_RIVER:
            return 2;
        //other Terrians..
    }
}

bool World::isWater(int x,int y)
{
    switch(tiles_[x][y])
    {
        case TERRAIN_GRASS:
            return false;
        case TERRAIN_HILL:
            return false;
        case TERRAIN_RIVER:
            return true;
        //other terrains...
    }
}

  这种做法是比较粗糙的,地形的数据分散在一个个的方法中,这与面向对象设计的原则不符。接下来我们看一种比较符合面向对象设计的做法:把地形的数据封装到一个地形类中,使用不同的数据实例化对应的地形对象,瓦片则引用这些地形对象即可。

  代码如下:

    class Texture
    {
    public:
        Texture(int id) :
            id_(id)
        {

        }
        int id_;
    };
    class Terrain
    {
    public:
        Terrain(int moveCost, bool isWater, Texture texture):
            moveCost_(moveCost),
            isWater_(isWater),
            texture_(texture)
        {

        }

        int getMoveCost()
        {
            return moveCost_;
        }
    private:
        int moveCost_;
        bool isWater_;
        Texture texture_;
    };

    class World
    {
    public:
        World() :
            grassTerrain_(1, false, Texture(1)),
            riverTerrain_(2, false, Texture(2)),
            hillTerrain_(3, false, Texture(3))
        {

        }

        void generateTerrain()
        {
            std::default_random_engine random;
            std::uniform_int_distribution<int> u(0, 9);
            for (int i = 0; i < kWidth; ++i)
            {
                for (int j = 0; j < kHeight; ++j)
                {
                    if (u(random) < 2)
                    {
                        titles_[i][j] = &hillTerrain_;
                    }
                    else
                    {
                        titles_[i][j] = &grassTerrain_;
                    }


                }
            }

            std::uniform_int_distribution<int> u100(0, kWidth-1);
            int x = u100(random);
            for (int i = 0; i < kHeight; ++i)
            {
                titles_[x][i] = &riverTerrain_;
            }
        }

        const Terrain& getTile(int x, int y) const
        {
            return *titles_[x][y];
        }

        
    private:
        const static int kWidth = 100;
        const static int kHeight = 100;
        Terrain* titles_[kWidth][kHeight];

        Terrain grassTerrain_;
        Terrain riverTerrain_;
        Terrain hillTerrain_;
    };

  对比之前的做法,我们可以看到其实就是枚举类型和指针的区别,有人可能会决定指针的性能会略慢,主要考虑的是指针是间接引用,需要查找地址,而且为了获取地形的数据,需要先找到对象,然后在根据对象查找其属性,在跟踪这样的指针过程中会引起缓存未命中,拖慢程序的速度。但现代计算机非常的复杂,性能的问题通常都不是单一因素引起的,所以如果你想为了性能而不使用享元模式时,最好先分析性能的瓶颈在哪,享元模式下,内存数据存放的好,同样可以获得很高的性能。

相关