cocos2dx[3.2](21)——觀察者模式NotificationCenter

【嘮叨】

    觀察者模式 也叫訂閱/發佈(Subscribe/Publish)模式,是 MVC( 模型-視圖-控制器)模式的重要組成部分。

    舉個例子:郵件消息的訂閱。  比如我們對51cto的最新技術動態頻道進行了消息訂閱。那麼每隔一段時間,有新的技術動態出來時,51cto網站就會將新技術的新聞自動發送郵件給每一個訂閱了該消息的用戶。當然你如果以後不想再收到這類郵件的話,你可以申請退訂消息。


    而在我們的遊戲中,也是需要這樣的訂閱/發佈模式的。在參考文獻《設計模式——觀察者模式》中給出了一個非常典型的應用場景:

        > 你的GameScene裏面有兩個Layer,一個gameLayer,它包含了遊戲中的對象,比如玩家、敵人等。

        > 另一個層是HudLayer,它包含了遊戲中顯示分數、生命值等信息。

        > 如何讓這兩個層相互通信?

        > 在這個示例中,希望將gameLayer中的分數、生命值等信息傳遞到HudLayer中顯示。

        > 而使用觀察者模式,只需要讓HudLayer類訂閱gameLayer類的消息,就可以實現數據的傳遞。


    另外我也想了個例子:主角類Hero,怪獸類Enemy。

        > 你和一羣怪獸在草地上撕鬥,怪獸會一直不停的打你。

        > 那麼它們到底什麼時候纔會停止打你的動作呢?對,直到你掛了。

        > 那麼在遊戲開發中,我們怎麼通知怪獸,你到底掛了還是沒掛?

        > 只要讓怪獸們都訂閱主角類中“掛了”這個信息,然後你掛了之後,發佈“掛了”的信息。

        > 然後所有訂閱了“掛了”信息的怪獸,就會收到信息,然後就會停止再打你了。


    講了這麼多例子,你應該明白觀察者模式是怎麼回事了把。。。i_f08.gif

    很榮幸的是,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類的核心部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//
/**
 * 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:當然消息訂閱不僅僅只侷限於同一個類對象,它也可以跨越不同類對象進行消息訂閱,實現兩個甚至多個類對象之間的數據通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//
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對象,作爲訂閱消息的觀察者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
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主體“發送郵件”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
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源碼後,就明白了。其中有那麼一條判斷語句。

1
2
3
4
5
6
7
8
//
    // 是否訂閱了名稱爲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數據,數據的值爲對應的類名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//
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數據。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
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沒有收到消息。

wKiom1TR6efDz54dAABRQnmfi_E001.jpg


4、分析與總結

    > 觀察者模式的使用很簡單,無非就只有三個業務:訂閱、發佈、退訂

    > 如果不用訂閱/發佈消息模式,那麼還可以在定時器update中,需要不斷監聽某個類的狀態,然後作出響應。這樣的效率自然很低。

    > 而訂閱/發佈模式,可以在某個類的狀態發生改變後,只要postNotification,即可將消息通知給對其感興趣的對象。

    > 特別要注意 addObserver 和 postNotification 函數的傳遞數據參數。如果都傳遞了參數,當數據不同,那麼會造成訂閱者接收不到發佈消息。當然你也可以向我上面舉的例子一樣,這樣就可以只給訂閱了某個消息的某一個類(或某一羣體)發送消息。


5、最後

    雖然 NotificationCenter 很強大,但是在3.x中還是無情的被拋棄了。

    所以你應該去學習一下 EventListenerCustom 這個事件驅動,爲什麼可以讓Cocos引擎喜新厭舊。



本文出自 “夏天的風” 博客,請務必保留此出處http://shahdza.blog.51cto.com/2410787/1611575

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章