游戲編程模式(四):原型模式和單例模式
原型模式
原型模式是一種對象創(chuàng)建型模式,它是使用原型實例指定待創(chuàng)建對象的類型,并且通過復(fù)制這個原型來創(chuàng)建新的對象。
它的工作原理很簡單:將一個原型對象傳給要發(fā)動創(chuàng)建的對象(即客戶端對象),這個要發(fā)動創(chuàng)建的對象通過請求原型對象復(fù)制自己來實現(xiàn)創(chuàng)建過程。
在Java和C#中,對象類中的clone()方法就是原型模式的應(yīng)用,在游戲開發(fā)中,考慮一個怪物生成的案例,不利用原型模式的代碼將會是:
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_;??
};
當(dāng)然,在實際開發(fā)中,通常需要注意clone()方法具體是做淺拷貝還是深拷貝,這個點在一般的軟件開發(fā)中都會遇到,這里打算只是簡單介紹原型模式的基本思想,就不繼續(xù)深入討論了。
單例模式
單例模式在軟件開發(fā)中出現(xiàn)率太過于頻繁,以至于我不打算集中注意力去討論如何使用它,而是討論如何避免使用它。因為盡管它確實非常方便,但在游戲開發(fā)中,更應(yīng)該謹(jǐn)慎地使用這個模式。
GoF中這樣描述單例模式:
保證一個類只有一個實例,并且提供了訪問該實例的全局訪問點。
快速過一遍它的最簡單的經(jīng)典實現(xiàn)方案(當(dāng)然此處不打算討論線程安全的實現(xiàn)方案):
class FileSystem??
{??
public:??
static FileSystem& instance()??
{??
// 惰性初始化(非線程安全)
if (instance_ == NULL) instance_ = new FileSystem();??
return *instance_;??
}??
private:??
FileSystem() {}??
static FileSystem* instance_;??
};
很明顯,用單例模式的最大好處就是該單例類在任何需要的地方都可用,而無需笨重地到處傳遞,在很多只需要一個實例的場景中,也保證了不會因不小心創(chuàng)建了多個實例而造成混亂。但是,我們需要考慮它可能帶來的各種麻煩事:
一、它是一個全局變量
降低代碼的可讀性。要理解一個單例類在某個方法中干了些啥,得追蹤整個代碼庫來搜尋什么修改了全局變量
增加了耦合性。全局變量很容易導(dǎo)致不小心在某處將兩塊不相干的模塊耦合起來
多線程不友好。將某個變量轉(zhuǎn)化為全局變量時,就等于創(chuàng)建了一塊每個線程都能訪問的內(nèi)存,要保證這塊內(nèi)存的線程安全性就會變得很困難,競爭狀態(tài)、死鎖、線程同步出現(xiàn)故障的概率將大大提升
二、實例的數(shù)量被嚴(yán)格約束
單例模式當(dāng)然是只用來創(chuàng)建唯一實例的,比如日志類,為了避免日志類在眾多方法中傳來傳去,單例模式確實是一個很好的解決方式。但是,這也使得單例類只能有唯一的一個實例,假如我們需要將日志分類記錄,它將不再允許我們創(chuàng)建多個實例。
三、惰性初始化剝奪了控制權(quán)
在一般的軟件開發(fā)中,惰性初始化確實可以幫助節(jié)省內(nèi)存,只在我們需要它的時候才會占用內(nèi)存。但對于游戲這種對優(yōu)化要求程序非常高的應(yīng)用來說,惰性初始化可能會導(dǎo)致降低游戲體驗。例如,游玩中在達(dá)到一個高潮階段時,可能會出現(xiàn)大量的畫面渲染、音樂播放等需求,它們都可能是首次被調(diào)用,如果放任惰性初始化不管,此時就可能會有十萬百萬千萬個實例被同時初始化,這將導(dǎo)致肉眼可見的掉幀和斷續(xù)。
好,單例模式確實會帶來一些問題,所以使用它就需要在一些方面做出權(quán)衡,我經(jīng)常會在游戲源碼中見到各種“管理器”類,開發(fā)者想要用這些類去管理其它對象。比如,怪物管理器類、粒子管理器類、聲音管理器類,甚至,管理器管理器類,例:
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類純屬多余,這屬于開發(fā)者對OOP的不熟悉,完全可以將它的功能在Bullet類本身中實現(xiàn):
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_;??
};
關(guān)于訪問權(quán)限的控制,考慮兩個案例,一、從基類中獲取到單例對象。二、將各個單例類合并到一個單例類中,而不必真正將它們單例化:
一、從基類中獲取到單例對象
很多游戲引擎中都會有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()
二、合并到一個單例類中
創(chuàng)建一個代表整個游戲狀態(tài)的Game類,讓這個全局對象捎帶上其它類,來減少全局變量類的數(shù)量,而不必讓Log,F(xiàn)ileSystem和AudioPlayer都變成單例:
class Game??
{??
public:??
static Game& instance() { return instance_; }??
// 設(shè)置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);