Cocos2d-x 3.x基础学习:观察者模式NotificationCenter

2015年03月23日 11:21 0 点赞 0 评论 更新于 2025-11-21 18:14

观察者模式概述

观察者模式,也被称为订阅/发布(Subscribe/Publish)模式,是MVC(模型 - 视图 - 控制器)模式的重要组成部分。它的核心思想是对象之间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。

生活中的例子

以邮件消息的订阅为例,当我们订阅了51cto的最新技术动态频道后,每当有新的技术动态发布,51cto网站就会自动将相关新闻以邮件的形式发送给每一位订阅者。如果后续不想再接收这类邮件,用户可以申请退订。

游戏开发中的应用场景

在游戏开发中,观察者模式也有广泛的应用。

  • 场景一:不同Layer之间的通信 在游戏场景(GameScene)中,通常会有多个Layer。例如,一个gameLayer包含游戏中的对象,如玩家、敌人等;另一个HudLayer用于显示分数、生命值等信息。若要将gameLayer中的分数、生命值等信息传递到HudLayer中显示,只需让HudLayer类订阅gameLayer类的相关消息,即可实现数据的传递。
  • 场景二:主角与怪兽的交互 在游戏中,主角(Hero)与一群怪兽(Enemy)战斗时,怪兽会持续攻击主角,直到主角“挂了”才会停止。在开发过程中,为了通知怪兽主角的状态,可让所有怪兽订阅主角类中“挂了”这个信息。当主角挂了之后,发布“挂了”的信息,所有订阅该信息的怪兽就会收到通知并停止攻击。

Cocos引擎中的实现

在Cocos引擎中,最初提供了NotificationCenter类来实现订阅/发布模式。但在3.x版本中,EventListenerCustom出现并取代了NotificationCenterNotificationCenter被弃用。不过,学习NotificationCenter仍然很有必要,因为观察者模式对于不同类之间的数据通信至关重要,同时也有助于更好地理解和使用EventListenerCustom事件驱动。关于EventListenerCustom的用法,可参考:Cocos2d-x 3.2——新事件分发机制

NotificationCenter详解

1. NotificationCenter类介绍

NotificationCenter是一个单例类,与Director类类似,主要用于管理订阅/发布消息。获取单例对象的方法是通过NotificationCenter::getInstance()

它包含三个核心函数和一个观察者数组:

  • 订阅消息addObserver(),用于订阅感兴趣的消息。
  • 发布消息postNotification(),用于发布消息。
  • 退订消息removeObserver(),用于取消对特定消息的订阅。
  • 观察者数组_observers,用于存储所有的观察者对象。

观察者对象由NotificationObserver类表示,其作用是将订阅的消息与相应的订阅者、订阅者绑定的回调函数联系起来。以下是NotificationCenterNotificationObserver类的核心部分代码:

/**
* 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):当调用addObserver方法时,NotificationCenter会自动创建一个NotificationObserver对象,并将其添加到观察者数组_observers中。
  • 发布消息(postNotification):调用postNotification方法时,会遍历_observers数组,查找消息名称为指定名称的所有订阅,然后执行其观察者对应的主体target类所绑定的消息回调函数selector

2. 简单使用示例

以下是一个简单的使用示例,展示了如何使用NotificationCenter进行消息的订阅和发布:

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)

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);
}

在订阅消息时,会先检查target是否已经订阅了该消息。若未订阅,则创建一个NotificationObserver对象,并将其添加到观察者数组中。

发布消息(postNotification)

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);
}
}
}

发布消息时,会先复制一份观察者数组,然后遍历该数组,查找订阅了指定名称消息的观察者,并执行其对应的回调函数。

4. addObserver与postNotification函数传递数据的区别

addObserverpostNotification函数都可以传递一个Ref数据。从postNotification的源码中可以看出,只有当传递的数据相同,或者只有一个函数传递了数据,或两个函数都未传数据时,才会将消息发送给对应的target订阅者。如果两个函数传递了不同的数据,订阅者将无法接收到消息,也不会执行相应的回调函数。这里的数据相同是指Ref*指针指向的内存地址一样。例如,定义两个字符串string a = "123"; string b = "123";,虽然ab的数值相同,但它们是两个不同的对象,因此数据不同。

5. 注意事项

NotificationCenter是一个单例类,在释放场景或某个对象之前,需要取消该场景或对象订阅的消息,否则当消息产生时,可能会因为对象不存在而产生意外的BUG。因此,在释放场景或对象时,记得调用removeObserver()removeAllObservers()方法来退订所有的消息。

代码实践:不同类对象之间的消息订阅和发布

1. 定义消息订阅者

创建两个订阅者类AB,并让它们订阅 "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" 消息时,AB两个类都收到了消息,并作出了响应。
  • 发布 "run" 消息时,由于故意传递了A类中的data数据,所以只有A收到了消息,而B没有收到消息。

4. 分析与总结

  • 观察者模式的使用非常简单,主要包括订阅、发布和退订三个操作。
  • 与在定时器update中不断监听某个类的状态相比,订阅/发布模式的效率更高。在某个类的状态发生改变后,只需调用postNotification方法,即可将消息通知给对其感兴趣的对象。
  • 在使用addObserverpostNotification函数时,要特别注意传递数据的参数。如果两个函数都传递了参数,当数据不同时,会导致订阅者接收不到发布的消息。同时,也可以利用这一特性,只给订阅了某个消息的特定类或群体发送消息。

5. 最后

虽然NotificationCenter功能强大,但在3.x版本中已被弃用。建议学习EventListenerCustom这个事件驱动,了解它为何能让Cocos引擎“喜新厌旧”。

作者信息

boke

boke

共发布了 3994 篇文章