欢迎光临散文网 会员登陆 & 注册

游戏编程模式(四):原型模式和单例模式

2023-07-25 21:24 作者:宁牁兒  | 我要投稿

原型模式

原型模式是一种对象创建型模式,它是使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。

它的工作原理很简单:将一个原型对象传给要发动创建的对象(即客户端对象),这个要发动创建的对象通过请求原型对象复制自己来实现创建过程。

在Java和C#中,对象类中的clone()方法就是原型模式的应用,在游戏开发中,考虑一个怪物生成的案例,不利用原型模式的代码将会是:

class Spawner  

{  

public:  

virtual ~Spawner() {}  

virtual Monster* spawnMonster() = 0;  

};  

class GhostSpawner : public Spawner  

{  

public:  

virtual Monster* spawnMonster()  

{  

return new Ghost();  

}  

};  

class DemonSpawner : public Spawner  

{  

public:  

virtual Monster* spawnMonster()  

{  

return new Demon();  

}  

};

我们可以把clone()方法放入Monster类中,使其可以生成出一个自己的副本:

class Monster  

{  

public:  

virtual ~Monster() {}  

virtual Monster* clone() = 0;  

// ……  

};


class Ghost : public Monster {  

public:  

Ghost(int health, int speed)  

: health_(health),  

speed_(speed)  

{}  

virtual Monster* clone()  

{  

return new Ghost(health_, speed_);  

}  

private:  

int health_;  

int speed_;  

};

将拥有clone()方法的原型类送入客户端对象(spawner类):

class Spawner  

{  

public:  

Spawner(Monster* prototype)  

: prototype_(prototype)  

{}  

Monster* spawnMonster()  

{  

return prototype_->clone();  

}  

private:  

Monster* prototype_;  

};

当然,在实际开发中,通常需要注意clone()方法具体是做浅拷贝还是深拷贝,这个点在一般的软件开发中都会遇到,这里打算只是简单介绍原型模式的基本思想,就不继续深入讨论了。

单例模式

单例模式在软件开发中出现率太过于频繁,以至于我不打算集中注意力去讨论如何使用它,而是讨论如何避免使用它。因为尽管它确实非常方便,但在游戏开发中,更应该谨慎地使用这个模式。

GoF中这样描述单例模式:

保证一个类只有一个实例,并且提供了访问该实例的全局访问点。

快速过一遍它的最简单的经典实现方案(当然此处不打算讨论线程安全的实现方案):

class FileSystem  

{  

public:  

static FileSystem& instance()  

{  

// 惰性初始化(非线程安全)

if (instance_ == NULL) instance_ = new FileSystem();  

return *instance_;  

}  

private:  

FileSystem() {}  

static FileSystem* instance_;  

};

很明显,用单例模式的最大好处就是该单例类在任何需要的地方都可用,而无需笨重地到处传递,在很多只需要一个实例的场景中,也保证了不会因不小心创建了多个实例而造成混乱。但是,我们需要考虑它可能带来的各种麻烦事:

一、它是一个全局变量

  • 降低代码的可读性。要理解一个单例类在某个方法中干了些啥,得追踪整个代码库来搜寻什么修改了全局变量

  • 增加了耦合性。全局变量很容易导致不小心在某处将两块不相干的模块耦合起来

  • 多线程不友好。将某个变量转化为全局变量时,就等于创建了一块每个线程都能访问的内存,要保证这块内存的线程安全性就会变得很困难,竞争状态、死锁、线程同步出现故障的概率将大大提升

二、实例的数量被严格约束

单例模式当然是只用来创建唯一实例的,比如日志类,为了避免日志类在众多方法中传来传去,单例模式确实是一个很好的解决方式。但是,这也使得单例类只能有唯一的一个实例,假如我们需要将日志分类记录,它将不再允许我们创建多个实例。

三、惰性初始化剥夺了控制权

在一般的软件开发中,惰性初始化确实可以帮助节省内存,只在我们需要它的时候才会占用内存。但对于游戏这种对优化要求程序非常高的应用来说,惰性初始化可能会导致降低游戏体验。例如,游玩中在达到一个高潮阶段时,可能会出现大量的画面渲染、音乐播放等需求,它们都可能是首次被调用,如果放任惰性初始化不管,此时就可能会有十万百万千万个实例被同时初始化,这将导致肉眼可见的掉帧和断续。


好,单例模式确实会带来一些问题,所以使用它就需要在一些方面做出权衡,我经常会在游戏源码中见到各种“管理器”类,开发者想要用这些类去管理其它对象。比如,怪物管理器类、粒子管理器类、声音管理器类,甚至,管理器管理器类,例:

class BulletManager  

{  

public:  

Bullet* create(int x, int y)  

{  

Bullet* bullet = new Bullet();  

bullet->setX(x);  

bullet->setY(y);  

return bullet;  

}  

bool isOnScreen(Bullet& bullet)  

{  

return bullet.getX() >= 0 &&  

bullet.getX() < SCREEN_WIDTH &&  

bullet.getY() >= 0 &&  

bullet.getY() < SCREEN_HEIGHT;  

}

...

}

像是上面这种Manager类纯属多余,这属于开发者对OOP的不熟悉,完全可以将它的功能在Bullet类本身中实现:

class Bullet  

{  

public:  

Bullet(int x, int y) : x_(x), y_(y) {}  

bool isOnScreen()  

{  

return x_ >= 0 && x_ < SCREEN_WIDTH &&  

y_ >= 0 && y_ < SCREEN_HEIGHT;  

}  

...  

private:  

int x_, y_;  

};

关于访问权限的控制,考虑两个案例,一、从基类中获取到单例对象。二、将各个单例类合并到一个单例类中,而不必真正将它们单例化:

一、从基类中获取到单例对象

很多游戏引擎中都会有GameObject基类,我们可以利用这点来从GameObject类中获取单例对象:

class GameObject  

{  

protected:  

Log& getLog() { return log_; }  

private:  

static Log& log_;  

};  

class Enemy : public GameObject  

{  

void doSomething()  

{  

getLog().write("log!");  

}  

};

这保证任何GameObject之外的代码都不能接触Log对象,但是每个派生的实体确实能使用getLog()

二、合并到一个单例类中

创建一个代表整个游戏状态的Game类,让这个全局对象捎带上其它类,来减少全局变量类的数量,而不必让Log,FileSystem和AudioPlayer都变成单例:

class Game  

{  

public:  

static Game& instance() { return instance_; }  

// 设置log_, et. al. ……  

Log& getLog() { return *log_; }  

FileSystem& getFileSystem() { return *fileSystem_; }  

AudioPlayer& getAudioPlayer() { return *audioPlayer_; }  

private:  

static Game instance_;  

Log *log_;  

FileSystem *fileSystem_;  

AudioPlayer *audioPlayer_;  

};

...

Game::instance().getAudioPlayer().play(VERY_LOUD_BANG);


游戏编程模式(四):原型模式和单例模式的评论 (共 条)

分享到微博请遵守国家法律