一、引言
在Cocos2dx游戏开发中,触摸事件是实现用户交互的核心(如按钮点击、拖拽、滑动)。当多个节点重叠时(如UI按钮覆盖在背景图上),如何控制触摸事件的响应顺序?如何通过触摸事件实现“模态对话框”(打开时屏蔽背景交互)?这些问题的答案都依赖于触摸优先级与吞噬事件(setSwallowTouches)。本文将系统讲解Cocos2dx触摸事件的优先级机制、吞噬事件原理,并通过完整代码演示其在复杂场景中的应用。
二、技术背景
1. Cocos2dx触摸事件体系
Cocos2dx触摸事件基于观察者模式与事件传播机制,核心组件包括:
-
EventListenerTouchOneByOne:单点触摸监听器,处理单个触摸点的BEGAN(开始)、MOVED(移动)、ENDED(结束)、CANCELLED(取消)事件。
-
EventListenerTouchAllAtOnce:多点触摸监听器,处理多个触摸点(如双指缩放)。
-
EventDispatcher:事件分发器,管理监听器注册、优先级排序、事件分发。
2. 核心概念
-
触摸优先级(Priority):决定监听器的执行顺序。数值越小,优先级越高(如优先级-10的监听器先于优先级0的执行)。
-
吞噬事件(Swallow Touches):通过
setSwallowTouches(true)设置,当监听器处理事件后,阻止事件继续向下传递(仅对单点触摸有效)。
-
-
捕获阶段:事件从根节点向下传递到目标节点(可通过
addEventListenerWithFixedPriority注册捕获阶段监听器)。
-
目标阶段:事件到达目标节点,执行目标节点的监听器。
-
冒泡阶段:事件从目标节点向上冒泡到根节点(默认阶段)。
三、应用场景
|
|
|
|
|
|
|
按钮监听器设置高优先级(小数值)+ setSwallowTouches(true)
|
|
|
|
|
|
|
|
|
|
|
|
|
四、核心原理与流程图
1. 原理解释
-
优先级控制:
EventDispatcher根据监听器优先级排序,高优先级(小数值)监听器先执行onTouchBegan。若onTouchBegan返回true,表示该监听器“认领”事件,后续监听器不再执行(除非设置吞噬事件)。
-
吞噬事件:当
setSwallowTouches(true)且onTouchBegan返回true时,事件在目标节点处理完后停止传播(不再进入冒泡阶段)。
2. 原理流程图
graph TD
A[触摸事件发生] --> B[EventDispatcher收集所有监听器]
B --> C[按优先级排序(数值小的在前)]
C --> D[执行捕获阶段监听器(若有)]
D --> E[执行目标阶段监听器(onTouchBegan)]
E -->|onTouchBegan返回true| F[标记当前监听器为"认领者"]
E -->|返回false| G[继续下一个监听器]
F --> H{是否设置setSwallowTouches(true)?}
H -->|是| I[停止传播(不执行后续监听器与冒泡阶段)]
H -->|否| J[继续执行后续监听器(同优先级按注册顺序)]
J --> K[执行冒泡阶段监听器(若有)]
I --> L[事件处理完毕]
K --> L
五、核心特性
-
优先级灵活配置:支持固定优先级(
addEventListenerWithFixedPriority)与场景图优先级(addEventListenerWithSceneGraphPriority,基于节点z-order)。
-
吞噬事件开关:通过
setSwallowTouches控制事件传播,实现“模态”效果。
-
事件传播阶段分离:捕获阶段(向下)、目标阶段(认领)、冒泡阶段(向上),满足复杂交互需求。
-
动态监听器管理:支持运行时添加/移除监听器,适应场景切换(如弹出窗口)。
六、环境准备
1. 开发环境
-
引擎版本:Cocos2dx 3.17+(推荐4.0+,优化事件性能)。
-
开发工具:Visual Studio 2019+(Windows)、Xcode 12+(macOS)、Android Studio(Android)。
-
语言:C++11+(支持Lambda表达式简化回调)。
2. 项目配置
#include "cocos2d.h"
using namespace cocos2d;
七、详细代码实现
以下分基础优先级与吞噬事件、模态对话框、多层菜单、拖拽与点击冲突处理四个场景,提供完整代码。
场景1:基础优先级与吞噬事件(按钮与背景)
功能:创建两个重叠按钮(红/蓝),设置不同优先级,演示吞噬事件对触摸响应的影响。
1. 头文件(TouchPriorityDemo.h)
#ifndef TOUCH_PRIORITY_DEMO_H
#define TOUCH_PRIORITY_DEMO_H
#include "cocos2d.h"
using namespace cocos2d;
class TouchPriorityDemo : public Layer {
public:
static Scene* createScene();
virtual bool init() override;
CREATE_FUNC(TouchPriorityDemo);
private:
// 按钮点击回调
void onRedBtnClick(Ref* sender, Touch::DispatchMode mode);
void onBlueBtnClick(Ref* sender, Touch::DispatchMode mode);
// 背景点击回调
bool onBgTouchBegan(Touch* touch, Event* event);
void onBgTouchEnded(Touch* touch, Event* event);
};
#endif // TOUCH_PRIORITY_DEMO_H
2. 源文件(TouchPriorityDemo.cpp)
#include "TouchPriorityDemo.h"
#include "ui/CocosGUI.h"
USING_NS_CC;
Scene* TouchPriorityDemo::createScene() {
auto scene = Scene::create();
auto layer = TouchPriorityDemo::create();
scene->addChild(layer);
return scene;
}
bool TouchPriorityDemo::init() {
if (!Layer::init()) return false;
Size visibleSize = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
// 1. 创建背景(响应触摸)
auto bg = LayerColor::create(Color4B(200, 200, 200, 255), visibleSize.width, visibleSize.height);
bg->setPosition(origin);
addChild(bg);
// 背景触摸监听器(低优先级:0,不吞噬)
auto bgListener = EventListenerTouchOneByOne::create();
bgListener->setSwallowTouches(false);
bgListener->onTouchBegan = CC_CALLBACK_2(TouchPriorityDemo::onBgTouchBegan, this);
bgListener->onTouchEnded = CC_CALLBACK_2(TouchPriorityDemo::onBgTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(bgListener, bg); // 场景图优先级(基于z-order)
// 2. 创建红色按钮(高优先级:-10,吞噬事件)
auto redBtn = ui::Button::create("btn_red.png", "btn_red_pressed.png"); // 假设资源存在
redBtn->setTitleText("红按钮(高优先级+吞噬)");
redBtn->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2 + 50));
addChild(redBtn);
// 红色按钮监听器(固定优先级-10,吞噬事件)
auto redListener = EventListenerTouchOneByOne::create();
redListener->setPriority(-10); // 高优先级(数值小)
redListener->setSwallowTouches(true); // 吞噬事件
redListener->onTouchBegan = [redBtn](Touch* touch, Event* event) {
return redBtn->hitTest(touch->getLocation()); // 判断触摸点是否在按钮内
};
redListener->onTouchEnded = CC_CALLBACK_2(TouchPriorityDemo::onRedBtnClick, this);
_eventDispatcher->addEventListenerWithFixedPriority(redListener, redBtn); // 固定优先级
// 3. 创建蓝色按钮(低优先级:10,不吞噬)
auto blueBtn = ui::Button::create("btn_blue.png", "btn_blue_pressed.png");
blueBtn->setTitleText("蓝按钮(低优先级)");
blueBtn->setPosition(Vec2(visibleSize.width/2, visibleSize.height/2 - 50));
addChild(blueBtn);
// 蓝色按钮监听器(固定优先级10,不吞噬)
auto blueListener = EventListenerTouchOneByOne::create();
blueListener->setPriority(10); // 低优先级(数值大)
blueListener->setSwallowTouches(false);
blueListener->onTouchBegan = [blueBtn](Touch* touch, Event* event) {
return blueBtn->hitTest(touch->getLocation());
};
blueListener->onTouchEnded = CC_CALLBACK_2(TouchPriorityDemo::onBlueBtnClick, this);
_eventDispatcher->addEventListenerWithFixedPriority(blueListener, blueBtn);
return true;
}
bool TouchPriorityDemo::onBgTouchBegan(Touch* touch, Event* event) {
CCLOG("背景触摸开始");
return true; // 认领事件(若不吞噬,后续按钮仍可响应)
}
void TouchPriorityDemo::onBgTouchEnded(Touch* touch, Event* event) {
CCLOG("背景触摸结束");
}
void TouchPriorityDemo::onRedBtnClick(Ref* sender, Touch::DispatchMode mode) {
CCLOG("红色按钮被点击(高优先级+吞噬)");
}
void TouchPriorityDemo::onBlueBtnClick(Ref* sender, Touch::DispatchMode mode) {
CCLOG("蓝色按钮被点击(低优先级)");
}
场景2:模态对话框(吞噬所有背景事件)
功能:点击按钮弹出模态对话框,对话框内按钮响应触摸,背景事件被吞噬。
1. 头文件(ModalDialog.h)
#ifndef MODAL_DIALOG_H
#define MODAL_DIALOG_H
#include "cocos2d.h"
#include "ui/CocosGUI.h"
using namespace cocos2d;
using namespace ui;
class ModalDialog : public Layer {
public:
CREATE_FUNC(ModalDialog);
virtual bool init() override;
bool onTouchBegan(Touch* touch, Event* event); // 对话框触摸事件(吞噬)
};
#endif // MODAL_DIALOG_H
2. 源文件(ModalDialog.cpp)
#include "ModalDialog.h"
bool ModalDialog::init() {
if (!Layer::init()) return false;
Size visibleSize = Director::getInstance()->getVisibleSize();
// 半透明背景(吞噬触摸)
auto bg = LayerColor::create(Color4B(0, 0, 0, 150), visibleSize.width, visibleSize.height);
addChild(bg);
// 对话框内容
auto dialog = LayerColor::create(Color4B(255, 255, 255, 255), 300, 200);
dialog->setPosition(Vec2(visibleSize.width/2 - 150, visibleSize.height/2 - 100));
addChild(dialog);
// 对话框按钮
auto btn = Button::create("btn_ok.png");
btn->setPosition(Vec2(150, 100));
btn->addClickEventListener([](Ref*) {
CCLOG("对话框按钮被点击");
Director::getInstance()->getRunningScene()->removeChild(this); // 关闭对话框
});
dialog->addChild(btn);
// 注册触摸监听器(吞噬所有事件)
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true); // 吞噬事件
listener->onTouchBegan = CC_CALLBACK_2(ModalDialog::onTouchBegan, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
return true;
}
bool ModalDialog::onTouchBegan(Touch* touch, Event* event) {
// 点击对话框外区域也吞噬事件(不关闭对话框,需额外逻辑处理关闭)
return true;
}
场景3:多层菜单(子菜单优先响应)
功能:主菜单按钮点击弹出子菜单,子菜单按钮响应触摸,主菜单不响应。
源文件(MenuDemo.cpp)
#include "MenuDemo.h"
bool MenuDemo::init() {
if (!Layer::init()) return false;
// 主菜单按钮
auto mainBtn = ui::Button::create("main_menu_btn.png");
mainBtn->setPosition(Vec2(100, 100));
addChild(mainBtn);
mainBtn->addClickEventListener([this](Ref*) {
// 弹出子菜单(高优先级)
auto subMenu = SubMenu::create();
subMenu->setPosition(Vec2(150, 150));
addChild(subMenu);
// 子菜单设置更高优先级(更低数值)
subMenu->setPriority(-5); // 假设SubMenu类支持优先级设置
});
return true;
}
// 子菜单类(SubMenu.h/.cpp)
class SubMenu : public Layer {
public:
CREATE_FUNC(SubMenu);
virtual bool init() override {
// 子菜单按钮
auto subBtn = ui::Button::create("sub_menu_btn.png");
subBtn->setPosition(Vec2(0, 0));
addChild(subBtn);
// 子菜单监听器(优先级-5,吞噬事件)
auto listener = EventListenerTouchOneByOne::create();
listener->setPriority(-5);
listener->setSwallowTouches(true);
listener->onTouchBegan = [subBtn](Touch* touch, Event* event) {
return subBtn->hitTest(touch->getLocation());
};
_eventDispatcher->addEventListenerWithFixedPriority(listener, this);
return true;
}
};
场景4:拖拽与点击冲突处理
功能:拖拽精灵时,不触发点击事件;点击时触发点击事件。
源文件(DragAndClickDemo.cpp)
#include "DragAndClickDemo.h"
bool DragAndClickDemo::init() {
if (!Layer::init()) return false;
// 可拖拽精灵
auto sprite = Sprite::create("sprite.png");
sprite->setPosition(Vec2(200, 200));
addChild(sprite);
// 触摸监听器(处理拖拽与点击)
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true);
bool isDragging = false;
Vec2 touchStartPos;
listener->onTouchBegan = [sprite, &isDragging, &touchStartPos](Touch* touch, Event* event) {
touchStartPos = touch->getLocation();
// 判断是否点击精灵
if (sprite->getBoundingBox().containsPoint(touchStartPos)) {
isDragging = false; // 初始假设为点击
return true;
}
return false;
};
listener->onTouchMoved = [sprite, &isDragging, touchStartPos](Touch* touch, Event* event) {
Vec2 delta = touch->getLocation() - touchStartPos;
if (delta.getLength() > 10) { // 移动超过10px视为拖拽
isDragging = true;
sprite->setPosition(sprite->getPosition() + delta);
touchStartPos = touch->getLocation(); // 更新起始位置
}
};
listener->onTouchEnded = [sprite, &isDragging](Touch* touch, Event* event) {
if (!isDragging) {
CCLOG("精灵被点击(未拖拽)");
sprite->runAction(ScaleBy::create(0.2f, 1.2f)); // 点击动画
} else {
CCLOG("精灵被拖拽结束");
}
isDragging = false;
};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, sprite);
return true;
}
八、运行结果与测试步骤
1. 预期效果
-
场景1:点击红色按钮(高优先级+吞噬),仅输出“红色按钮被点击”;点击蓝色按钮区域,若未被红色按钮覆盖则输出“蓝色按钮被点击”,否则被红色按钮吞噬。
-
场景2:弹出对话框后,点击背景无反应(事件被吞噬),仅对话框内按钮响应。
-
场景3:子菜单弹出后,点击子菜单按钮响应,主菜单按钮不响应。
-
场景4:拖动精灵时不触发点击动画,点击时触发缩放动画。
2. 测试步骤
-
环境配置:创建Cocos2dx项目,添加上述代码文件,准备按钮图片(
btn_red.png、btn_blue.png等)放入Resources目录。
-
编译运行:部署到真机/模拟器,按场景描述操作,观察控制台日志与UI反馈。
九、部署场景
|
|
|
|
|
触摸事件原生支持,注意全面屏安全区(通过SafeArea组件调整触摸区域)。
|
|
|
鼠标事件模拟触摸:BEGAN=鼠标按下,MOVED=鼠标拖动,ENDED=鼠标释放。
|
|
|
通过cc.eventManager绑定触摸事件,注意浏览器对触摸事件的兼容性(如Safari)。
|
十、疑难解答
|
|
|
|
|
|
使用了场景图优先级(addEventListenerWithSceneGraphPriority)却期望固定优先级效果。
|
改用addEventListenerWithFixedPriority并设置setPriority。
|
|
|
onTouchBegan返回false(未认领事件),或setSwallowTouches设为false。
|
确保onTouchBegan返回true,并显式调用setSwallowTouches(true)。
|
|
|
未使用EventListenerTouchAllAtOnce,单点监听器无法处理多点。
|
对多点触摸场景(如缩放),改用EventListenerTouchAllAtOnce。
|
|
|
上层节点监听器未吞噬事件,或onTouchBegan返回false。
|
设置setSwallowTouches(true),并确保onTouchBegan返回true。
|
十一、未来展望与技术趋势
1. 趋势
-
3D触摸支持:结合压力感应(如iPhone 3D Touch),扩展触摸优先级维度(压力值)。
-
手势融合优先级:滑动、长按等手势与触摸优先级联动(如滑动时临时提升优先级)。
-
AI辅助优先级预测:通过用户行为分析预判触摸目标,动态调整优先级。
-
跨平台统一触摸协议:Cocos2dx可能推出更抽象的
TouchManager类,简化优先级与吞噬事件配置。
2. 挑战
-
VR/AR触摸:空间触摸(如手势识别)与2D触摸优先级的整合。
-
低延迟要求:竞技游戏中触摸响应延迟需<16ms,对事件分发效率提出挑战。
十二、总结
Cocos2dx触摸优先级与吞噬事件是复杂交互场景的核心控制手段:
-
优先级通过数值大小(越小越优先)控制监听器执行顺序,解决多节点重叠时的响应冲突。
-
吞噬事件通过
setSwallowTouches(true)阻止事件传播,实现“模态”效果(如对话框、菜单)。
-
-
简单UI用场景图优先级(
addEventListenerWithSceneGraphPriority),复杂交互用固定优先级(addEventListenerWithFixedPriority)。
-
模态组件(对话框、菜单)必须设置高优先级+吞噬事件。
-
拖拽与点击冲突通过移动距离阈值判断,动态切换事件处理逻辑。
通过合理配置优先级与吞噬事件,可显著提升游戏交互的稳定性与用户体验,避免因事件冲突导致的逻辑错误。
https://github.com/chukong/cocos2d-x-samples/tree/v4/input/touch_priority_swallow
评论(0)