您好,登錄后才能下訂單哦!
【嘮叨】
觀察者模式 也叫訂閱/發布(Subscribe/Publish)模式,是 MVC( 模型-視圖-控制器)模式的重要組成部分。
舉個例子:郵件消息的訂閱。 比如我們對51cto的最新技術動態頻道進行了消息訂閱。那么每隔一段時間,有新的技術動態出來時,51cto網站就會將新技術的新聞自動發送郵件給每一個訂閱了該消息的用戶。當然你如果以后不想再收到這類郵件的話,你可以申請退訂消息。
而在我們的游戲中,也是需要這樣的訂閱/發布模式的。在參考文獻《設計模式——觀察者模式》中給出了一個非常典型的應用場景:
> 你的GameScene里面有兩個Layer,一個gameLayer,它包含了游戲中的對象,比如玩家、敵人等。
> 另一個層是HudLayer,它包含了游戲中顯示分數、生命值等信息。
> 如何讓這兩個層相互通信?
> 在這個示例中,希望將gameLayer中的分數、生命值等信息傳遞到HudLayer中顯示。
> 而使用觀察者模式,只需要讓HudLayer類訂閱gameLayer類的消息,就可以實現數據的傳遞。
另外我也想了個例子:主角類Hero,怪獸類Enemy。
> 你和一群怪獸在草地上撕斗,怪獸會一直不停的打你。
> 那么它們到底什么時候才會停止打你的動作呢?對,直到你掛了。
> 那么在游戲開發中,我們怎么通知怪獸,你到底掛了還是沒掛?
> 只要讓怪獸們都訂閱主角類中“掛了”這個信息,然后你掛了之后,發布“掛了”的信息。
> 然后所有訂閱了“掛了”信息的怪獸,就會收到信息,然后就會停止再打你了。
講了這么多例子,你應該明白觀察者模式是怎么回事了把。。。
很榮幸的是,Cocos引擎中已經為我們提供了訂閱/發布模式的類 NotificationCenter 。
更榮幸的是,在3.x版本中,又出現了EventListenerCustom ,它取代了NotificationCenter,并將其棄用了。
盡管被棄用了,但是還是要學習的,觀察者模式對于不同類之間的數據通信是很重要的知識。同時也會讓你能夠更好的理解和使用EventListenerCustom事件驅動。
對于EventListenerCustom的用法,參見:http://shahdza.blog.51cto.com/2410787/1560222
【致謝】
http://cn.cocos2d-x.org/tutorial/show?id=1041 (設計模式——觀察者模式)。
http://blog.csdn.net/jackystudio/article/details/17088979
笨木頭的《Cocos2d-x 3.x 游戲開發之旅》這本書中講得很詳細。
> 這是他的博客:http://www.benmutou.com/
【觀察者模式】
因為要掌握NotificationCenter的使用方法,需要了解各個函數的實現原理,才能理解的透徹一點。所以我將源碼也拿出來分析了。
1、NotificationCenter
NotificationCenter是一個單例類,即與Director類一樣。它主要用來管理訂閱/發布消息的中心。
單例類的使用:通過 NotificationCenter::getInstance() 來獲取單例對象。
它有三個核心函數和一個觀察者數組:
> 訂閱消息 : addObserver() 。訂閱感興趣的消息。
> 發布消息 : postNotification() 。發布消息。
> 退訂消息 : removeObserver() 。不感興趣了,就退訂。
> 觀察者數組 : _observers
而觀察者對象是NotificationObserver類,它的作用就是:將訂閱的消息與相應的訂閱者、訂閱者綁定的回調函數聯系起來。
NotificationCenter/Observer類的核心部分如下:
// /** * NotificationObserver * 觀察者類 * 這個類在NotificationCenter的addObserver中會自動創建,不需要你去使用它。 **/ class CC_DLL NotificationObserver : public Ref { private: Ref* _target; // 觀察者主體對象 SEL_CallFuncO _selector; // 消息回調函數 std::string _name; // 消息名稱 Ref* _sender; // 消息傳遞的數據 public: // 創建一個觀察者對象 NotificationObserver(Ref *target, SEL_CallFuncO selector, const std::string& name, Ref *sender); // 當post發布消息時,執行_selector回調函數,傳入sender消息數據 void performSelector(Ref *sender); }; /** * NotificationCenter * 消息訂閱/發布中心類 */ class CC_DLL __NotificationCenter : public Ref { private: // 保存觀察者數組 NotificationObserver __Array *_observers; public: // 獲取單例對象 static __NotificationCenter* getInstance(); static void destroyInstance(); // 訂閱消息。為某指定的target主體,訂閱消息。 // target : 要訂閱消息的主體(一般為 this) // selector : 消息回調函數(發布消息時,會調用該函數) // name : 消息名稱(類型) // sender : 需要傳遞的數據。若不傳數據,則置為 nullptr void addObserver(Ref* target, SEL_CallFuncO selector, const std::string& name, Ref* sender); // 發布消息。根據某個消息名稱name,發布消息。 // name : 消息名稱 // sender : 需要傳遞的數據。默認為 nullptr void postNotification(const std::string& name, Ref* sender = nullptr); // 退訂消息。移除某指定的target主體中,消息名稱為name的訂閱。 // target : 主體對象 // name : 消息名稱 void removeObserver(Ref* target,const std::string& name); // 退訂消息。移除某指定的target主體中,所有的消息訂閱。 // target : 主體對象 // @returns : 移除的訂閱數量 int removeAllObservers(Ref* target); }; //
工作原理:
> 訂閱消息時(addObserver) :NotificationCenter會自動新建一個對象,這個對象是NotificationObserver,即觀察者。然后將 observer 添加到觀察者數組 _observers 中。
> 發布消息時(postNotification):遍歷 _observers 數組。查找消息名稱為name的所有訂閱,然后執行其觀察者對應的主體target類所綁定的消息回調函數selector。
2、簡單的例子
講了這么多概念,想必大家看得也很暈了把?先來個簡單的使用例子,讓大家了解一下基本的用法。這樣大家的心中也會明朗許多。
PS:當然消息訂閱不僅僅只局限于同一個類對象,它也可以跨越不同類對象進行消息訂閱,實現兩個甚至多個類對象之間的數據通信。
// bool HelloWorld::init() { if ( !Layer::init() ) return false; // 訂閱消息 addObserver // target主體對象 : this // 回調函數 : getMsg() // 消息名稱 : "test" // 傳遞數據 : nullptr NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(HelloWorld::getMsg), "test", nullptr); // 發布消息 postNotification this->sendMsg(); return true; } // 發布消息 void HelloWorld::sendMsg() { // 發布名稱為"test"的消息 NotificationCenter::getInstance()->postNotification("test", nullptr); } // 消息回調函數,接收到的消息傳遞數據為sender void HelloWorld::getMsg(Ref* sender) { CCLOG("getMsg in HelloWorld"); } //
3、訂閱消息:addObserver
源碼實現如下:
訂閱消息的時候,會創建一個NotificationObserver對象,作為訂閱消息的觀察者。
// void __NotificationCenter::addObserver(Ref *target, SEL_CallFuncO selector, const std::string& name, Ref *sender) { // target已經訂閱了name這個消息 if (this->observerExisted(target, name, sender)) return; // 為target主體訂閱的name消息,創建一個觀察者 NotificationObserver *observer = new NotificationObserver(target, selector, name, sender); if (!observer) return; // 加入 _observers 數組 observer->autorelease(); _observers->addObject(observer); } //
4、發布消息:postNotification
源碼實現如下:
發布消息的時候,會遍歷_observer數組,為那些訂閱了name消息的target主體“發送郵件”。
// void __NotificationCenter::postNotification(const std::string& name, Ref *sender = nullptr) { __Array* ObserversCopy = __Array::createWithCapacity(_observers->count()); ObserversCopy->addObjectsFromArray(_observers); Ref* obj = nullptr; // 遍歷觀察者數組 CCARRAY_FOREACH(ObserversCopy, obj) { NotificationObserver* observer = static_cast<NotificationObserver*>(obj); if (!observer) continue; // 是否訂閱了名稱為name的消息 if (observer->getName() == name && (observer->getSender() == sender || observer->getSender() == nullptr || sender == nullptr)) { // 執行observer對應的target主體所綁定的selector回調函數 observer->performSelector(sender); } } } //
5、addObserver與postNotification函數傳遞數據的區別
引自笨木頭的書《Cocos2d-x 3.x 游戲開發之旅》。
細心的同學,肯定發現了一個問題:addObserver與postNotification都可以傳遞一個Ref數據。
那么兩個函數傳遞的數據參數有何不同呢?如果兩個函數都傳遞了數據,在接收消息時,我們應該取誰的數據呢?
其實在第4節中,看過postNotification源碼后,就明白了。其中有那么一條判斷語句。
// // 是否訂閱了名稱為name的消息 if (observer->getName() == name && (observer->getSender() == sender || observer->getSender() == nullptr || sender == nullptr)) { // 執行observer對應的target主體所綁定的selector回調函數 observer->performSelector(sender); } //
也就是說:
> 只有傳遞的數據相同,或者只有一個傳遞了數據,或都沒傳數據,才會將消息發送給對應的target訂閱者。
> 而如果兩個函數傳遞了不同的數據,那么訂閱者將無法接收到消息,也不執行相應的回調函數。
注意:數據相同,表示Ref*指針指向的內存地址一樣。
> 如:定義兩個串 string a = "123"; string b = "123"。雖然a和b數值一樣,但它們是兩個不同的對象,故數據不同。
6、注意事項
Notification是一個單例類,通常在釋放場景或者某個對象之前,都要取消場景或對象訂閱的消息,否則,當消息產生是,會因為對象不存在而產生一些意外的BUG。
所以釋放場景或某個對象時,記得要調用 removeObserver() 來退訂所有的消息。
【代碼實踐】
接下來講講:不同類對象之間,如何通過NotificationCenter實現消息的訂閱和發布 把。
1、定義消息訂閱者
這里我創建了兩個訂閱者A類和B類,并訂閱 "walk" 和 "run" 這兩個消息。
訂閱消息的時候,我故意傳遞了一個類自身定義的data數據,數據的值為對應的類名。
// class Base : public Ref { public: void walk(Ref* sender) { CCLOG("%s is walk", data); } void run(Ref* sender) { CCLOG("%s is run", data); } // 訂閱消息 void addObserver() { // 訂閱 "walk" 和 "run" 消息 // 故意傳遞一個 data 數據 NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(Base::walk), "walk", (Ref*)data); NotificationCenter::getInstance()->addObserver(this, callfuncO_selector(Base::run), "run", (Ref*)data); } public: char data[10]; // 類數據,表示類名 }; class A : public Base { public: A() { strcpy(data, "A"); } // 數據為類名 "A" }; class B : public Base { public: B() { strcpy(data, "B"); } // 數據為類名 "B" }; //
2、發布消息
在HelloWorld類的init()中,創建A類和B類的對象,并分別發布 "walk" 和 "run" 消息。
發布 "run" 的消息的時候,我故意傳遞了一個A類中的data數據。
// bool HelloWorld::init() { if ( !Layer::init() ) return false; // 創建A類和B類。 A* a = new A(); B* b = new B(); a->addObserver(); // A類 訂閱消息 b->addObserver(); // B類 訂閱消息 // 發布 "walk" 消息 NotificationCenter::getInstance()->postNotification("walk"); // 分割線 CCLOG("--------------------------------------------------"); // 發布 "run" 消息 // 故意傳遞一個數據 a類的data數據 NotificationCenter::getInstance()->postNotification("run", (Ref*)a->data); return true; } //
3、運行結果
> 對于發布 "walk" 消息,兩個類A和B都收到消息了,并作出了響應。
> 而對于發布 "run" 消息,因為我故意傳遞了A類中的data數據。所以只有A收到了消息,而B沒有收到消息。
4、分析與總結
> 觀察者模式的使用很簡單,無非就只有三個業務:訂閱、發布、退訂。
> 如果不用訂閱/發布消息模式,那么還可以在定時器update中,需要不斷監聽某個類的狀態,然后作出響應。這樣的效率自然很低。
> 而訂閱/發布模式,可以在某個類的狀態發生改變后,只要postNotification,即可將消息通知給對其感興趣的對象。
> 特別要注意 addObserver 和 postNotification 函數的傳遞數據參數。如果都傳遞了參數,當數據不同,那么會造成訂閱者接收不到發布消息。當然你也可以向我上面舉的例子一樣,這樣就可以只給訂閱了某個消息的某一個類(或某一群體)發送消息。
5、最后
雖然 NotificationCenter 很強大,但是在3.x中還是無情的被拋棄了。
所以你應該去學習一下 EventListenerCustom 這個事件驅動,為什么可以讓Cocos引擎喜新厭舊。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。