一、引言
在Cocos2dx游戏开发中,模块间通信与逻辑解耦是架构设计的核心需求。自定义事件(Custom Event)基于EventDispatcher机制,允许不同模块(如UI、游戏逻辑、网络层)通过发布-订阅模式传递信息,避免直接依赖。例如,玩家拾取道具时,道具模块可发布“道具拾取”事件,背包模块、成就模块通过监听该事件更新状态,无需相互引用。本文将系统讲解Cocos2dx自定义事件的实现原理、代码实践与高级应用,帮助开发者构建松耦合的游戏架构。
二、技术背景
1. Cocos2dx事件系统架构
Cocos2dx事件系统基于观察者模式,核心组件包括:
-
EventDispatcher(事件分发器):全局单例,负责事件的注册、分发与移除,每个
Node节点可通过getEventDispatcher()获取。
-
EventListener(事件监听器):抽象类,派生出
EventListenerCustom(自定义事件)、EventListenerTouchOneByOne(单点触摸)等子类,定义事件触发时的回调逻辑。
-
Event(事件基类):派生出
EventCustom(自定义事件)、EventTouch(触摸事件)等,携带事件类型与数据。
2. 自定义事件 vs 系统事件
|
|
|
|
|
|
|
|
|
|
字符串标识(如"PLAYER_LEVEL_UP")
|
固定枚举(如EventType::TOUCH_BEGAN)
|
|
|
EventCustom::getUserData()
|
|
|
|
|
|
三、应用场景
|
|
|
|
|
|
|
道具模块dispatchEvent("ITEM_PICKED", itemData),背包模块监听该事件并更新UI
|
|
|
暂停/继续游戏时通知所有子系统(音效、动画、网络)
|
游戏管理类发布"GAME_PAUSE"/"GAME_RESUME"事件,各子系统监听并执行对应逻辑
|
|
|
按钮点击后触发多个UI元素更新(如分数面板、特效播放)
|
按钮点击时dispatchEvent("BUTTON_CLICKED", buttonID),多个UI组件监听并更新
|
|
|
|
场景A结束时发布"SCENE_TRANSITION"事件,场景B初始化时监听并接收数据
|
四、核心原理与流程图
1. 原理解释
自定义事件的生命周期分为创建→注册→分发→处理→销毁五个阶段:
-
创建事件:通过
EventCustom类实例化,指定事件名称(如"EVENT_NAME"),并可附加用户数据(setUserData)。
-
注册监听器:通过
EventDispatcher::addEventListenerWithSceneGraphPriority(场景图优先级,随节点销毁自动移除)或addEventListenerWithFixedPriority(固定优先级,需手动移除)注册EventListenerCustom,绑定回调函数。
-
分发事件:调用
EventDispatcher::dispatchEvent(event)将事件推入分发队列,按顺序触发监听器。
-
处理事件:监听器回调函数被执行,提取事件数据并处理业务逻辑。
-
销毁事件:事件处理完毕后自动回收,监听器需在节点销毁时通过
removeEventListener移除,避免内存泄漏。
2. 原理流程图
graph TD
A[业务逻辑触发事件] --> B[创建EventCustom对象]
B --> C[设置事件名称与用户数据]
C --> D[EventDispatcher注册监听器]
D --> E[监听器绑定回调函数]
E --> F[调用dispatchEvent分发事件]
F --> G[EventDispatcher按优先级排序监听器]
G --> H[依次执行监听器回调]
H --> I[处理事件数据(业务逻辑)]
I --> J[事件处理完毕,清理资源]
J --> K[移除无用监听器(可选)]
五、核心特性
-
灵活的事件标识:通过字符串命名事件(如"PLAYER_HURT"),支持动态扩展。
-
优先级控制:监听器可设置优先级(数值越小优先级越高),高优先级监听器先执行。
-
事件吞噬(Swallow):部分事件支持吞噬(如触摸事件),但自定义事件默认不吞噬,需手动控制传播。
-
跨节点通信:事件可在任意节点间传递,无需知道发送者/接收者具体类型。
-
用户数据传递:通过
EventCustom::setUserData传递任意类型数据(需继承Ref或用std::shared_ptr管理)。
六、环境准备
1. 开发环境
-
引擎版本:Cocos2dx 3.17+(推荐4.0+,优化了事件系统性能)
-
-
Windows:Visual Studio 2019+
-
-
Android:Android Studio + NDK r21+
-
语言:C++11+(支持lambda表达式简化回调)
2. 项目配置
无需额外依赖,直接使用Cocos2dx内置事件模块。确保项目中包含cocos2d.h头文件,并通过Node节点获取事件分发器:
auto dispatcher = Director::getInstance()->getEventDispatcher(); // 全局分发器
// 或节点局部分发器(随节点销毁)
auto nodeDispatcher = this->getEventDispatcher();
七、详细代码实现
以下分基础自定义事件、带参数事件、跨场景事件、优先级与吞噬事件四个场景,提供完整可运行代码。
场景1:基础自定义事件(无参数)
功能:点击按钮触发“按钮点击”事件,日志输出事件信息。
1. 头文件(CustomEventBase.h)
#ifndef CUSTOM_EVENT_BASE_H
#define CUSTOM_EVENT_BASE_H
#include "cocos2d.h"
using namespace cocos2d;
class CustomEventBase : public Layer {
public:
static Scene* createScene();
virtual bool init() override;
CREATE_FUNC(CustomEventBase);
private:
// 按钮点击回调(触发事件)
void onButtonClick(Ref* sender, ui::Widget::TouchEventType type);
// 自定义事件监听器回调
void onCustomEvent(EventCustom* event);
};
#endif // CUSTOM_EVENT_BASE_H
2. 源文件(CustomEventBase.cpp)
#include "CustomEventBase.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
Scene* CustomEventBase::createScene() {
auto scene = Scene::create();
auto layer = CustomEventBase::create();
scene->addChild(layer);
return scene;
}
bool CustomEventBase::init() {
if (!Layer::init()) return false;
// 1. 创建按钮(触发事件)
auto button = ui::Button::create("button_normal.png", "button_pressed.png");
button->setTitleText("触发事件");
button->setPosition(Vec2(VisibleRect::center().x, VisibleRect::center().y));
button->addClickEventListener(CC_CALLBACK_1(CustomEventBase::onButtonClick, this));
addChild(button);
// 2. 注册自定义事件监听器(事件名称:"BUTTON_CLICKED")
auto listener = EventListenerCustom::create("BUTTON_CLICKED",
CC_CALLBACK_1(CustomEventBase::onCustomEvent, this));
// 使用场景图优先级(随节点销毁自动移除)
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
return true;
}
void CustomEventBase::onButtonClick(Ref* sender, ui::Widget::TouchEventType type) {
if (type == ui::Widget::TouchEventType::ENDED) {
// 3. 创建并分发自定义事件(无参数)
EventCustom event("BUTTON_CLICKED");
_eventDispatcher->dispatchEvent(&event);
CCLOG("【触发】按钮点击事件已分发");
}
}
void CustomEventBase::onCustomEvent(EventCustom* event) {
// 4. 处理事件(输出日志)
CCLOG("【接收】监听到自定义事件:%s", event->getEventName().c_str());
// 可通过event->getUserData()获取附加数据(本示例无参数)
}
场景2:带参数的自定义事件
功能:玩家升级时发布“玩家升级”事件,传递新等级与新属性,监听者更新UI。
1. 数据载体(PlayerLevelUpData.h)
#ifndef PLAYER_LEVEL_UP_DATA_H
#define PLAYER_LEVEL_UP_DATA_H
#include "cocos2d.h"
using namespace cocos2d;
class PlayerLevelUpData : public Ref {
public:
static PlayerLevelUpData* create(int newLevel, int newHP) {
auto data = new (std::nothrow) PlayerLevelUpData();
if (data && data->init(newLevel, newHP)) {
data->autorelease();
return data;
}
CC_SAFE_DELETE(data);
return nullptr;
}
int getNewLevel() const { return _newLevel; }
int getNewHP() const { return _newHP; }
private:
bool init(int newLevel, int newHP) {
_newLevel = newLevel;
_newHP = newHP;
return true;
}
int _newLevel;
int _newHP;
};
#endif // PLAYER_LEVEL_UP_DATA_H
2. 事件发布者(EventPublisher.h)
#ifndef EVENT_PUBLISHER_H
#define EVENT_PUBLISHER_H
#include "cocos2d.h"
#include "PlayerLevelUpData.h"
using namespace cocos2d;
class EventPublisher : public Node {
public:
CREATE_FUNC(EventPublisher);
void triggerLevelUpEvent(int newLevel, int newHP) {
// 1. 创建事件数据
auto data = PlayerLevelUpData::create(newLevel, newHP);
// 2. 创建自定义事件,附加数据
EventCustom event("PLAYER_LEVEL_UP");
event.setUserData(data); // 设置用户数据
// 3. 分发事件
_eventDispatcher->dispatchEvent(&event);
CCLOG("【发布】玩家升级事件:等级%d,生命值%d", newLevel, newHP);
}
};
#endif // EVENT_PUBLISHER_H
3. 事件监听者(EventSubscriber.h + EventSubscriber.cpp)
// EventSubscriber.h
#ifndef EVENT_SUBSCRIBER_H
#define EVENT_SUBSCRIBER_H
#include "cocos2d.h"
#include "PlayerLevelUpData.h"
using namespace cocos2d;
class EventSubscriber : public Node {
public:
CREATE_FUNC(EventSubscriber);
virtual bool init() override;
private:
Label* _levelLabel; // 显示等级的标签
void onLevelUpEvent(EventCustom* event); // 事件回调
};
#endif // EVENT_SUBSCRIBER_H
// EventSubscriber.cpp
#include "EventSubscriber.h"
bool EventSubscriber::init() {
if (!Node::init()) return false;
// 创建UI标签
_levelLabel = Label::createWithSystemFont("当前等级:1", "Arial", 30);
_levelLabel->setPosition(Vec2(VisibleRect::center().x, VisibleRect::center().y));
addChild(_levelLabel);
// 注册监听器(事件名称:"PLAYER_LEVEL_UP")
auto listener = EventListenerCustom::create("PLAYER_LEVEL_UP",
CC_CALLBACK_1(EventSubscriber::onLevelUpEvent, this));
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
return true;
}
void EventSubscriber::onLevelUpEvent(EventCustom* event) {
// 1. 获取事件数据
auto data = static_cast<PlayerLevelUpData*>(event->getUserData());
if (!data) return;
// 2. 处理数据(更新UI)
int newLevel = data->getNewLevel();
int newHP = data->getNewHP();
_levelLabel->setString(StringUtils::format("当前等级:%d\n生命值:%d", newLevel, newHP));
CCLOG("【订阅】收到升级事件:等级%d,生命值%d", newLevel, newHP);
}
4. 整合场景(GameScene.cpp)
#include "GameScene.h"
#include "EventPublisher.h"
#include "EventSubscriber.h"
bool GameScene::init() {
if (!Layer::init()) return false;
// 添加发布者
auto publisher = EventPublisher::create();
addChild(publisher);
// 添加订阅者
auto subscriber = EventSubscriber::create();
addChild(subscriber);
// 模拟玩家升级(3秒后触发事件)
scheduleOnce([=](float dt) {
publisher->triggerLevelUpEvent(5, 200); // 等级5,生命值200
}, 3.0f, "level_up_schedule");
return true;
}
场景3:跨场景事件与监听器移除
功能:场景A切换到场景B时,场景A发布的事件需被场景B监听,且场景A销毁时移除其监听器。
关键代码(SceneTransition.cpp)
// 场景A(发布事件)
void SceneA::switchToSceneB() {
// 发布场景切换事件,传递场景B路径
EventCustom event("SCENE_TRANSITION");
event.setUserData(String::create("SceneB")); // 传递场景名称
_eventDispatcher->dispatchEvent(&event);
// 切换到场景B
Director::getInstance()->replaceScene(SceneB::createScene());
}
// 场景B(监听事件)
bool SceneB::init() {
if (!Layer::init()) return false;
// 注册监听器(固定优先级,需手动移除)
auto listener = EventListenerCustom::create("SCENE_TRANSITION",
CC_CALLBACK_1(SceneB::onSceneTransition, this));
_eventDispatcher->addEventListenerWithFixedPriority(listener, 1); // 优先级1
return true;
}
void SceneB::onSceneTransition(EventCustom* event) {
auto sceneName = static_cast<String*>(event->getUserData())->getCString();
CCLOG("【场景B】收到切换事件,目标场景:%s", sceneName);
}
// 场景B销毁时移除监听器(避免内存泄漏)
void SceneB::onExit() {
Layer::onExit();
_eventDispatcher->removeEventListener(listener); // 需保存listener指针
}
场景4:优先级与事件吞噬
功能:两个监听器监听同一事件,高优先级监听器先执行,并可决定是否吞噬事件(阻止低优先级执行)。
// 监听器1(高优先级,优先级1)
auto listener1 = EventListenerCustom::create("PRIORITY_TEST",
[](EventCustom* event) {
CCLOG("【高优先级监听器】执行");
// 吞噬事件(阻止低优先级执行)
// event->stopPropagation(); // 自定义事件默认不吞噬,需手动实现标记
});
_eventDispatcher->addEventListenerWithFixedPriority(listener1, 1); // 优先级1(高)
// 监听器2(低优先级,优先级2)
auto listener2 = EventListenerCustom::create("PRIORITY_TEST",
[](EventCustom* event) {
CCLOG("【低优先级监听器】执行");
});
_eventDispatcher->addEventListenerWithFixedPriority(listener2, 2); // 优先级2(低)
// 分发事件
EventCustom event("PRIORITY_TEST");
_eventDispatcher->dispatchEvent(&event);
// 输出:先执行listener1,再执行listener2(若未调用stopPropagation)
八、运行结果与测试步骤
1. 预期效果
-
场景1:点击按钮后,日志输出“【触发】按钮点击事件已分发”和“【接收】监听到自定义事件:BUTTON_CLICKED”。
-
场景2:3秒后,标签显示“当前等级:5 生命值:200”,日志输出发布与订阅信息。
-
-
场景4:事件分发后,高优先级监听器先执行,低优先级后执行(若未吞噬)。
2. 测试步骤
-
环境配置:创建Cocos2dx项目,将上述代码文件加入
Classes目录,配置资源路径(如按钮图片)。
-
编译运行:使用VS/Xcode编译并部署到真机/模拟器。
-
九、部署场景
|
|
|
|
|
事件监听逻辑与Android一致,需注意Objective-C与C++混编时的内存管理(如使用__bridge)。
|
|
|
避免在Java层频繁触发事件,C++层事件分发需注意线程安全(主线程更新UI)。
|
|
|
模拟器测试时,可通过键盘事件模拟按钮点击(如按空格键触发事件)。
|
|
|
使用Cocos2dx JS绑定,事件系统兼容Web平台,需注意JavaScript与原生代码的数据类型转换。
|
十、疑难解答
|
|
|
|
|
|
1. 事件名称拼写错误;2. 监听器未注册;3. 事件分发前监听器已被移除。
|
检查事件名称一致性,确保监听器在分发前通过addEventListener注册,避免提前调用removeEventListener。
|
|
|
监听器未随节点销毁而移除(如使用addEventListenerWithFixedPriority但未在onExit中移除)。
|
优先使用addEventListenerWithSceneGraphPriority(自动随节点移除),或在onExit中手动移除固定优先级监听器。
|
事件数据为空(getUserData()返回nullptr)
|
未调用event->setUserData或数据类型转换错误。
|
确保发布事件时设置用户数据,接收时用static_cast正确转换类型(如PlayerLevelUpData*)。
|
|
|
优先级设置错误(数值越小优先级越高),或混合使用场景图优先级与固定优先级。
|
统一使用固定优先级并明确数值,或按需求调整优先级(如UI监听器优先级高于逻辑监听器)。
|
十一、未来展望与技术趋势
1. 趋势
-
异步事件支持:Cocos2dx可能引入异步事件分发(如基于
std::future),避免阻塞主线程。
-
事件总线(Event Bus)封装:更高层的事件总线类,简化多事件管理与订阅者注册。
-
类型安全事件:通过模板或强类型枚举替代字符串事件名称,减少拼写错误(如
enum class EventType { PLAYER_LEVEL_UP })。
-
跨线程事件:支持后台线程分发事件,主线程监听并更新UI(需线程同步机制)。
2. 挑战
-
事件依赖管理:复杂系统中事件间可能存在依赖(如A事件需等待B事件完成),需引入事件编排机制。
-
性能优化:高频事件(如每帧更新的位置同步)可能导致性能瓶颈,需支持事件合并与节流。
-
调试难度:事件流分散在各模块,需开发可视化事件调试工具(如事件日志追踪)。
十二、总结
Cocos2dx自定义事件基于EventDispatcher实现了灵活的模块间通信,核心价值在于解耦与扩展性。通过本文的学习,开发者应掌握:
-
核心流程:创建
EventCustom→注册EventListenerCustom→分发事件→处理回调。
-
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
评论(0)