【唠叨】

观察者模式也叫订阅/发布(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://www.jb51.cc/article/p-zwifwspg-wx.html


【致谢】

http://cn.cocos2d-x.org/tutorial/show?id=1041(设计模式――观察者模式)。

http://www.jb51.cc/article/p-vwigfxeg-ep.html


笨木头的《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中会自动创建,不需要你去使用它。
**/
classCC_DLLNotificationObserver:publicRef{
	private:
		Ref*_target;//观察者主体对象
		SEL_CallFuncO_selector;//消息回调函数
		std::string_name;//消息名称
		Ref*_sender;//消息传递的数据

	public:
		//创建一个观察者对象
		NotificationObserver(Ref*target,SEL_CallFuncOselector,conststd::string&name,Ref*sender);

		//当post发布消息时,执行_selector回调函数,传入sender消息数据
		voidperformSelector(Ref*sender);
};


/**
*	NotificationCenter
*	消息订阅/发布中心类
*/
classCC_DLL__NotificationCenter:publicRef{
	private:
		//保存观察者数组NotificationObserver
		__Array*_observers;

	public:
		//获取单例对象
		static__NotificationCenter*getInstance();
		staticvoiddestroyInstance();


		//订阅消息。为某指定的target主体,订阅消息。
		//target:要订阅消息的主体(一般为this)
		//selector:消息回调函数(发布消息时,会调用该函数)
		//name:消息名称(类型)
		//sender:需要传递的数据。若不传数据,则置为nullptr
		voidaddobserver(Ref*target,Ref*sender);


		//发布消息。根据某个消息名称name,发布消息。
		//name:消息名称
		//sender:需要传递的数据。默认为nullptr
		voidpostNotification(conststd::string&name,Ref*sender=nullptr);


		//退订消息。移除某指定的target主体中,消息名称为name的订阅。
		//target:主体对象
		//name:消息名称
		voidremoveObserver(Ref*target,conststd::string&name);
		//退订消息。移除某指定的target主体中,所有的消息订阅。
		//target:主体对象
		//@returns:移除的订阅数量
		intremoveAllObservers(Ref*target);
};
//

工作原理:

> 订阅消息时(addobserver) :NotificationCenter会自动新建一个对象,这个对象是NotificationObserver,即观察者。然后将 observer 添加到观察者数组 _observers 中。

> 发布消息时(postNotification):遍历 _observers 数组。查找消息名称为name的所有订阅,然后执行其观察者对应的主体target类所绑定的消息回调函数selector。


2、简单的例子

讲了这么多概念,想必大家看得也很晕了把?先来个简单的使用例子,让大家了解一下基本的用法。这样大家的心中也会明朗许多。

PS:当然消息订阅不仅仅只局限于同一个类对象,它也可以跨越不同类对象进行消息订阅,实现两个甚至多个类对象之间的数据通信。

//
boolHelloWorld::init()
{
if(!Layer::init())returnfalse;

//订阅消息addobserver
//target主体对象:this
//回调函数:getMsg()
//消息名称:"test"
//传递数据:nullptr
NotificationCenter::getInstance()->addobserver(this,callfuncO_selector(HelloWorld::getMsg),"test",nullptr);

//发布消息postNotification
this->sendMsg();

returntrue;
}

//发布消息
voidHelloWorld::sendMsg()
{
//发布名称为"test"的消息
NotificationCenter::getInstance()->postNotification("test",nullptr);
}

//消息回调函数,接收到的消息传递数据为sender
voidHelloWorld::getMsg(Ref*sender)
{
cclOG("getMsginHelloWorld");
}
//


3、订阅消息:addobserver

源码实现如下:

订阅消息的时候,会创建一个NotificationObserver对象,作为订阅消息的观察者。

//
void__NotificationCenter::addobserver(Ref*target,Ref*sender)
{
	//target已经订阅了name这个消息
	if(this->observerExisted(target,name,sender))return;

	//为target主体订阅的name消息,创建一个观察者
	NotificationObserver*observer=newNotificationObserver(target,selector,sender);
	if(!observer)return;

	//加入_observers数组
	observer->autorelease();
	_observers->addobject(observer);
}
//


4、发布消息:postNotification

源码实现如下:

发布消息的时候,会遍历_observer数组,为那些订阅了name消息的target主体“发送邮件”。

//
void__NotificationCenter::postNotification(conststd::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数据,数据的值为对应的类名。

//
classBase:publicRef{
public:
voidwalk(Ref*sender){
cclOG("%siswalk",data);
}
voidrun(Ref*sender){
cclOG("%sisrun",data);
}

//订阅消息
voidaddobserver(){
//订阅"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:
chardata[10];//类数据,表示类名
};

classA:publicBase{
public:
A(){strcpy(data,"A");}//数据为类名"A"
};

classB:publicBase{
public:
B(){strcpy(data,"B");}//数据为类名"B"
};
//


2、发布消息

在HelloWorld类的init()中,创建A类和B类的对象,并分别发布 "run" 消息。

发布 "run" 的消息的时候,我故意传递了一个A类中的data数据。

//
boolHelloWorld::init()
{
if(!Layer::init())returnfalse;

//创建A类和B类。
A*a=newA();
B*b=newB();
a->addobserver();//A类订阅消息
b->addobserver();//B类订阅消息

//发布"walk"消息
NotificationCenter::getInstance()->postNotification("walk");

//分割线
cclOG("--------------------------------------------------");

//发布"run"消息
//故意传递一个数据a类的data数据
NotificationCenter::getInstance()->postNotification("run",(Ref*)a->data);

returntrue;
}
//


3、运行结果

> 对于发布 "walk" 消息,两个类A和B都收到消息了,并作出了响应。

> 而对于发布 "run" 消息,因为我故意传递了A类中的data数据。所以只有A收到了消息,而B没有收到消息。

wKiom1TR6efDz54dAABRQnmfi_E001.jpg


4、分析与总结

> 观察者模式的使用很简单,无非就只有三个业务:订阅、发布、退订

> 如果不用订阅/发布消息模式,那么还可以在定时器update中,需要不断监听某个类的状态,然后作出响应。这样的效率自然很低。

> 而订阅/发布模式,可以在某个类的状态发生改变后,只要postNotification,即可将消息通知给对其感兴趣的对象。

> 特别要注意 addobserver 和 postNotification 函数的传递数据参数。如果都传递了参数,当数据不同,那么会造成订阅者接收不到发布消息。当然你也可以向我上面举的例子一样,这样就可以只给订阅了某个消息的某一个类(或某一群体)发送消息。


5、最后

虽然NotificationCenter很强大,但是在3.x中还是无情的被抛弃了。

所以你应该去学习一下 EventListenerCustom 这个事件驱动,为什么可以让Cocos引擎喜新厌旧。

cocos2dx[3.2](21)――观察者模式NotificationCenter的更多相关文章

  1. ios – Objective-C管理观察者的设计模式

    有一种方法来检测UI控制器是否由父控制器发布,而不使用viewWilldisappear方法?有最好的做法来解决这种情况吗?

  2. Swift设计模式之观察者模式

    转自Swift设计模式原文Design-Patterns-In-Swift

  3. 观察者模式 swift

    在MVC里,观察者模式意味着需要允许Model对象和View对象进行交流,而不能有直接的关联。Cocoa使用两种方式实现了观察者模式:Notification和Key-ValueObserving。Apple对于通知的使用很频繁,比如当键盘弹出或者收起的时候,系统会分别发送UIKeyboardWillShowNotification/UIKeyboardWillHideNotification的通知。在LibaratyAPI.swift里加上取消订阅的代码:deinit{NSNotificationCen

  4. KVO原理分析及使用进阶

    本篇文章对KVO的实现原理进行了详细的分析,并且简单的实现了一个KVO,来当做技术交流。概述KVO全称keyvalueObserving,是苹果提供的一套事件通知机制。由于KVO的实现机制,所以对属性才会发生作用,一般继承自NSObject的对象都默认支持KVO。实际应用KVO主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用KVO实现最为合适。下面是KVO前后打印的关键信息,我们在下面做详细分析。

  5. 浅谈Nodejs观察者模式

    这篇文章主要介绍了浅谈Nodejs观察者模式的相关资料,需要的朋友可以参考下

  6. PHP中常用的三种设计模式详解【单例模式、工厂模式、观察者模式】

    这篇文章主要介绍了PHP中常用的三种设计模式,结合实例形式详细分析了php单例模式、工厂模式与观察者模式概念、功能、相关使用技巧与操作注意事项,需要的朋友可以参考下

  7. 深入理解Javascript中的观察者模式

    观察者模式又称发布订阅模式,是一种最常用的设计模式之一了。下面这篇文章主要给大家深入的介绍了Javascript中观察者模式的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。

  8. PHP设计模式的策略,适配器和观察者模式详解

    这篇文章主要为大家详细介绍了PHP设计模式的策略,适配器和观察者模式,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助

  9. PHP设计模式之观察者模式入门与应用案例详解

    这篇文章主要介绍了PHP设计模式之观察者模式入门与应用,结合具体案例形式详细分析了PHP观察者模式的相关概念、原理、使用方法及操作注意事项,需要的朋友可以参考下

  10. 详解Android观察者模式的使用与优劣

    这篇文章主要介绍了Android观察者模式的相关资料,帮助大家更好的理解和学习Android的设计模式,感兴趣的朋友可以了解下

随机推荐

  1. 【cocos2d-x 3.x 学习笔记】对象内存管理

    Cocos2d-x的内存管理cocos2d-x中使用的是上面的引用计数来管理内存,但是又增加了一些自己的特色。cocos2d-x中通过Ref类来实现引用计数,所有需要实现内存自动回收的类都应该继承自Ref类。下面是Ref类的定义:在cocos2d-x中创建对象通常有两种方式:这两中方式的差异可以参见我另一篇博文“对象创建方式讨论”。在cocos2d-x中提倡使用第二种方式,为了避免误用第一种方式,一般将构造函数设为protected或private。参考资料:[1]cocos2d-x高级开发教程2.3节[

  2. 利用cocos2dx 3.2开发消灭星星六如何在cocos2dx中显示中文

    由于编码的不同,在cocos2dx中的Label控件中如果放入中文字,往往会出现乱码。为了方便使用,我把这个从文档中获取中文字的方法放在一个头文件里面Chinese.h这里的tex_vec是cocos2dx提供的一个保存文档内容的一个容器。这里给出ChineseWords,xml的格式再看看ChineseWord的实现Chinese.cpp就这样,以后在需要用到中文字的地方,就先include这个头文件然后调用ChineseWord函数,获取一串中文字符串。

  3. 利用cocos2dx 3.2开发消灭星星七关于星星的算法

    在前面,我们已经在GameLayer中利用随机数初始化了一个StarMatrix,如果还不知道怎么创建星星矩阵请回去看看而且我们也讲了整个游戏的触摸事件的派发了。

  4. cocos2dx3.x 新手打包APK注意事项!

    这个在编译的时候就可以发现了比较好弄这只是我遇到的,其他的以后遇到再补充吧。。。以前被这两个问题坑了好久

  5. 利用cocos2dx 3.2开发消灭星星八游戏的结束判断与数据控制

    如果你看完之前的,那么你基本已经拥有一个消灭星星游戏的雏形。开始把剩下的两两互不相连的星星消去。那么如何判断是GameOver还是进入下一关呢。。其实游戏数据贯穿整个游戏,包括星星消除的时候要加到获得分数上,消去剩下两两不相连的星星的时候的加分政策等,因此如果前面没有做这一块的,最好回去搞一搞。

  6. 利用cocos2dx 3.2开发消灭星星九为游戏添加一些特效

    needClear是一个flag,当游戏判断不能再继续后,这个flag变为true,开始消除剩下的星星clearSumTime是一个累加器ONE_CLEAR_TIME就是每颗星星消除的时间2.连击加分信息一般消除一次星星都会有连击信息和加多少分的信息。其实这些combo标签就是一张图片,也是通过控制其属性或者runAction来实现。源码ComboEffect.hComboEffect.cpp4.消除星星粒子效果消除星星时,为了实现星星爆裂散落的效果,使用了cocos2d提供的粒子特效引擎对于粒子特效不了

  7. 02 Cocos2D-x引擎win7环境搭建及创建项目

    官网有搭建的文章,直接转载记录。环境搭建:本文介绍如何搭建Cocos2d-x3.2版本的开发环境。项目创建:一、通过命令创建项目前面搭建好环境后,怎样创建自己的Cocos2d-x项目呢?先来看看Cocos2d-x3.2的目录吧这就是Cocos2d-x3.2的目录。输入cocosnew项目名–p包名–lcpp–d路径回车就创建成功了例如:成功后,找到这个项目打开proj.win32目录下的Hello.slnF5成功了。

  8. 利用cocos2dx 3.2开发消灭星星十为游戏添加音效项目源码分享

    一个游戏,声音也是非常的重要,其实cocos2dx里面的简单音效引擎的使用是非常简单的。我这里只不过是用一个类对所有的音效进行管理罢了。Audio.hAudio.cpp好了,本系列教程到此结束,第一次写教程如有不对请见谅或指教,谢谢大家。最后附上整个项目的源代码点击打开链接

  9. 03 Helloworld

    程序都有一个入口点,在C++就是main函数了,打开main.cpp,代码如下:123456789101112131415161718#include"main.h"#include"AppDelegate.h"#include"cocos2d.h"USING_NS_CC;intAPIENTRY_tWinMain{UNREFERENCED_ParaMETER;UNREFERENCED_ParaMETER;//createtheapplicationinstanceAppDelegateapp;return

  10. MenuItemImage*图标菜单创建注意事项

    学习cocos2dx,看的是cocos2d-x3.x手游开发实例详解,这本书错误一大把,本着探索求知勇于发现错误改正错误的精神,我跟着书上的例子一起调试,当学习到场景切换这个小节的时候,出了个错误,卡了我好几个小时。

返回
顶部