观察者模式也叫订阅/发布(Subscribe/Publish)模式,是 MVC(模型-视图-控制器)模式的重要组成部分。
举个例子:邮件消息的订阅。 比如我们对51cto的最新技术动态频道进行了消息订阅。那么每隔一段时间,有新的技术动态出来时,51cto网站就会将新技术的新闻自动发送邮件给每一个订阅了该消息的用户。当然你如果以后不想再收到这类邮件的话,你可以申请退订消息。
而在我们的游戏中,也是需要这样的订阅/发布模式的。在参考文献《设计模式——观察者模式》中给出了一个非常典型的应用场景:
> 你的GameScene里面有两个Layer,一个gameLayer,它包含了游戏中的对象,比如玩家、敌人等。
> 另一个层是HudLayer,它包含了游戏中显示分数、生命值等信息。
> 如何让这两个层相互通信?
> 在这个示例中,希望将gameLayer中的分数、生命值等信息传递到HudLayer中显示。
> 而使用观察者模式,只需要让HudLayer类订阅gameLayer类的消息,就可以实现数据的传递。
另外我也想了个例子:主角类Hero,怪兽类Enemy。
> 你和一群怪兽在草地上撕斗,怪兽会一直不停的打你。
> 那么它们到底什么时候才会停止打你的动作呢?对,直到你挂了。
> 那么在游戏开发中,我们怎么通知怪兽,你到底挂了还是没挂?
> 只要让怪兽们都订阅主角类中“挂了”这个信息,然后你挂了之后,发布“挂了”的信息。
> 然后所有订阅了“挂了”信息的怪兽,就会收到信息,然后就会停止再打你了。
讲了这么多例子,你应该明白观察者模式是怎么回事了把。。。
很荣幸的是,Cocos引擎中已经为我们提供了订阅/发布模式的类NotificationCenter。
更荣幸的是,在3.x版本中,又出现了EventListenerCustom,它取代了NotificationCenter,并将其弃用了。
尽管被弃用了,但是还是要学习的,观察者模式对于不同类之间的数据通信是很重要的知识。同时也会让你能够更好的理解和使用EventListenerCustom事件驱动。
对于EventListenerCustom的用法,参见:Cocos2d-x 3.2——新事件分发机制
【观察者模式】
因为要掌握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(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引擎喜新厌旧。