简介:基于cocos creator开发的单机中国象棋资源,面向游戏开发学习者与象棋算法爱好者。电脑AI采用经典Alpha-Beta剪枝算法,并划分简单、普通、困难三档棋力,困难模式已具备较强对战能力,可直接体验或作为策略游戏研究样例。整套资源共730个文件,压缩包约17.89MB,以json配置、png图片、prefab预制体、js脚本、map地图等为主,另含plist图集、ttf字体、fire场景及mp4演示视频,覆盖从场景搭建、界面资源到核心逻辑的完整工程结构。目前已有1275人学习下载。读者可借此了解Cocos Creator工程的组织方式,分析AI走棋与剪枝的实现思路,也可替换资源或扩展棋力算法,适合有一定JavaScript基础、想动手实践棋类AI的开发者。 做了几年小游戏,这阵子有个老同学让我帮忙做一款中国象棋的Demo,说是要用 Cocos Creator 打包成APK,给家里的老爷子在手机上玩。说实话,市面上现成的象棋源码不少,但要么是Laya的、要么是白鹭的,直接拿来在 Cocos Creator 里跑总会有一堆兼容问题。与其改别人的烂摊子,不如从头捋一遍。
这篇文章我就把从零开始做《中国象棋 Cocos Creator 版》的完整思路和关键代码分享出来,包括棋盘渲染、走棋规则、简单的电脑AI,以及打包安卓APK时容易踩的坑。想自己动手写一个棋类游戏的朋友,这篇应该能帮你省下不少时间。
1. 项目整体设计:先把棋局想明白
1.1 为什么用 Cocos Creator 2.x 而不是 3.x
先说结论:这个项目用的是 Cocos Creator 2.4.x。原因很直接——2.4 的 API 稳定、文档全、社区资料丰富,打包 APK 的流程也相对成熟。3.x 虽然新,但改版幅度大,很多老教程直接白看,而且 2D 棋类游戏根本用不上 3D 那套东西,没必要为了新版而新版。
如果你的项目是从零开始且必须用 3.x,那核心思路也是一样的,就是组件挂载、坐标转换、事件监听这些 API 名字变了而已。
1.2 功能需求拆解
拿象棋来说,最基本的玩法闭环是:
- 棋盘渲染(10横线 × 9竖线,楚河汉界、九宫、炮位标记)
- 棋子摆盘(红黑双方各16子)
- 走棋规则(车的直线、马的日字、象的田字、士的斜线、将帅九宫、炮的翻山)
- 吃子判定(不能吃自己的子,不能送将,将帅不能照面)
- 胜负判定(将帅被将死、困毙)
- 简单电脑AI(残局或者随机走子,做Demo够了)
这里最容易被新手忽略的是“走棋合法性校验”。很多第一次写象棋的人只顾着棋子移动,忘了“走完这步棋之后,自己的帅(将)还在不在”这个核心规则——也就是所谓的“不能送将”。这个必须在所有走棋逻辑的最后统一校验,而不是每个棋子单独判断,否则会出现“吃掉了对方的老将但自己也被将军”这种逻辑漏洞。
1.3 数据模型设计
棋局的数据结构我用了一个一维数组,长度90,对应9行×10列。节点下标和坐标的换算公式是:
行 = Math.floor(index / 9) 列 = index % 9棋子的类型用数字表示:
| 数字 | 含义 |
|---|---|
| 0 | 空位 |
| 1 | 帅 |
| 2 | 仕 |
| 3 | 相 |
| 4 | 马 |
| 5 | 车 |
| 6 | 炮 |
| 7 | 兵 |
黑方用负数表示:-1为将,-7为卒,以此类推。这样判断敌我只需要value * value > 0(同号是自己人,异号是敌人)。
这个模型的好处是:判断吃子只需要检查目标位置的值是否是异号;渲染棋子时用value的绝对值找对应的贴图资源即可,非常干净。
2. 棋盘与棋子的渲染:Cocos Creator 里的坐标换算
2.1 棋盘节点搭建
在场景里建一个空节点作为棋盘根节点,挂一个Graphics组件用来画线,这样不用准备棋盘图片素材,纯代码绘制,后续换皮肤也方便。
关键代码大概是这样的:
const g = this.node.getComponent(Graphics); g.lineWidth = 2; g.strokeColor = new Color(72, 46, 23, 255); // 画横线10条 for (let i = 0; i < 10; i++) { g.moveTo(0, i * CELL_SIZE); g.lineTo(8 * CELL_SIZE, i * CELL_SIZE); } // 画竖线9条(中间第四到第五行之间断开,留出河界) for (let j = 0; j < 9; j++) { if (j === 0 || j === 8) { g.moveTo(j * CELL_SIZE, 0); g.lineTo(j * CELL_SIZE, 9 * CELL_SIZE); } else { g.moveTo(j * CELL_SIZE, 0); g.lineTo(j * CELL_SIZE, 4 * CELL_SIZE); g.moveTo(j * CELL_SIZE, 5 * CELL_SIZE); g.lineTo(j * CELL_SIZE, 9 * CELL_SIZE); } } // 画九宫斜线、炮位、兵位等标记……炮位和兵位的标记是棋盘美感的细节。炮位我用两个小的交叉线段表示,兵位用小圆点。这些都可以用Graphics的circle方法画出来,注意坐标要落在交叉点上,别偏移。
2.2 棋子的坐标换算
棋子在棋盘上的位置不是标准网格坐标,而是交叉点。所以棋子的摆盘要按“交叉点”定位,而不是“格子中心”。
坐标换算的核心是:先把棋盘的左下角锚点对齐到场景坐标,然后:
x = col * CELL_SIZE + BOARD_OFFSET_X y = row * CELL_SIZE + BOARD_OFFSET_Y这里最容易出问题的是行、列的方向。Cocos Creator 的坐标系是 y 轴向上,和数组的行索引正好相反。我建议统一用“行从下往上、列从左往右”来定位棋子,即[0][0]对应棋盘左下角。这样在写走棋规则时,向上走就是row + 1,向下走就是row - 1,符合直觉。
2.3 棋子的节点结构与点击事件
每个棋子是一个独立的Sprite节点,挂一个Pieces脚本组件,记录两个属性:Row和Col,以及Value。点击事件用Node的on(Node.EventType.TOUCH_END)来监听。
注意:棋子节点一定要记得UITransform的ContentSize设置得比棋子图片大一些,比如棋子直径64,触摸区域可以设到80。否则手指粗一点的用户点边缘会没反应。实测下来这是很多新手最容易忽略的体验细节。
3. 走棋规则与AI实现:不能只会移棋子
3.1 走棋合法性的完整校验流程
每个棋子的走法规则,本质上就是返回一组“可以去的坐标列表”。比如车:
getValidMoves(row, col, board) { let moves = []; const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; for (let d of dirs) { let r = row + d[0]; let c = col + d[1]; while (inBoard(r,c)) { if (board[r][c] === 0) { moves.push([r,c]); } else { if (board[r][c] * this.value < 0) { moves.push([r,c]); // 吃子 } break; // 被挡住 } r += d[0]; c += d[1]; } } return moves; }马的走法要注意“蹩马腿”,兵要注意“过河前后方向不同”,士和将会被限制在九宫内,炮吃子时必须隔一个棋子。
3.2 不能送将:所有走法的最后一道关卡
我之前写过一个版本,单独校验每个棋子的移动,结果出现了一个很隐蔽的Bug:红车可以移动到某一列去将军,但它自己移动到那个位置后,黑方的炮正好在那条线上隔着马瞄着红帅,也就是说红方这一步把自家的帅暴露在炮的威胁下了。这个Bug在“吃子后自曝”的场景里特别容易出现。
解决方法很朴素:写一个isKingAliveAfterMove(fromRow, fromCol, toRow, toCol, board)函数,模拟走子之后,检查己方帅/将是否被攻击。如果是,则这一步非法。这个函数对所有的“合法移动”结果都要过滤一遍,不能只靠单个棋子的走法列表。
模拟走棋时的写法要注意:先深拷贝棋盘数组,再交换值,再校验。浅拷贝只会让你改到原数组,排查半天指针问题。
3.3 电脑AI:Demo级别的简单评估函数
做给长辈玩的Demo,AI不要求多强,能走就行。我用了最简单的“贪心评估”:
- 对每一步可能走法,计算走完后己方棋子的总价值与对方棋子的总价值之差
- 棋子价值:车9、马4、炮4.5、兵1、相2、仕2、将1000
- 选择价值增量最大的走法
如果是人机对战,让电脑走一步,等500毫秒再走,避免瞬间移动让人困惑。
function getBestMove(board, isRed) { let bestScore = -Infinity; let bestMove = null; for (let r=0; r<10; r++) { for (let c=0; c<9; c++) { let v = board[r][c]; if (isRed && v <= 0) continue; if (!isRed && v >= 0) continue; let moves = getValidMovesWithCheck(r,c,board); for (let m of moves) { let score = evaluate(board, r, c, m[0], m[1]); if (score > bestScore) { bestScore = score; bestMove = {from:[r,c], to:m}; } } } } return bestMove; }这个AI的强度大概介于“完全不会下的人”和“刚会规则的新手”之间,作为Demo展示完全够用。想要更强就得加搜索深度和剪枝,但体感没必要,毕竟棋盘上棋子越多搜索越慢,手机端要想流畅只能在启动时把评估函数算完缓存起来。
在实现时还有个小细节:评估函数里,如果吃到了对方的将/帅,分数直接给最高。这样AI会优先去捉将,而不会傻乎乎地吃个卒。
3.4 和棋与特殊局面的判断
如果没做“自然限着”或者“60回合不吃子判和”,你会碰到一个问题:AI和人对弈,双方就剩那么几个棋子互相转悠,永远结束不了。Demo可以简单处理——连续40步没有吃子就提示和棋。这个数值和正式规则有出入,但在实际游玩场景中体验不错,不会让人烦躁。
4. 打包APK:Cocos Creator 生成安卓安装包全流程
4.1 环境准备与构建
打包APK涉及两个工具链,缺一不可:
- Android Studio(用于编译和签名,这里只用到 SDK 和 Build Tools)
- Cocos Creator 的构建发布面板
构建步骤:
- 在 Creator 菜单栏选择“项目 → 构建发布”
- 平台选择“Android”
- 填好包名,建议用
com.xxx.chess这种格式,不要用默认的com.creator.empty,不然后面签名会有坑 - 构建模式选“Release”,源码模式不用管
- 点击构建,等待生成
build/android工程
构建完成后,用 Android Studio 打开生成的原生工程,等 Gradle 同步完,然后选择 Build → Build Bundle(s) / APK(s) → Build APK(s)。
4.2 构建过程中常见的打包错误
我在这里踩过几个实实在在的坑:
坑一:Gradle 版本与 SDK 版本不匹配Cocos Creator 2.4 自带的 Gradle 版本比较老,而新安装的 Android Studio 默认用的 JDK 是 17 或更高,构建时会直接报错。解决办法是在gradle-wrapper.properties里指定一个支持的 Gradle 版本,同时把项目的 JDK 版本调到 1.8。
坑二:找不到local.properties新克隆的构建工程没有这个文件,Android Studio 会报 SDK location not found。直接在local.properties里写:
sdk.dir=C:\\Users\\你的用户名\\AppData\\Local\\Android\\Sdk注意 Windows 路径里的反斜杠要写成两个,否则解析失败。
坑三:64位和32位 so 库问题Cocos Creator 2.4 默认打包包含 arm64-v8a 和 armeabi-v7a 两个架构的 so 库,如果你在构建面板只勾了一个,但手机上运行还是报找不到 so,往往是 Android 模拟器是 x86 架构。真机一般没这个问题,开发调试时尽量用真机。
4.3 APK瘦身与首屏优化
一个空项目打包出来就有几十MB,象棋这种2D小游戏纯粹是让玩家等加载,体验很差。我做了三件事把包压到了 8MB 左右:
- 棋盘、棋子素材用 PNG 压缩格式,不要用大尺寸透明背景图
- 把其他语言和不需要的加载资源删掉:构建发布面板里去掉不必要的“图集”和“音频”
- 关闭“调试模式”,选“发布模式”
首屏方面,这个体量的游戏加载很快,没必要做闪屏,但如果你加了启动动画,记得把加载脚本挂到常驻节点上,保证场景切换时初始化代码不会重复执行。
5. 常见问题与调试技巧实录
5.1 棋子点击无响应怎么办
首选检查UITransform的大小和节点的 zIndex。棋子之间如果有重叠,后面的节点会挡住前面的触摸事件。建议所有棋子的 zIndex 设为同一个值,选中时临时把 zIndex 调高,走完再调回来。这样就不会出现“兵被马挡住点不到”的情况。
5.2 走棋后棋盘坐标错乱
出现坐标偏移,基本可以断定是“行”的方向搞反了。Cocos Creator 的getPosition()是左下角为原点,但你在视觉上习惯“最上面一行是第一行”。解决方式是在初始化棋子位置时,直接把第一行放到(0, 8 * CELL_SIZE),而不是硬套数组下标。
5.3 高速重复点击导致棋子瞬移
这是事件绑定重复导致的。如果你在挂载on(TOUCH_END)时没有在onDestroy里移除监听,场景切换或重开对局后,同一个节点的触摸回调会被注册两次,点一下会走两步棋。这个Bug排查起来很隐蔽,我当时的表象是“棋子会自己跳一下”。
处理办法是在组件销毁时统一调用this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this),并且所有玩家选择棋子的逻辑入口用同一个方法,保证不会重复绑定。
5.4 如何提高 AI 的响应速度
如果你的 AI 评估函数里有循环嵌套数组拷贝,走一步棋可能要卡个一两秒。在 Demo 阶段最简单的优化是:把可走位置的for循环里,把常量提出来,别在循环体内频繁调用this.getComponent()这类获取组件的方法。
// 每次循环都拿组件 → 很慢 let pieces = this.node.getComponent(Pieces); // 循环前拿一次 → 快得多 const piecesComp = this.node.getComponent(Pieces); for (let m of moves) { piecesComp.moveTo(m); }真机测试的时候,我碰到一次点击完,电脑卡了差不多4秒才有动作,把所有getComponent改成缓存引用、数组用TypedArray之后,这个数值降到了 200ms 以内,体验完全不一样。
5.5 完整对局测试的土办法
自己跟自己下棋,很容易漏掉边角情况。我最后是用了一个“双人模式”来测试的——就是一个屏幕内红黑双方都能点击移动,这样我坐在电脑前左手右手交替下棋,把能想到的残局都走了一遍。测试过程中,一旦出现非法走子没有拦住的情况,就在getValidMovesWithCheck入口处打日志,把棋盘数组 dump 出来。
这个方法虽然土,但效率极高。代码里可以留一个DEBUG_MODE开关,线上版本关掉,开发版本打开,遇到问题直接看打印的棋盘快照,比自己瞎猜坐标快得多。
6. 写在最后的几点经验
打包APK实际跑在Android真机上之后,真机性能和模拟器差距明显。老款手机开高精度渲染反而有可能出现触摸偏移。如果美术素材不是必须高清,建议把Canvas的适配策略设成“固定宽度”,高度自动缩放,这样各种屏幕都能正常显示,不会出现黑边。
另外,如果准备给长辈用,字号和棋子的尺寸尽量大一些。我的棋盘用了 64 像素直径的棋子,在6.7英寸手机上看起来刚好,再小就费眼了。
做这个小项目最大的体会是:棋类游戏表面上逻辑简单,但真正要做得“没人骂”需要花不少功夫在边界条件和交互手感上。强烈建议在写规则时把“走棋合法性”和“走棋效果”分成两个独立模块,前者只负责判断,后者只负责播放动画和刷新UI,这样改AI时完全不用碰渲染代码。
最后再分享一个小技巧:棋子的选中状态用高亮描边比改贴图颜色更直观,也不会破坏原素材。Cocos Creator 2.4 里可以直接给棋子节点的 Sprite 加一层Graphics的圆环描边,选中时显示、落子后清除,代码量很小但手感提升明显。
本文还有配套的精品资源,点击获取