一、引言
在移动游戏与交互应用中,手势识别是提升用户体验的核心技术之一。相比传统按键输入,手势(滑动、长按、双击、 pinch 等)更符合直觉,能大幅降低用户学习成本。Cocos2dx 基于触摸事件(EventListenerTouch)提供了底层输入能力,但原生 API 仅支持基础的触摸点检测,需开发者手动封装复杂手势逻辑。本文将系统讲解如何基于 Cocos2dx 封装滑动、长按、双击等常用手势,实现“开箱即用”的手势识别模块,并支持自定义扩展。
二、技术背景
1. 触摸事件基础
Cocos2dx 触摸事件体系基于观察者模式,核心组件包括:
-
EventListenerTouchOneByOne:单点触摸监听器,处理单个触摸点的 BEGAN(开始)、MOVED(移动)、ENDED(结束)、CANCELLED(取消)事件。
-
EventListenerTouchAllAtOnce:多点触摸监听器,处理多个触摸点(如 pinch 手势)。
-
Touch对象:包含触摸点坐标(getLocation())、唯一 ID(getID())等信息。
2. 手势识别核心逻辑
手势识别本质是对触摸事件序列的参数分析,关键参数包括:
-
时间:触摸持续时间(长按)、两次触摸间隔(双击)。
-
空间:触摸点移动距离(滑动)、两点间距变化(pinch)。
-
状态:触摸点数量(单点/多点)、移动方向(滑动方向)。
三、应用场景
|
|
|
|
|
|
|
触摸移动距离>阈值(如 20px)→ 判定为滑动,根据方向(上/下/左/右)触发对应逻辑
|
|
|
|
触摸持续时间>阈值(如 0.5s)→ 判定为长按,触发持续选中状态
|
|
|
|
两次触摸间隔<阈值(如 0.3s)且位置偏差<阈值(如 30px)→ 判定为双击
|
|
|
|
两点间距变化>阈值 → 判定为 pinch,根据间距增减控制缩放比例
|
四、核心原理与流程图
1. 原理解释
-
底层触摸事件捕获:通过
EventListenerTouchOneByOne监听单点触摸,记录触摸点坐标、时间戳。
-
手势参数分析:在触摸事件回调中,计算移动距离、持续时间、时间间隔等参数。
-
手势判定与回调:当参数满足预设阈值(如滑动距离>20px),触发对应手势的回调函数(如
onSwipeLeft)。
2. 原理流程图
graph TD
A[触摸开始 BEGAN] --> B[记录初始位置 (x0,y0) 与时间 t0]
B --> C{触摸移动 MOVED?}
C -->|是| D[记录当前位置 (x1,y1) 与时间 t1]
D --> E[计算移动距离 d=√((x1-x0)²+(y1-y0)²) 与方向 θ]
E --> F{d ≥ 阈值 (如20px)?}
F -->|是| G[判定为滑动,触发 onSwipe 回调]
C -->|否| H{触摸结束 ENDED?}
H -->|是| I[计算持续时间 Δt = t1-t0]
I --> J{Δt ≥ 长按阈值 (如0.5s)?}
J -->|是| K[判定为长按,触发 onLongPress 回调]
J -->|否| L[记录本次点击位置 (x1,y1) 与时间 t1]
L --> M{与上一次点击间隔 ≤ 双击阈值 (如0.3s) 且位置偏差 ≤ 阈值?}
M -->|是| N[判定为双击,触发 onDoubleTap 回调]
M -->|否| O[判定为单击,触发 onSingleTap 回调]
五、核心特性
-
模块化封装:支持单独启用/禁用某类手势(如仅保留滑动识别)。
-
参数可配置:滑动阈值、长按时间、双击间隔等参数开放调整。
-
多手势共存:支持同时识别滑动+长按(如长按时伴随轻微滑动不中断)。
-
跨平台适配:PC 端通过鼠标事件模拟触摸(如鼠标拖动=滑动,右键长按=长按)。
-
六、环境准备
1. 开发环境
-
引擎版本:Cocos2dx 3.17+(推荐 4.0+,优化了触摸事件性能)。
-
开发工具:Visual Studio 2019+(Windows)、Xcode 12+(macOS)、Android Studio(Android)。
-
语言:C++11+(支持 Lambda 表达式简化回调)。
2. 项目配置
无需额外依赖,直接包含 Cocos2dx 触摸事件头文件:
#include "cocos2d.h"
#include "ui/CocosGUI.h"
using namespace cocos2d;
using namespace cocos2d::ui;
七、详细代码实现
以下分基础触摸事件封装、滑动识别、长按识别、双击识别、综合手势识别器五个场景,提供完整代码。
场景1:基础触摸事件封装(GestureBase)
功能:封装触摸点坐标、时间记录,提供基础参数计算接口。
1. 头文件(GestureBase.h)
#ifndef GESTURE_BASE_H
#define GESTURE_BASE_H
#include "cocos2d.h"
using namespace cocos2d;
class GestureBase : public Node {
public:
CREATE_FUNC(GestureBase);
virtual bool init() override;
// 触摸事件回调(子类重写)
virtual bool onTouchBegan(Touch* touch, Event* event);
virtual void onTouchMoved(Touch* touch, Event* event);
virtual void onTouchEnded(Touch* touch, Event* event);
protected:
// 记录触摸点信息
struct TouchInfo {
Vec2 startPos; // 初始位置
Vec2 currentPos; // 当前位置
double startTime; // 开始时间(秒,基于 Director::getTime())
int touchId; // 触摸点ID(单点手势固定)
};
TouchInfo _touchInfo; // 单点触摸信息(当前处理的触摸点)
bool _isTouching = false; // 是否正在触摸
// 工具函数:计算两点距离
float calcDistance(const Vec2& p1, const Vec2& p2) const;
// 工具函数:计算两点方向(角度,弧度制)
float calcAngle(const Vec2& p1, const Vec2& p2) const;
};
#endif // GESTURE_BASE_H
2. 源文件(GestureBase.cpp)
#include "GestureBase.h"
USING_NS_CC;
bool GestureBase::init() {
if (!Node::init()) return false;
_isTouching = false;
return true;
}
bool GestureBase::onTouchBegan(Touch* touch, Event* event) {
if (_isTouching) return false; // 单点手势,忽略多点触摸
_isTouching = true;
_touchInfo.startPos = touch->getLocation(); // 初始位置(世界坐标)
_touchInfo.currentPos = _touchInfo.startPos;
_touchInfo.startTime = Director::getInstance()->getTotalTime(); // 开始时间(秒)
_touchInfo.touchId = touch->getID();
return true; // 吞噬触摸事件(可选,根据需求)
}
void GestureBase::onTouchMoved(Touch* touch, Event* event) {
if (!_isTouching || touch->getID() != _touchInfo.touchId) return;
_touchInfo.currentPos = touch->getLocation(); // 更新当前位置
}
void GestureBase::onTouchEnded(Touch* touch, Event* event) {
if (!_isTouching || touch->getID() != _touchInfo.touchId) return;
_isTouching = false;
}
float GestureBase::calcDistance(const Vec2& p1, const Vec2& p2) const {
return sqrtf(powf(p1.x - p2.x, 2) + powf(p1.y - p2.y, 2));
}
float GestureBase::calcAngle(const Vec2& p1, const Vec2& p2) const {
return atan2f(p2.y - p1.y, p2.x - p1.x); // 弧度制,范围 [-π, π]
}
场景2:滑动识别(SwipeGesture)
1. 头文件(SwipeGesture.h)
#ifndef SWIPE_GESTURE_H
#define SWIPE_GESTURE_H
#include "GestureBase.h"
class SwipeGesture : public GestureBase {
public:
CREATE_FUNC(SwipeGesture);
virtual bool init() override;
// 滑动回调(参数:方向字符串 "up"/"down"/"left"/"right",移动距离)
std::function<void(const std::string&, float)> onSwipeDetected;
// 配置参数(可外部修改)
float swipeThreshold = 20.0f; // 滑动阈值(像素),大于此值才判定为滑动
private:
virtual bool onTouchBegan(Touch* touch, Event* event) override;
virtual void onTouchMoved(Touch* touch, Event* event) override;
virtual void onTouchEnded(Touch* touch, Event* event) override;
};
#endif // SWIPE_GESTURE_H
2. 源文件(SwipeGesture.cpp)
#include "SwipeGesture.h"
USING_NS_CC;
bool SwipeGesture::init() {
if (!GestureBase::init()) return false;
// 注册触摸事件监听器
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true);
listener->onTouchBegan = CC_CALLBACK_2(SwipeGesture::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(SwipeGesture::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(SwipeGesture::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
return true;
}
bool SwipeGesture::onTouchBegan(Touch* touch, Event* event) {
return GestureBase::onTouchBegan(touch, event); // 调用基类记录初始信息
}
void SwipeGesture::onTouchMoved(Touch* touch, Event* event) {
GestureBase::onTouchMoved(touch, event); // 更新当前位置
}
void SwipeGesture::onTouchEnded(Touch* touch, Event* event) {
if (!_isTouching) return;
GestureBase::onTouchEnded(touch, event);
// 计算滑动距离与方向
float distance = calcDistance(_touchInfo.startPos, _touchInfo.currentPos);
if (distance < swipeThreshold) return; // 未达到滑动阈值,不触发
// 计算角度(弧度),转换为角度(0~360°)
float angle = calcAngle(_touchInfo.startPos, _touchInfo.currentPos) * 180 / M_PI;
if (angle < 0) angle += 360;
// 判断方向(简化为四象限)
std::string direction;
if (angle >= 45 && angle < 135) {
direction = "up"; // 上滑(90°±45°)
} else if (angle >= 135 && angle < 225) {
direction = "left"; // 左滑(180°±45°)
} else if (angle >= 225 && angle < 315) {
direction = "down"; // 下滑(270°±45°)
} else {
direction = "right"; // 右滑(0°±45°或360°±45°)
}
// 触发回调
if (onSwipeDetected) {
onSwipeDetected(direction, distance);
}
}
场景3:长按识别(LongPressGesture)
功能:触摸持续时间超过阈值(如 0.5s)触发长按回调。
1. 头文件(LongPressGesture.h)
#ifndef LONG_PRESS_GESTURE_H
#define LONG_PRESS_GESTURE_H
#include "GestureBase.h"
class LongPressGesture : public GestureBase {
public:
CREATE_FUNC(LongPressGesture);
virtual bool init() override;
// 长按回调(参数:触摸点位置)
std::function<void(const Vec2&)> onLongPressDetected;
// 配置参数
float longPressDuration = 0.5f; // 长按阈值(秒)
private:
virtual bool onTouchBegan(Touch* touch, Event* event) override;
virtual void onTouchMoved(Touch* touch, Event* event) override;
virtual void onTouchEnded(Touch* touch, Event* event) override;
// 长按定时器回调
void checkLongPress(float dt);
bool _isLongPressTriggered = false; // 是否已触发长按(避免重复触发)
};
#endif // LONG_PRESS_GESTURE_H
2. 源文件(LongPressGesture.cpp)
#include "LongPressGesture.h"
USING_NS_CC;
bool LongPressGesture::init() {
if (!GestureBase::init()) return false;
_isLongPressTriggered = false;
// 注册触摸事件监听器(同 SwipeGesture)
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true);
listener->onTouchBegan = CC_CALLBACK_2(LongPressGesture::onTouchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(LongPressGesture::onTouchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(LongPressGesture::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
return true;
}
bool LongPressGesture::onTouchBegan(Touch* touch, Event* event) {
if (!GestureBase::onTouchBegan(touch, event)) return false;
_isLongPressTriggered = false;
// 启动定时器,在 longPressDuration 后检查是否仍为触摸状态
scheduleOnce(CC_CALLBACK_1(LongPressGesture::checkLongPress, this), longPressDuration);
return true;
}
void LongPressGesture::onTouchMoved(Touch* touch, Event* event) {
GestureBase::onTouchMoved(touch, event);
// 可选:长按时允许轻微移动(如容差 10px),超过则取消长按
float moveDist = calcDistance(_touchInfo.startPos, _touchInfo.currentPos);
if (moveDist > 10.0f) {
unschedule(CC_CALLBACK_1(LongPressGesture::checkLongPress, this)); // 取消定时器
_isLongPressTriggered = false;
}
}
void LongPressGesture::onTouchEnded(Touch* touch, Event* event) {
GestureBase::onTouchEnded(touch, event);
unschedule(CC_CALLBACK_1(LongPressGesture::checkLongPress, this)); // 触摸结束,取消定时器
_isLongPressTriggered = false;
}
void LongPressGesture::checkLongPress(float dt) {
if (_isTouching && !_isLongPressTriggered) {
_isLongPressTriggered = true;
if (onLongPressDetected) {
onLongPressDetected(_touchInfo.currentPos); // 触发长按回调,传递当前位置
}
}
}
场景4:双击识别(DoubleTapGesture)
功能:两次触摸间隔<阈值(如 0.3s)且位置偏差<阈值(如 30px)触发双击。
1. 头文件(DoubleTapGesture.h)
#ifndef DOUBLE_TAP_GESTURE_H
#define DOUBLE_TAP_GESTURE_H
#include "GestureBase.h"
class DoubleTapGesture : public GestureBase {
public:
CREATE_FUNC(DoubleTapGesture);
virtual bool init() override;
// 双击回调(参数:触摸点位置)
std::function<void(const Vec2&)> onDoubleTapDetected;
// 配置参数
float doubleTapInterval = 0.3f; // 两次点击最大间隔(秒)
float doubleTapDistance = 30.0f; // 两次点击最大位置偏差(像素)
private:
virtual bool onTouchBegan(Touch* touch, Event* event) override;
virtual void onTouchEnded(Touch* touch, Event* event) override;
Vec2 _lastTapPos; // 上一次点击位置
double _lastTapTime = 0; // 上一次点击时间(秒)
};
#endif // DOUBLE_TAP_GESTURE_H
2. 源文件(DoubleTapGesture.cpp)
#include "DoubleTapGesture.h"
USING_NS_CC;
bool DoubleTapGesture::init() {
if (!GestureBase::init()) return false;
_lastTapTime = 0;
// 注册触摸事件监听器(仅关注 BEGAN 和 ENDED,双击无需移动事件)
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true);
listener->onTouchBegan = CC_CALLBACK_2(DoubleTapGesture::onTouchBegan, this);
listener->onTouchEnded = CC_CALLBACK_2(DoubleTapGesture::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
return true;
}
bool DoubleTapGesture::onTouchBegan(Touch* touch, Event* event) {
// 双击只需检测点击开始,无需记录移动,直接返回 true 允许触摸传递(或根据需求吞噬)
return true;
}
void DoubleTapGesture::onTouchEnded(Touch* touch, Event* event) {
double currentTime = Director::getInstance()->getTotalTime();
Vec2 currentPos = touch->getLocation();
// 判断是否满足双击条件:与上一次点击间隔 ≤ doubleTapInterval,且位置偏差 ≤ doubleTapDistance
if (_lastTapTime > 0) { // 存在上一次点击
float timeDiff = currentTime - _lastTapTime;
float distDiff = calcDistance(_lastTapPos, currentPos);
if (timeDiff <= doubleTapInterval && distDiff <= doubleTapDistance) {
// 触发双击回调
if (onDoubleTapDetected) {
onDoubleTapDetected(currentPos);
}
_lastTapTime = 0; // 重置,避免三次点击触发多次双击
return;
}
}
// 不满足条件,更新上一次点击信息(作为下一次点击的“上一次”)
_lastTapPos = currentPos;
_lastTapTime = currentTime;
}
场景5:综合手势识别器(GestureRecognizer)
功能:整合滑动、长按、双击,支持同时启用多种手势,统一管理回调。
1. 头文件(GestureRecognizer.h)
#ifndef GESTURE_RECOGNIZER_H
#define GESTURE_RECOGNIZER_H
#include "SwipeGesture.h"
#include "LongPressGesture.h"
#include "DoubleTapGesture.h"
class GestureRecognizer : public Node {
public:
CREATE_FUNC(GestureRecognizer);
virtual bool init() override;
// 统一回调接口(可根据需求拆分)
std::function<void(const std::string&, float)> onSwipe; // 滑动:方向+距离
std::function<void(const Vec2&)> onLongPress; // 长按:位置
std::function<void(const Vec2&)> onDoubleTap; // 双击:位置
std::function<void(const Vec2&)> onSingleTap; // 单击:位置(未双击时触发)
// 启用/禁用特定手势
void enableSwipe(bool enable) { _enableSwipe = enable; }
void enableLongPress(bool enable) { _enableLongPress = enable; }
void enableDoubleTap(bool enable) { _enableDoubleTap = enable; }
private:
SwipeGesture* _swipeGesture = nullptr;
LongPressGesture* _longPressGesture = nullptr;
DoubleTapGesture* _doubleTapGesture = nullptr;
bool _enableSwipe = true;
bool _enableLongPress = true;
bool _enableDoubleTap = true;
bool _isSingleTapPending = false; // 标记是否为待确认的单击(可能转为双击)
};
#endif // GESTURE_RECOGNIZER_H
2. 源文件(GestureRecognizer.cpp)
#include "GestureRecognizer.h"
USING_NS_NS;
bool GestureRecognizer::init() {
if (!Node::init()) return false;
// 初始化子手势模块
if (_enableSwipe) {
_swipeGesture = SwipeGesture::create();
_swipeGesture->onSwipeDetected = [this](const std::string& dir, float dist) {
if (onSwipe) onSwipe(dir, dist);
};
addChild(_swipeGesture);
}
if (_enableLongPress) {
_longPressGesture = LongPressGesture::create();
_longPressGesture->onLongPressDetected = [this](const Vec2& pos) {
if (onLongPress) onLongPress(pos);
};
addChild(_longPressGesture);
}
if (_enableDoubleTap) {
_doubleTapGesture = DoubleTapGesture::create();
_doubleTapGesture->onDoubleTapDetected = [this](const Vec2& pos) {
if (onDoubleTap) onDoubleTap(pos);
_isSingleTapPending = false; // 双击触发,取消单击待确认
};
addChild(_doubleTapGesture);
}
return true;
}
八、运行结果与测试步骤
1. 预期效果
-
滑动:触摸并移动>20px,控制台输出“Swipe: right, distance=50.0”。
-
-
双击:快速点击同一位置两次,精灵放大 1.5 倍。
-
2. 测试步骤
-
-
创建 Cocos2dx 项目,将上述代码文件加入
Classes目录。
-
准备测试精灵(如
test_sprite.png),放在 Resources目录。
-
#include "GestureRecognizer.h"
bool GameScene::init() {
if (!Layer::init()) return false;
// 创建测试精灵
auto sprite = Sprite::create("test_sprite.png");
sprite->setPosition(Director::getInstance()->getVisibleSize() / 2);
addChild(sprite);
// 创建手势识别器
auto recognizer = GestureRecognizer::create();
recognizer->onSwipe = [&](const std::string& dir, float dist) {
CCLOG("Swipe: %s, distance=%.1f", dir.c_str(), dist);
// 滑动控制精灵移动
Vec2 moveDir = Vec2::ZERO;
if (dir == "left") moveDir.x = -50;
else if (dir == "right") moveDir.x = 50;
else if (dir == "up") moveDir.y = 50;
else if (dir == "down") moveDir.y = -50;
sprite->setPosition(sprite->getPosition() + moveDir);
};
recognizer->onLongPress = [&](const Vec2& pos) {
CCLOG("LongPress at (%.1f, %.1f)", pos.x, pos.y);
sprite->setColor(Color3B::RED); // 长按变红
};
recognizer->onDoubleTap = [&](const Vec2& pos) {
CCLOG("DoubleTap at (%.1f, %.1f)", pos.x, pos.y);
sprite->setScale(sprite->getScale() * 1.5f); // 双击放大
};
addChild(recognizer);
return true;
}
-
-
部署到真机/模拟器,分别测试滑动、长按、双击,观察精灵行为与日志输出。
九、部署场景
|
|
|
|
|
触摸事件原生支持,注意全面屏安全区(通过 SafeArea组件调整手势区域)。
|
|
|
鼠标事件模拟触摸:BEGAN=鼠标按下,MOVED=鼠标拖动,ENDED=鼠标释放。
|
|
|
通过 cc.eventManager绑定触摸事件,注意浏览器对触摸事件的兼容性(如 Safari)。
|
十、疑难解答
|
|
|
|
|
|
|
增大 swipeThreshold(如 30px),长按容差设为 10px。
|
|
|
|
调整 doubleTapInterval(如 0.4s),单击延迟触发(双击未触发时才执行单击)。
|
|
|
未限制单点触摸,多个手指同时操作时 _touchInfo被覆盖。
|
在 onTouchBegan中通过 touch->getID()区分触摸点,仅处理第一个触摸点。
|
|
|
|
|
十一、未来展望与技术趋势
1. 趋势
-
复杂手势支持:封装 pinch(捏合缩放)、rotate(旋转)、swipe with multiple fingers(多指滑动)。
-
机器学习优化:通过 LSTM 网络学习用户手势习惯,动态调整阈值(如老人用户增大长按时间)。
-
跨平台统一 API:Cocos2dx 可能推出官方
GestureDetector类,整合常用手势。
-
无障碍手势:支持单键长按+短按组合、眼动追踪等替代输入方式。
2. 挑战
-
低性能设备适配:复杂手势(如连续滑动+长按)的计算开销需优化(如减少浮点运算)。
-
文化差异:不同地区用户对“滑动方向”的认知差异(如 RTL 语言下左右滑动语义反转)。
-
安全合规:手势数据可能涉及用户隐私(如连续点击位置),需符合 GDPR 等法规。
十二、总结
Cocos2dx 手势识别的核心是基于触摸事件参数分析的封装,通过记录触摸时间、位置,结合阈值判断手势类型。本文实现的模块具备以下价值:
-
开箱即用:滑动、长按、双击识别可直接集成到项目,无需重复开发。
-
灵活扩展:通过继承
GestureBase可新增手势(如 pinch),或修改阈值适配不同场景。
-
跨平台兼容:一套代码适配 iOS/Android/PC,降低多端开发成本。
-
根据游戏类型调整阈值(如动作游戏减小滑动阈值提升灵敏度)。
-
-
通过
UserDefault保存用户自定义手势参数(如调整长按时间)。
通过手势识别,游戏与应用可实现更自然的交互,最终提升用户留存与体验。
https://github.com/chukong/cocos2d-x-samples/tree/v4/input/gesture_recognizer
评论(0)