CocosCreator 事件系统全解析:从基础监听、冒泡捕获到实战应用 (第五篇)
1. 事件监听与关闭:从入门到精通
刚接触CocosCreator时,我发现很多新手最困惑的就是事件监听机制。其实理解起来很简单,就像给家里的门铃安装一个接收器——当有人按门铃(触发事件)时,接收器(监听函数)就会发出响声(执行回调)。
在CocosCreator中,最基础的事件监听方式是使用this.node.on()方法。我特别喜欢它的第三个参数target,这个设计真的很贴心。比如下面这两种写法效果完全一样:
// 写法一:使用bind this.node.on('mousedown', function(event) { this.node.color = cc.Color.RED; }.bind(this)); // 写法二:使用target参数 this.node.on('mousedown', function(event) { this.node.color = cc.Color.RED; }, this);在实际项目中,我更推荐使用第二种写法。因为使用bind会创建一个新函数,这在频繁注册/注销事件时可能会影响性能。而使用target参数则更高效,代码也更简洁。
事件关闭是很多开发者容易忽略的部分。我见过不少项目因为忘记关闭事件监听而导致内存泄漏。正确的做法是在onDisable或onDestroy生命周期中调用off方法:
onDisable: function() { // 必须保证参数与on完全一致 this.node.off('mousedown', this.callback, this); }这里有个坑我踩过:如果回调函数是匿名函数,就无法正确关闭监听。所以一定要像上面这样,把回调函数定义为类方法。
2. 触摸事件:移动开发的核心
触摸事件是移动游戏开发的重中之重。CocosCreator的巧妙之处在于,它统一处理了移动端的触摸和PC端的鼠标事件。这意味着你只需要写一套代码,就能适配两种平台。
触摸事件有四种类型,我用一个表格来总结它们的区别:
| 事件类型 | 触发时机 | 典型应用场景 |
|---|---|---|
| TOUCH_START | 手指接触屏幕 | 开始拖拽、按钮按下效果 |
| TOUCH_MOVE | 手指在屏幕上移动 | 拖拽物体、绘制轨迹 |
| TOUCH_END | 手指离开屏幕 | 结束拖拽、确认点击 |
| TOUCH_CANCEL | 手指在区域外离开 | 取消操作、恢复初始状态 |
处理触摸事件时,event.getLocation()和event.getDelta()是最常用的API。比如实现一个拖拽功能:
this.node.on('touchmove', function(event) { let delta = event.getDelta(); this.node.x += delta.x; this.node.y += delta.y; }.bind(this));多点触控是进阶功能,通过event.getID()可以区分不同的触点。我曾经用这个特性实现了一个双指缩放的功能,核心代码如下:
let startDistance = 0; this.node.on('touchstart', function(event) { if(event.getAllTouches().length === 2) { let touch1 = event.getTouchByIndex(0); let touch2 = event.getTouchByIndex(1); startDistance = cc.Vec2.distance(touch1.getLocation(), touch2.getLocation()); } }); this.node.on('touchmove', function(event) { if(event.getAllTouches().length === 2) { // 计算当前两指距离并缩放节点 } });3. 鼠标事件:PC端交互的灵魂
虽然触摸事件在移动端很强大,但在PC端游戏开发中,鼠标事件才是主角。CocosCreator提供了完整的鼠标事件支持,包括点击、移动、滚轮等。
鼠标事件有个特别有用的特性:mouseenter和mouseleave。这两个事件不需要点击,只要鼠标移入移出就会触发,非常适合实现悬停效果:
this.node.on('mouseenter', function() { this.node.scale = 1.2; // 悬停放大效果 }, this); this.node.on('mouseleave', function() { this.node.scale = 1.0; // 恢复原大小 }, this);鼠标滚轮事件处理有个小技巧。event.getScrollY()返回的是滚轮滚动的Y轴距离,但这个值在不同浏览器中可能正负相反。我通常这样处理:
this.node.on('mousewheel', function(event) { let delta = -event.getScrollY() / 120; // 标准化滚轮值 this.node.scale += delta * 0.1; }, this);在实际项目中,我经常需要区分鼠标左右键。通过event.getButton()可以获取按下的按钮:
this.node.on('mousedown', function(event) { if(event.getButton() === cc.Event.EventMouse.BUTTON_RIGHT) { // 右键菜单逻辑 } }, this);4. 键盘与重力感应:全局事件处理
键盘和重力感应事件属于全局事件,需要通过cc.systemEvent来监听。这类事件的处理方式与节点事件略有不同。
键盘事件最常用于角色控制。下面是一个典型的WASD移动实现:
onLoad: function() { cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); }, onKeyDown: function(event) { switch(event.keyCode) { case cc.macro.KEY.w: this.player.y += 10; break; case cc.macro.KEY.s: this.player.y -= 10; break; // 其他按键处理... } }重力感应事件需要先启用加速度计。在手机游戏中,我常用它来控制赛车或平衡球:
onLoad: function() { cc.systemEvent.setAccelerometerEnabled(true); cc.systemEvent.on(cc.SystemEvent.EventType.DEVICEMOTION, this.onMotion, this); }, onMotion: function(event) { let acc = event.acc; this.ball.x += acc.x * 10; // 注意:Y轴在手机上通常是上下方向 this.ball.y += acc.y * 10; }这里有个重要提示:不同设备的加速度计数据可能有差异,最好在实际设备上测试并添加数据过滤:
// 简单的低通滤波,减少抖动 this.filteredAccX = this.filteredAccX * 0.8 + event.acc.x * 0.2;5. 自定义事件:组件通信的利器
当项目规模变大时,节点之间的通信就成了挑战。CocosCreator提供了强大的自定义事件系统,我总结出两种主要使用场景。
场景一:简单通知使用emit方法最适合组件内部的简单通信:
// 发送端 this.node.emit('score-update', 100); // 接收端 this.node.on('score-update', function(score) { this.scoreLabel.string = score; }, this);场景二:跨节点通信当事件需要在节点树中传递时,dispatchEvent就派上用场了:
// 创建并派发事件 let event = new cc.Event.EventCustom('item-selected', true); event.detail = {id: 123}; this.node.dispatchEvent(event); // 在父节点监听 parentNode.on('item-selected', function(event) { let itemId = event.detail.id; // 处理逻辑... });我在一个商城项目中大量使用了这种模式,使得商品选择、购物车更新等逻辑完全解耦。
6. 事件冒泡与捕获:深入事件机制
理解事件冒泡和捕获是掌握CocosCreator事件系统的关键。我画了个示意图帮助理解:
捕获阶段:父节点 -> 子节点 目标阶段:当前节点 冒泡阶段:子节点 -> 父节点默认情况下,事件监听都是在冒泡阶段触发的。如果想让父节点先于子节点处理事件,就需要使用捕获阶段:
// 第四个参数true表示使用捕获阶段 parentNode.on('touchstart', function() { console.log('父节点先执行'); }, this, true);这个特性在实现一些特殊UI时非常有用。比如我做过一个卡片游戏,需要在卡片被拖动时先由牌桌处理:
// 牌桌节点 tableNode.on('touchstart', function() { // 检查是否可以拖动 }, this, true);停止传播是另一个重要概念。event.stopPropagation()会阻止事件继续冒泡,而stopPropagationImmediate()还会阻止当前节点的其他监听器执行。
7. 实战案例:拼图游戏
让我们用一个完整的拼图游戏案例,串联前面学到的所有知识点。这个游戏会用到触摸、自定义事件、事件冒泡等多种技术。
核心功能实现:
- 拼图块拖动(触摸事件)
this.node.on('touchmove', function(event) { let delta = event.getDelta(); this.node.x += delta.x; this.node.y += delta.y; // 检查是否拼接到正确位置 if(this.checkPosition()) { this.node.emit('piece-complete'); } }, this);- 游戏状态管理(自定义事件)
// 当所有拼图完成时 this.node.on('all-complete', function() { this.showSuccess(); }, this); // 在拼图块中 this.node.emit('piece-complete');- 边界限制(事件冒泡)
// 游戏区域节点 gameArea.on('touchmove', function(event) { if(!this.checkBoundary(event.getLocation())) { event.stopPropagation(); } }, this, true);这个案例展示了如何将各种事件类型有机结合起来,构建复杂的交互逻辑。在实际开发中,合理使用事件系统可以让代码结构更清晰,各模块职责更明确。
