【转】Cocos2d - 观察者模式NotificationCenter
http://shahdza.blog.51cto.com/2410787/1611575
【唠叨】
观察者模式 也叫订阅/发布(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类的核心部分如下:
|
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);}// 消息回调函数,接收到的消息传递数据为sendervoid 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没有收到消息。

4、分析与总结
> 观察者模式的使用很简单,无非就只有三个业务:订阅、发布、退订。
> 如果不用订阅/发布消息模式,那么还可以在定时器update中,需要不断监听某个类的状态,然后作出响应。这样的效率自然很低。
> 而订阅/发布模式,可以在某个类的状态发生改变后,只要postNotification,即可将消息通知给对其感兴趣的对象。
> 特别要注意 addObserver 和 postNotification 函数的传递数据参数。如果都传递了参数,当数据不同,那么会造成订阅者接收不到发布消息。当然你也可以向我上面举的例子一样,这样就可以只给订阅了某个消息的某一个类(或某一群体)发送消息。
5、最后
虽然 NotificationCenter 很强大,但是在3.x中还是无情的被抛弃了。
所以你应该去学习一下 EventListenerCustom 这个事件驱动,为什么可以让Cocos引擎喜新厌旧。
本文出自 “夏天的风” 博客,请务必保留此出处http://shahdza.blog.51cto.com/2410787/1611575
【转】Cocos2d - 观察者模式NotificationCenter的更多相关文章
- cocos2dx+lua注册事件函数详解 事件
coocs2dx 版本 3.1.1 registerScriptTouchHandler 注册触屏事件 registerScriptTapHandler ...
- cocos2dx[3.2](11) 新事件分发机制
在2.x中处理事件需要用到委托代理(delegate),相信学过2.x的触摸事件的同学,都知道创建和移除的流程十分繁琐. 而在3.x中由于加入了C++11的特性,而对事件的分发机制通过事件分发器Eve ...
- cocos2dx[3.2](10) 新回调函数std::bind
在2.x中处理事件需要用到委托代理(delegate),相信学过2.x的触摸事件的同学,都知道创建和移除的流程十分繁琐. 而在3.x中由于加入了C++11的特性,而对事件的分发机制通过事件分发器Eve ...
- cocos2dx观察者模式EventListenerCustom的使用(代替NotificationCenter)
在cocos2dx 3.x版本已经被弃用,改用EventDispatcher代替. 观察者模式是MVC模式的一种,一个model可以对应很多个观察者view,当model收到事件通知时,对应的view ...
- iOS学习之观察者模式
观察者模式: 观察者具体应用有两个:通知机制(notification)和KVO(key-value-observing)机制 通知机制: 谁要监听值的变化,谁就注册通知 ,特别要注意,通知的接受者必 ...
- iOS设计模式和机制之观察者模式
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象.这个主题对象在状态上发生变化时,会通知所有观察者对象,使它们能够自动更新自己. 观察者模式的思想:当某对象改变时,观察者会 ...
- 【Unity3D技巧】在Unity中使用事件/委托机制(event/delegate)进行GameObject之间的通信 (二) : 引入中间层NotificationCenter
作者:王选易,出处:http://www.cnblogs.com/neverdie/ 欢迎转载,也请保留这段声明.如果你喜欢这篇文章,请点[推荐].谢谢! 一对多的观察者模式机制有什么缺点? 想要查看 ...
- 设计模式-(10)观察者模式 (swift版)
一,概念 观察者(Observer)模式又名发布-订阅(Publish/Subscribe)模式.GOF给观察者模式如下定义:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它 ...
- 观察者模式在Foundation框架通知中的应用
GitHub传送门 1.何为观察者模式? 观察者设计模式定义了对象间的一种一对多的依赖关系,以便一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动刷新. 举个简单的例子:你和你的舍友都订阅了 ...
随机推荐
- C/C++函数使用
1 memset 将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASCII值, 块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作, 其返回值为指向s的指针. 函数介绍 ...
- 建模算法(二)——整数规划
一.概述 1.定义:规划中变量部分或全部定义成整数是,称为整数规划. 2.分类:纯整数规划和混合整数规划. 3.特点: (1)原线性规划有最优解,当自变量限制为整数后: a.原最优解全是整数,那最优解 ...
- 手持PDA智能条码扫描RFID打印POS机
手持PDA智能条码扫描RFID打印POS机 一.系统稳定性: 1.硬件稳定性: 采用了华为海思(国内唯一可以媲美全球顶级的CPU+射频方案厂商,可以和英伟达等一决高下)手机方案,CPU+射频浑然一 ...
- http://www.cnblogs.com/Matrix54/archive/2012/05/03/2481260.html
http://www.cnblogs.com/Matrix54/archive/2012/05/03/2481260.html
- 简单几何(直线与线段相交) POJ 1039 Pipe
题目传送门 题意:一根管道,有光源从入口发射,问光源最远到达的地方. 分析:黑书上的例题,解法是枚举任意的一个上顶点和一个下顶点(优化后),组成直线,如果直线与所有竖直线段有交点,则表示能穿过管道. ...
- python 代码片段9
#coding=utf-8 # 字符串指示符号 r表示raw u表示unicode mystring=u'this is unicode!--by' print mystring # 'raw'表示告 ...
- BZOJ2707 : [SDOI2012]走迷宫
首先求出SCC缩点,E[T]=0,按拓扑序计算 对于无边连出的块,如果不是T所在块,则称该块是死路块 对于一个块,如果其中的点连出的边是死路块,则它也是死路块 否则对于每块进行高斯消元求出期望 如果S ...
- 自己的一份Spring的xml配置文件
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.spr ...
- 一个简单的SqlServer游标使用
declare @id int ) declare c_department cursor for select id,name from department open c_department f ...
- Delphi中控制Excel(转载)
用Delphi从数据库中取得资料,然后导出到Excel中做成报表是个不错的选择,因为Excel强大的报表功能那可是没话说前提Delphi中要 uses comobj;var Excel:Variant ...