【唠叨】

观察者模式 也叫订阅/发布(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   : 需要传递的数据。若不传数据,则置为 nullptrvoid addObserver(Ref* target, SEL_CallFuncO selector, const std::string& name, Ref* sender);// 发布消息。根据某个消息名称name,发布消息。// name   : 消息名称// sender : 需要传递的数据。默认为 nullptrvoid 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"// 传递数据       : nullptrNotificationCenter::getInstance()->addObserver(this, callfuncO_selector(HelloWorld::getMsg), "test", nullptr);// 发布消息 postNotificationthis->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引擎喜新厌旧。

转载于:https://blog.51cto.com/shahdza/1611575

cocos2dx[3.2](21)——观察者模式NotificationCenter相关推荐

  1. cocos2d-x 观察者模式

    题目有点大,但是确实就是观察者模式. 游戏中经常遇到几个层之间互相通信的问题.比如,当前页是游戏页,游戏结束的时候,直接显示结算页,不进行转场了,结算页上又有按钮.这时如果要响应按钮点击事件处理一些数 ...

  2. cocos2dx的几种常见设计模式

    设计模式--二段构建模式 设计模式在程序设计中会经常用到,也许你从来没有留意过设计模式,其实你却一直在使用设计模式!cocos2dx中有不少的设计模式,所以从本篇博客开始探讨一下cocos2dx中的设 ...

  3. 设计模式-观察者模式(Observer)-Java

    设计模式-观察者模式(Observer)-Java 目录 文章目录 1.前言 2.示例案例-多人联机对战游戏的设计 3.观察者模式概述 3.1.观察者模式定义 3.2.观察者模式结构 3.3.观察者模 ...

  4. Cocos Creator 入门笔记

    推荐文档https://www.tslang.cn/docs/home.html 文章目录 (一).从装机箱谈到面向对象再到游戏引擎 (二).Cocos发展史 (三).编程语言 环境: 语言区别: 开 ...

  5. 2016年最权威的1000集大型web前端视频教程(爱创课堂出品)

    爱创课堂Web前端开发工程师培训-价值1万8课程  Web前端开发工程师,主要职责是利用HTML.XHTML.CSS.JAVAScript.FLASH等各种Web前端技术进行客户端产品的开发.完成客户 ...

  6. Java SE 基础知识

    Java SE 基础知识 1 2 @(Notes)[J2SE, Notes] VICTORY LOVES PREPARATION. 特别说明: 该文档在马克飞象查阅最佳: 本部分知识还在迭代中,欢迎补 ...

  7. 尚学堂JAVA高级学习笔记_1/2

    尚学堂JAVA高级学习笔记 文章目录 尚学堂JAVA高级学习笔记 写在前面 第1章 手写webserver 1. 灵魂反射 2. 高效解析xml 3. 解析webxml 4. 反射webxml 5. ...

  8. 【转】Android4.4 之Bluetooth整理

    原文网址:http://www.cnblogs.com/shed/p/3737016.html Android 4.4上蓝牙协议栈采用的是BRCM和Google共同开发的bluedroid,代替了之前 ...

  9. 小江cocos2d-x 3.3游戏学习之旅之观察者模式

    今天看了一篇文章,学习了cocos2d-x下的观察者模式,觉得受益匪浅,观察者模式就是发送一个消息,让观察者执行某个动作,可用于数据传递,以可以用于消息传递 1.在同个层里面的观察者模式,用Hello ...

最新文章

  1. Computer Vision Tasks
  2. 软件测试系列---软件测试基础
  3. webbench源码解析
  4. StackOverflow程序员推荐:每个程序员都应读的30本书
  5. 数学--数论--容斥定理完全解析(转)
  6. 三星核S5PV210AH-A0 SAMSUNG
  7. 【报告分享】2021年网生代线上社交行为洞察报告:95后、00后社交江湖大揭秘.pdf(附下载链接)...
  8. c++指针各种用法小结
  9. 4.2.2 - Logical and/or Operators
  10. 计算机一级在线练习,计算机一级练习系统
  11. 【洛谷 2863】牛的舞会
  12. TI DSP位域寄存器文件(Bit Field and Register-File Struc...
  13. C#与JavaScript中URL编码解码问题(转)
  14. windows系统搭建redis集群
  15. LeetCode每日一题——592. 分数加减运算
  16. 运动世界校园显示服务器异常,运动世界校园为什么成绩异常 成绩异常相关
  17. wps垂直居中快捷键_word如何设置垂直居中 wpsword设置垂直居中
  18. flutter大小单位:dp
  19. 最短路径(加权有向图)
  20. 上传下载文件实例(vsftp服务器+nginx)

热门文章

  1. 20200119:(leetcode)回文数(3种解法)
  2. 如何linux安装apache服务器,Linux中如何安装Apache服务器
  3. jfinal中使用freemarker
  4. Sublime 格式化 JSON
  5. linux shell和配置文件的执行顺序
  6. 自动档汽车正确的操作方法和习惯---请教贴
  7. VB 提取TextBox 文本框中指定一行字符串
  8. 等级考试(三):三级网络---似曾相识(续)
  9. 银河水滴张曼:远距离步态识别系统与应用 |量子位沙龙回顾
  10. 谷歌非洲AI中心成立,有坑,速来