游戲編程模式(三):觀察者模式
只要你隨便打開一個手機或者電腦的應用,十有八九它就用了MVC架構(gòu),觀察者模式幾乎隨處可見,以至于在Java語言中它被放進了核心庫中(java.util.Observer),C#語言中更是直接嵌入了語法(event關(guān)鍵字)。
在游戲開發(fā)中,說到觀察者模式,典型的場景——成就系統(tǒng):
一.
設想一個場景,有一段物理系統(tǒng)相關(guān)的代碼處理重力,并且追蹤了哪些物體待在地表哪些墜入深淵,而開發(fā)者需要實現(xiàn)”從地表掉落到深淵“的成就。如果把成就相關(guān)代碼放入物理相關(guān)代碼中,那會是一團糟。當然,我們大概也不會這樣做,通常會稍微做一些分離,比如把被追蹤的物體和事件抽離出來:
void Physics::updateEntity(Entity& entity)
{
? ?bool wasOnSurface = entity.isOnSurface();
? ?entity.accelerate(GRAVITY);
? ?entity.update();
? ?if (wasOnSurface && !entity.isOnSurface())
? ?{
? ? notify(entity, EVENT_START_FALL);
? ?}
}
但這明顯還不夠,并沒有完全解耦,現(xiàn)在可以直接引入觀察者模式:
二.
觀察者
class Observer
{
public:
? ?virtual ~Observer() {}
? ?virtual void onNotify(const Entity& entity, Event event) = 0;
};
實現(xiàn)了Observer類的具體類就成為了觀察者,在成就系統(tǒng)中,可以是:
class Achievements : public Observer
{
public:
? ?virtual void onNotify(const Entity& entity, Event event){
? ? ? ?switch (event){
? ? ? ? ? ?case EVENT_ENTITY_FELL:
? ? ? ? ? ?if (entity.isHero() && heroIsOnBridge_){
? ? ? ? ? ? unlock(ACHIEVEMENT_FELL_OFF_BRIDGE);
? ? ? ? ? ?}
? ? ? ? ? ?...
? ? ? ?}
? ?}
private:
? ?...
? ?bool heroIsOnBridge_;
};
被觀察者
class Subject
{
private:
? ?Observer* observers_[MAX_OBSERVERS];
? ?int numObservers_;
public:
? ?void addObserver(Observer* observer){
? ? // 添加到數(shù)組中……
? ?}
? ?void removeObserver(Observer* observer){
? ? // 從數(shù)組中移除……
? ?}
? ?void notify(const Entity& entity, Event event){
? ? ? ?for (int i = 0; i < numObservers_; i++){
? ? ? ? observers_[i]->onNotify(entity, event);
? ? ? ?}
? ?}
};
被觀察的對象擁有通知的方法,并維護了一個列表,保存等它通知的觀察者。現(xiàn)在可以讓物理系統(tǒng)實現(xiàn)Subject類:
class Physics : public Subject{
public:
? ?void updateEntity(Entity& entity);
};
三.
多線程和同步
值得注意的是,觀察者模式是同步的,也就是說,被觀察者直接調(diào)用了觀察者,這意味著直到所有觀察者通知方法返回后,被觀察者才會繼續(xù)自己的工作。在UI線程中,這需要小心,對事件的同步響應應該盡快返回,否則會導致UI鎖死。當有必要的耗時操作時,應該將其送到其它線程或者工作隊列中去。而當進一步引入了線程和鎖,要防止觀察者獲得被觀察者擁有的鎖,否則就會進入死鎖。
四.
鏈式觀察者
上述實現(xiàn)方法中,Subject擁有一列指針指向觀察它的Observer。我們可以將觀察者的列表分布到觀察者自己中來解決內(nèi)存的動態(tài)分配問題,鏈表在這里起到重要作用:
class Subject{
private:
Observer* head_;
? ?
? ?//添加和刪除節(jié)點的方法 add/remove
? ?...
};
class Observer{
friend class Subject;
private:
Observer* next_;
};
void Subject::notify(const Entity& entity, Event event)
{
? ?Observer* observer = head_;
? ?while (observer != NULL)
? ?{
? ? ? ?observer->onNotify(entity, event);
? ? ? ?observer = observer->next_;
? ?}
}
鏈表的各個基本操作在此就不展開敘述,當然,和其它單鏈表一樣,在刪除節(jié)點時需要遍歷鏈表,所以進一步可以考慮引入雙向鏈表來獲得常量時間的刪除操作。
還有一個問題,被觀察者是按鏈表的順序來通知觀察者的,這就要求觀察者之間不應該有順序相關(guān)性,否則觀察者之間會依然存在一個微妙的耦合。
五.
鏈表節(jié)點池
繼續(xù)深入,會發(fā)現(xiàn)之前的方法存在一個致命的問題:如果我們打算將某個觀察者注冊到不同的被觀察者列表中,鏈表的節(jié)點添加操作會使得靠前加入的鏈表遭到破壞,鏈表節(jié)點池可以解決這一問題:
class Node{
private:
Observer* observer;
? ?Node* next;
};
把鏈表節(jié)點本身和觀察者的角色解耦,在每次將觀察者加入列表時,新建一個Node節(jié)點。
六.
對象銷毀
再往下深入,討論一個被觀察者或者觀察者被刪除時會發(fā)生什么。
在C++這種可以主動進行內(nèi)存回收的語言中,如果不小心在某些觀察者上調(diào)用了delete,而被觀察者仍然擁有指向它的指針,這時被觀察試圖發(fā)送一個通知,自然會導致程序出錯,所以,在被刪除時取消注冊是觀察者的職責,通??梢栽谒奈鰳?gòu)器上加上removeObserver() ?。
如果是不小心在被觀察者上調(diào)用delete,這倒是不會導致程序出錯,但是它的觀察者列表中的那些觀察者可能就永遠收不到通知了,這將造成內(nèi)存浪費。解決辦法也簡單,讓被觀察者在它被刪除前發(fā)送一個最終的“死亡通知” ,通知各觀察者“自身死亡的消息”,觀察者就可以做出相應的行為。
垃圾回收
那在那些不能主動進行內(nèi)存回收的語言中呢?它們有垃圾回收器,不存在不小心調(diào)用delete的情況,但這并不能說明就安全了。