ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

Cocos Creator事件系统实战:从零构建五子棋游戏

Cocos Creator事件系统实战:从零构建五子棋游戏 1. 项目概述与核心价值最近在社区看到不少朋友对Cocos Creator的交互逻辑实现特别是事件处理这块感到头疼总觉得概念懂了一上手就乱。正好我之前用Cocos Creator 3.x完整实现了一个带简单AI的人机对战五子棋整个过程对节点事件的应用算是“扒了一层皮”踩了不少坑也总结出一些非常实用的套路。这个项目不像那些复杂的RPG或动作游戏它规则明确、交互聚焦是绝佳的、用于吃透Cocos Creator事件系统与基础游戏逻辑的练手项目。你不仅能学会如何让棋盘响应点击、如何高亮落子更能掌握如何构建一个清晰的游戏状态机并为其注入一个“会思考”的对手——哪怕这个AI最初只是随机落子。很多人学引擎容易陷入两个极端要么只盯着官方文档的API看感觉都会了但无从下手要么直接克隆一个复杂项目被各种模块绕晕。这个五子棋项目恰恰卡在中间它要求你必须理解“节点树”、“事件监听与派发”、“组件间通信”这些核心概念才能让游戏跑起来。通过它你会真切地感受到在Cocos Creator里一切交互的起点都是“节点”而“事件”是驱动节点活起来的血液。我们不止要实现功能更要弄明白为什么这样设计代码结构更清晰、为什么用这种方式监听事件性能更好。接下来我就把这个项目的实战经验尤其是“节点事件篇”的精华毫无保留地拆解给你看。2. 项目整体设计与核心思路拆解2.1 为什么选择五子棋作为实战项目五子棋规则简单但实现它所需的技术栈却非常典型几乎涵盖了2D棋盘类游戏的所有基础要素场景搭建棋盘、棋子、用户输入处理鼠标点击、游戏逻辑胜负判定、AI逻辑以及状态管理。选择它意味着你可以集中火力攻克Cocos Creator的核心机制而不是被美术资源、复杂动画所干扰。我们的核心目标是利用Cocos Creator的节点与组件系统构建一个响应灵敏、逻辑清晰、且具备可扩展AI接口的游戏框架。这个项目的设计思路遵循“模型-视图-控制器”的简易变体但更贴合Cocos Creator的“组件化”思想数据模型 (Model)一个纯JavaScript类如GameModel负责维护棋盘数据15x15的二维数组、当前行棋方、游戏状态进行中、结束。它不依赖任何引擎API只关心数据逻辑。视图 (View)由Cocos Creator场景中的节点树构成。包括背景、棋盘网格线、棋子精灵Prefab、当前玩家提示UI等。视图的职责是根据模型的数据进行渲染。控制器 (Controller)由挂载在各个节点上的Component脚本担任。它们监听用户输入节点事件调用模型的方法更新数据再根据模型的变化通知视图更新。节点事件是连接视图与控制器的桥梁。2.2 技术选型与节点事件的核心地位在这个项目中我们几乎会用到Cocos Creator事件系统所有常见的用法input.on与节点事件监听处理鼠标点击棋盘格子。事件派发 (EventTarget)用于模型状态改变时通知多个视图组件更新例如棋子落下后需要更新棋盘渲染和判断胜负。触摸事件与UI事件虽然五子棋主要用鼠标但理解触摸事件对于移动端适配至关重要。自定义事件定义如GameEventType.ON_PIECE_PLACED这样的事件实现模块间解耦。为什么强调节点事件因为Cocos Creator中所有交互的捕获点都在节点上。你不能直接去“点击一个数组”你必须点击代表那个棋盘格子的节点或一个覆盖在其上的透明按钮。理解这一点就理解了Cocos Creator交互开发的入门钥匙。注意在项目初期务必在项目设置-功能裁剪中确保Canvas和EventSystem相关模块没有被裁剪掉否则所有输入事件都会失效。这是一个常见的打包后事件无响应的“坑”。3. 核心细节解析与实操要点3.1 棋盘与棋子节点的创建与管理棋盘通常不直接用15x15个Sprite节点来拼那样节点数太多225个对性能和管理都是挑战。更常见的做法是一个棋盘背景节点包含一张棋盘纹理图片。一个棋盘交互层节点这是一个空节点我们为其添加一个BlockInput组件一个自定义脚本用于统一处理点击事件。或者也可以使用一个全屏大小的Button组件但通常更灵活的是使用Graphics组件绘制网格线并结合事件监听。棋子Prefab创建一个精灵节点作为预制体包含黑棋和白棋两种状态的Sprite。Prefab是动态生成棋子的蓝图。关键技巧坐标转换这是第一个难点。当你点击屏幕时获得的是屏幕坐标。你需要将其转换到棋盘交互层节点的本地坐标系中再根据格子尺寸如每个格子40像素计算出对应的行列索引。// 在 BlockInput 组件的 onLoad 或 start 中监听触摸事件 this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); private onTouchEnd(event: EventTouch) { // 1. 获取触摸点的世界坐标 const touchPos event.getUILocation(); // 2. 将世界坐标转换为当前节点棋盘交互层的本地坐标 const localPos this.node.getComponent(UITransform)!.convertToNodeSpaceAR(v3(touchPos.x, touchPos.y, 0)); // 3. 根据本地坐标和格子大小计算行列 const gridSize 40; const row Math.floor(localPos.y / gridSize 7.5); // 假设棋盘中心为(0,0)共15行 const col Math.floor(localPos.x / gridSize 7.5); // 确保行列在有效范围内 if (row 0 row 15 col 0 col 15) { // 触发落子逻辑 this.onGridClicked(row, col); } }这里用到了UITransform组件的convertToNodeSpaceAR方法它是处理UI坐标转换的核心。务必理解世界坐标、本地坐标、锚点这些概念否则点击位置永远对不上。3.2 游戏模型GameModel的设计模型是游戏的大脑它应该是“纯净”的不依赖于Cocos Creator的运行时环境。这样便于单独测试AI算法。// GameModel.ts export enum PieceType { Empty 0, Black 1, White 2 } export enum GameState { Playing, BlackWin, WhiteWin, Draw } export class GameModel { private _board: number[][] []; private _currentPlayer: PieceType PieceType.Black; private _gameState: GameState GameState.Playing; // 事件派发器用于通知外部状态变更 public eventTarget: EventTarget new EventTarget(); constructor() { this.resetBoard(); } public resetBoard() { this._board Array(15).fill(0).map(() Array(15).fill(PieceType.Empty)); this._currentPlayer PieceType.Black; this._gameState GameState.Playing; this.eventTarget.emit(onGameReset); } public placePiece(row: number, col: number): boolean { if (this._gameState ! GameState.Playing || this._board[row][col] ! PieceType.Empty) { return false; } this._board[row][col] this._currentPlayer; // 派发棋子落下事件携带行列和棋子类型信息 this.eventTarget.emit(onPiecePlaced, {row, col, type: this._currentPlayer}); if (this.checkWin(row, col)) { this._gameState this._currentPlayer PieceType.Black ? GameState.BlackWin : GameState.WhiteWin; this.eventTarget.emit(onGameOver, { winner: this._currentPlayer }); } else if (this.isBoardFull()) { this._gameState GameState.Draw; this.eventTarget.emit(onGameOver, { winner: PieceType.Empty }); } else { this.switchPlayer(); } return true; } private checkWin(row: number, col: number): boolean { const directions [ [1, 0], // 水平 [0, 1], // 垂直 [1, 1], // 右下 [1, -1] // 右上 ]; const type this._board[row][col]; for (const [dx, dy] of directions) { let count 1; // 正向检查 for (let i 1; i 5; i) { const newRow row dx * i; const newCol col dy * i; if (this._board[newRow]?.[newCol] type) count; else break; } // 反向检查 for (let i 1; i 5; i) { const newRow row - dx * i; const newCol col - dy * i; if (this._board[newRow]?.[newCol] type) count; else break; } if (count 5) return true; } return false; } // ... 其他方法如 switchPlayer, isBoardFull, getBoard 等 }模型通过EventTarget派发事件这是实现与视图解耦的关键。控制器组件监听这些事件然后去更新节点状态而不是由模型直接操作节点。3.3 节点事件的监听与派发实战在Cocos Creator中事件处理主要有三种方式在这个项目里都会用到1. 内置事件监听如触摸、鼠标如上文所述在BlockInput组件中使用this.node.on(Node.EventType.TOUCH_END, ...)来监听棋盘点击。2. 自定义事件监听监听模型事件在负责渲染棋盘的BoardManager组件中需要监听模型的事件来更新视图。// BoardManager.ts import { GameModel } from ./GameModel; export class BoardManager extends Component { property(Prefab) public piecePrefab: Prefab null!; private _pieceMap: Node[][] []; // 存储已创建的棋子节点 private _model: GameModel null!; start() { this._model new GameModel(); this.initBoardView(); // 监听模型的自定义事件 this._model.eventTarget.on(onPiecePlaced, this.onPiecePlaced, this); this._model.eventTarget.on(onGameReset, this.onGameReset, this); } private onPiecePlaced(data: {row: number, col: number, type: PieceType}) { // 在指定位置实例化一个棋子Prefab const pieceNode instantiate(this.piecePrefab); this.node.addChild(pieceNode); // 设置棋子的位置、精灵帧根据type const spriteComp pieceNode.getComponent(Sprite); spriteComp.spriteFrame data.type PieceType.Black ? this.blackSpriteFrame : this.whiteSpriteFrame; // 计算世界坐标并设置 const worldPos this.convertGridToWorld(data.row, data.col); pieceNode.setPosition(worldPos); // 存入映射方便管理如重置游戏时销毁 if (!this._pieceMap[data.row]) this._pieceMap[data.row] []; this._pieceMap[data.row][data.col] pieceNode; } private onGameReset() { // 销毁所有棋子节点 for (let row of this._pieceMap) { if (row) { for (let pieceNode of row) { if (pieceNode pieceNode.isValid) { pieceNode.destroy(); } } } } this._pieceMap []; } }3. 使用全局事件系统可选用于更松散的耦合对于一些跨场景、跨组件的通知比如更新UI顶部的状态文字可以使用Cocos Creator内置的director.getScene().emit或自己维护一个全局事件管理器。但在本例中通过模型的事件派发器已经足够。重要心得一定要在组件的onDestroy或合适的生命周期里取消事件监听否则会导致内存泄漏和报错尝试调用已销毁组件的方法。protected onDestroy(): void { if (this._model) { this._model.eventTarget.off(onPiecePlaced, this.onPiecePlaced, this); this._model.eventTarget.off(onGameReset, this.onGameReset, this); } // 同时取消节点自身的事件监听 this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); }这是新手极易忽略的一点尤其是在反复切换场景时问题会集中爆发。4. 实操过程与核心环节实现4.1 场景搭建与节点结构创建场景新建一个GameScene。构建节点树CanvasBackground(Sprite) - 放置棋盘背景图。GameBoard(Node) - 棋盘逻辑节点。GridGraphics(Graphics) - 用于绘制棋盘网格线可选也可以用图片。BlockInput(Node BlockInput组件) - 一个大小覆盖整个棋盘的透明节点专门处理点击。PiecesLayer(Node) - 一个空节点作为所有动态生成的棋子节点的父节点便于统一管理。UI(Node) - UI层。StatusLabel(Label) - 显示“黑方回合”、“白方胜利”等状态。RestartBtn(Button) - 重新开始按钮。这个结构清晰地将背景、交互层、动态元素、UI分层符合Cocos Creator的渲染和事件处理顺序。4.2 连接交互、模型与视图现在我们需要把BlockInput、GameModel、BoardManager和UIManager串联起来。BlockInput组件控制器的一部分// BlockInput.ts import { _decorator, Component, Node, EventTouch, input, Input, UITransform, v3 } from cc; import { GameModel } from ./GameModel; const { ccclass, property } _decorator; ccclass(BlockInput) export class BlockInput extends Component { // 通过属性绑定传入模型实例或者通过全局访问。这里假设BoardManager创建了模型并传递过来。 private _model: GameModel null!; // 通常由BoardManager在start时设置 public setModel(model: GameModel) { this._model model; } start() { // 监听触摸结束事件 this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); } private onTouchEnd(event: EventTouch) { if (!this._model) return; // 坐标转换逻辑同上文 const touchPos event.getUILocation(); const uiTrans this.node.getComponent(UITransform); const localPos uiTrans!.convertToNodeSpaceAR(v3(touchPos.x, touchPos.y, 0)); const gridSize 40; const row Math.floor(localPos.y / gridSize 7.5); const col Math.floor(localPos.x / gridSize 7.5); if (row 0 row 15 col 0 col 15) { // 调用模型的落子方法 const success this._model.placePiece(row, col); if (!success) { console.log(落子无效位置已有棋子或游戏已结束); } } } onDestroy() { this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); } }BoardManager组件视图管理器 它创建模型并将模型传递给BlockInput同时监听模型事件来更新棋子视图。// BoardManager.ts (补充) start() { this._model new GameModel(); this.initBoardView(); // 将模型传递给交互控制器 const blockInputComp this.node.parent.getChildByName(BlockInput)?.getComponent(BlockInput); if (blockInputComp) { blockInputComp.setModel(this._model); } // 监听模型事件 this._model.eventTarget.on(onPiecePlaced, this.onPiecePlaced, this); this._model.eventTarget.on(onGameReset, this.onGameReset, this); this._model.eventTarget.on(onGameOver, this.onGameOver, this); }UIManager组件 它同样监听模型的事件来更新界面上的状态文字。// UIManager.ts export class UIManager extends Component { property(Label) public statusLabel: Label null!; private _model: GameModel null!; start() { // 如何获取model可以从BoardManager传来或者设计成单例/全局访问。这里假设通过依赖注入。 // this._model ... this._model.eventTarget.on(onPiecePlaced, this.updateStatus, this); this._model.eventTarget.on(onGameOver, this.onGameOver, this); this.updateStatus(); } private updateStatus() { if (this._model.gameState GameState.Playing) { this.statusLabel.string this._model.currentPlayer PieceType.Black ? 黑方回合 : 白方回合; } } private onGameOver(data: {winner: PieceType}) { if (data.winner PieceType.Black) { this.statusLabel.string 游戏结束黑方胜利; } else if (data.winner PieceType.White) { this.statusLabel.string 游戏结束白方胜利; } else { this.statusLabel.string 游戏结束平局; } } }至此一个完整的数据流闭环就形成了用户点击 -BlockInput捕获并转换坐标 - 调用GameModel.placePiece- 模型更新数据并派发事件 -BoardManager和UIManager监听事件并更新视图。整个过程清晰解耦。4.3 实现简单AI随机落子人机对战电脑对手的AI是灵魂。我们从最简单的开始随机选择一个空位落子。在GameModel中提供一个方法获取所有空位。public getEmptyPositions(): [number, number][] { const empties: [number, number][] []; for (let i 0; i 15; i) { for (let j 0; j 15; j) { if (this._board[i][j] PieceType.Empty) { empties.push([i, j]); } } } return empties; }创建一个AIController组件它同样监听游戏状态。当轮到AI例如白方时延迟一段时间后执行落子。// AIController.ts export class AIController extends Component { private _model: GameModel null!; private _aiPlayer: PieceType PieceType.White; // AI执白 private _isAITurn: boolean false; start() { // 获取model this._model.eventTarget.on(onPiecePlaced, this.onPlayerMove, this); this._model.eventTarget.on(onGameReset, this.onGameReset, this); } private onPlayerMove() { if (this._model.gameState ! GameState.Playing) return; // 检查当前回合是否是AI if (this._model.currentPlayer this._aiPlayer) { this._isAITurn true; // 延迟一段时间模拟思考避免立即响应显得不自然 this.scheduleOnce(this.makeAIMove, 0.5); } } private makeAIMove() { if (!this._isAITurn || this._model.gameState ! GameState.Playing) return; const empties this._model.getEmptyPositions(); if (empties.length 0) { const randomIndex Math.floor(Math.random() * empties.length); const [row, col] empties[randomIndex]; this._model.placePiece(row, col); } this._isAITurn false; } private onGameReset() { this._isAITurn false; this.unschedule(this.makeAIMove); } }这个AI虽然笨但已经完成了“自动对战”的闭环。你可以在此基础上轻松替换makeAIMove方法实现更复杂的算法如贪心算法、极小化极大算法等。5. 常见问题与排查技巧实录在开发过程中我遇到了不少典型问题这里记录下排查思路和解决方案。5.1 事件相关典型问题问题1点击事件完全没有反应。检查点1节点是否可交互确保接收点击的节点如BlockInput的enabled属性为true并且其UITransform组件的ContentSize覆盖了期望的点击区域。一个快速测试方法是给该节点临时加一个带颜色的Sprite组件看看它是否可见且大小位置正确。检查点2是否被其他节点遮挡Cocos Creator中节点树靠下的节点在层级管理器中显示在下面会渲染在上层并可能拦截触摸事件。确保你的交互层节点在渲染顺序上位于可被点击的位置。可以尝试暂时禁用可能遮挡的节点。检查点3事件监听注册成功了吗在start或onEnable方法中添加console.log确认监听代码被执行了。确保this指向正确特别是在使用箭头函数或需要绑定时。检查点4项目设置中的事件系统如前所述检查项目设置 - 功能裁剪确保没有误删事件模块。问题2点击位置坐标计算不准落子总是偏移。根源坐标空间转换错误。这是最高频的问题。再次确认你使用的转换方法convertToNodeSpaceAR基于锚点和convertToNodeSpace基于左下角区别很大通常使用AR版本更直观。打印出每一步的坐标event.getUILocation(),localPos.x,localPos.y以及计算后的row,col。对照你棋盘节点的实际位置和锚点通常是(0.5, 0.5)进行检查。关键技巧在BlockInput节点下创建一个子节点DebugPos在onTouchEnd中将其位置设置为localPos你可以直观地看到点击的本地坐标原点在哪里。检查网格大小(gridSize)和棋盘中心偏移量的计算是否正确。如果棋盘节点中心不在世界原点计算行列时需要加上偏移。问题3重复监听导致事件触发多次。现象点击一次逻辑执行了两次或更多。原因事件监听器被重复注册了。常见于脚本组件被多次addComponent。在onEnable中注册监听但在onDisable中没有取消导致节点重复激活时监听器叠加。全局事件在多个地方被监听且没有正确管理生命周期。解决严格遵守“在哪监听在哪取消”的原则。在onDestroy或onDisable中使用off方法取消监听并且确保on和off的回调函数是同一个引用使用类方法并绑定this是最稳妥的方式。5.2 游戏逻辑与性能问题问题4AI落子或玩家快速点击导致状态错乱。现象游戏状态如_isAITurn判断失效可能连续落子或AI在玩家回合行动。原因异步操作和事件触发顺序问题。scheduleOnce是异步的如果在延迟期间玩家又点击了或者游戏被重置状态可能被污染。解决加锁在AIController中设置一个_isProcessing标志在开始AI思考和落子期间设为true阻止新的AI决策。及时清理在onGameReset中不仅重置标志还要用this.unscheduleAllCallbacks()取消所有计划任务。模型层校验在GameModel.placePiece方法入口处进行严格的状态和输入校验如游戏是否已结束、位置是否为空这是最后一道防线。问题5动态生成的棋子节点越来越多可能导致内存泄漏或性能下降。解决对象池对于频繁创建和销毁的节点如棋子、特效一定要使用对象池(cc.NodePool)。在我们的例子中虽然一局游戏最多225个棋子但使用对象池是良好的习惯。及时销毁在游戏重置onGameReset时遍历_pieceMap手动调用pieceNode.destroy()。确保在节点销毁前从地图中移除引用。避免在update中创建节点创建和销毁节点的操作尽量放在事件回调中而不是每帧执行的update里。5.3 调试与优化技巧善用Cocos Creator调试器在浏览器中运行游戏打开开发者工具。在Sources面板中可以给你的TypeScript源码打断点。在Console中多打印关键变量坐标、状态、行列索引。可视化调试如前所述创建调试节点来显示坐标。还可以在棋盘格子上动态创建编号Label方便快速定位行列。性能预览对于简单的五子棋性能通常不是问题。但可以打开Chrome的Performance面板录制一段时间查看是否有不必要的重绘或频繁的垃圾回收。确保事件监听器在场景切换时被正确清理。代码结构反思随着AI逻辑变复杂GameModel可能会膨胀。考虑将胜负判断checkWin、AI算法单独抽离成工具类或独立的类保持模型简洁。AIController也可以进一步抽象定义IAIStrategy接口方便切换不同难度的AI。通过这个项目你将深刻体会到在Cocos Creator中开发游戏“节点”是骨骼“组件”是肌肉而“事件”是神经系统。处理好它们之间的关系就能构建出响应迅速、结构清晰的游戏。从随机AI开始试着为它加入一些简单的规则比如优先下在对方棋子旁边你会立刻感受到游戏性提升的乐趣。这只是一个起点基于这个框架你可以尝试加入悔棋、存盘读盘、甚至网络对战功能每一步都是对Cocos Creator更深层次的理解。
返回列表