1. 项目概述:从零到一构建跨平台2048
最近在整理过往项目时,翻出了一个用Cocos Creator 3.x版本开发的《2048》小游戏。这个项目麻雀虽小,五脏俱全,完整实现了从核心玩法逻辑、UI交互到最终打包发布到PC(Web)、安卓(APK)和H5(微信小游戏等平台)的全流程。对于刚接触Cocos Creator,或者想了解如何将一个简单想法落地为多端可玩产品的开发者来说,这个项目是一个绝佳的练手样板。它不涉及复杂的图形学和网络通信,能让你集中精力理解Cocos Creator的核心工作流:场景搭建、脚本编写、资源管理和多平台发布。很多人觉得游戏开发门槛高,其实从这样一个经典的益智类小游戏入手,你会发现用现代引擎开发游戏比想象中要直观得多。
2. 核心玩法逻辑设计与实现
2048的规则非常简单:在一个4x4的格子中,通过上下左右滑动,使相同数字的方块合并,每次滑动后会在空白处随机生成一个2或4的方块,目标是合成一个“2048”的方块。但简单的规则背后,需要一套清晰、健壮的逻辑来支撑。
2.1 数据模型:网格与方块状态管理
游戏的核心是一个4x4的二维数组,我们称之为grid。每个格子存储一个Block对象的信息,包括其数字(0表示空)、在网格中的位置(行row和列col)以及对应的场景节点(Node)。我选择将数据与表现分离,grid数组只管理逻辑状态,场景中的方块节点是它的视觉呈现。
// Block.ts - 方块数据类 export class Block { value: number = 0; // 方块数字,0为空 node: Node | null = null; // 关联的Cocos节点 row: number = 0; col: number = 0; // 是否在本轮合并中已合并过,防止连续合并 merged: boolean = false; } // GameManager.ts - 游戏管理器部分代码 export class GameManager extends Component { private grid: Block[][] = []; private readonly GRID_SIZE = 4; start() { this.initGrid(); this.generateNewBlock(); this.generateNewBlock(); } private initGrid() { this.grid = []; for (let r = 0; r < this.GRID_SIZE; r++) { this.grid[r] = []; for (let c = 0; c < this.GRID_SIZE; c++) { this.grid[r][c] = new Block(); this.grid[r][c].row = r; this.grid[r][c].col = c; } } } }为什么选择二维数组?因为它最直观地映射了游戏棋盘,通过grid[row][col]可以快速访问和修改任意位置的方块状态,进行碰撞检测和合并判断非常方便。虽然也可以用一维数组,但二维数组在逻辑清晰度上更胜一筹。
2.2 滑动与合并算法:一次操作的全过程
这是整个游戏最核心的部分。以“向左滑动”为例,其过程可以分解为几个步骤:
- 遍历每一行:从最左侧(第0列)开始,作为目标位置。
- 寻找当前行可移动的方块:从目标位置的下一个格子开始向右查找第一个数字不为0的方块。
- 判断合并条件:
- 如果目标位置为空,则将找到的方块移动过去。
- 如果目标位置有方块,且数字与找到的方块相同,并且目标位置的方块在本轮尚未合并,则执行合并(数字翻倍,分数增加),并标记为已合并,同时将移动过来的方块置空。
- 如果目标位置有方块且数字不同,则将找到的方块移动到目标位置紧邻的右侧空位(如果存在)。
- 完成一行后,进入下一行,重复上述过程。
这里的关键是处理完一行后,需要重置该行所有方块的merged标记,否则下一轮滑动时,已合并的方块将无法再次合并。同时,在滑动开始前,需要备份当前网格状态,以便与滑动后的状态对比,判断是否有方块真的发生了移动或合并,只有发生了改变,才需要在空白处生成新方块。
private moveLeft(): boolean { let moved = false; // 先备份当前状态,用于比较 const oldGrid = this.serializeGrid(); for (let r = 0; r < this.GRID_SIZE; r++) { for (let c = 0; c < this.GRID_SIZE; c++) { this.grid[r][c].merged = false; // 重置合并状态 } } for (let r = 0; r < this.GRID_SIZE; r++) { for (let c = 1; c < this.GRID_SIZE; c++) { // 从第1列开始检查,第0列是目标列 if (this.grid[r][c].value !== 0) { let targetCol = c; // 向左寻找可以移动或合并的位置 for (let k = c - 1; k >= 0; k--) { if (this.grid[r][k].value === 0) { targetCol = k; // 可以移动到空位 } else if (this.grid[r][k].value === this.grid[r][c].value && !this.grid[r][k].merged) { targetCol = k; // 可以合并 break; } else { break; // 遇到不同数字且非空的方块,停止 } } if (targetCol !== c) { moved = this.moveOrMergeBlock(r, c, r, targetCol) || moved; } } } } // 比较滑动前后网格是否变化 if (moved && !this.isGridEqual(oldGrid)) { this.generateNewBlock(); this.checkGameOver(); } return moved; } private moveOrMergeBlock(fromRow: number, fromCol: number, toRow: number, toCol: number): boolean { const fromBlock = this.grid[fromRow][fromCol]; const toBlock = this.grid[toRow][toCol]; if (toBlock.value === 0) { // 移动到空位 toBlock.value = fromBlock.value; fromBlock.value = 0; // 这里触发方块节点的移动动画 this.scheduleMoveAnimation(fromBlock, toBlock); return true; } else if (toBlock.value === fromBlock.value && !toBlock.merged) { // 合并 toBlock.value *= 2; toBlock.merged = true; fromBlock.value = 0; // 更新分数 this.score += toBlock.value; // 触发合并动画(如缩放、粒子效果) this.scheduleMergeAnimation(toBlock); // 销毁被合并的方块节点(或隐藏) if (fromBlock.node) { fromBlock.node.destroy(); fromBlock.node = null; } return true; } return false; }注意:动画与逻辑的分离。在
moveOrMergeBlock函数中,我立即更新了数据模型(grid数组),但方块节点的移动和合并效果是通过动画异步完成的。这意味着在动画播放期间,逻辑上已经可以接受下一次输入了。为了避免冲突,可以设置一个isAnimating标志位,在动画播放期间锁定输入。另一种更优雅的做法是使用Promise或回调函数,等待所有动画完成后再解锁。
2.3 游戏状态判定:胜利、失败与随机生成
胜利条件很简单,遍历grid,检查是否有方块的value达到2048。一旦检测到,即弹出胜利UI,并可以选择继续游戏。
失败(Game Over)的判定稍微复杂一些。不能简单地说没有空格了就失败,因为可能通过一次滑动合并出空格。正确的判定条件是:当前网格已满(无空格)且任意方向(上下左右)上,相邻的格子都不存在数字相同的方块。这意味着没有任何一步可以改变棋盘状态。实现时需要编写一个canMove函数,模拟四个方向的移动,检查是否有可能改变网格。
随机生成新方块的算法需要一点技巧。首先收集所有值为0的格子(空位),然后随机选择一个。生成数字2的概率通常设为90%,数字4为10%,以控制游戏节奏。这里有个细节:如果游戏已处于失败边缘(空格极少),随机算法应保持稳定,不能因为随机数问题导致意外。
3. Cocos Creator工程结构与资源管理
一个清晰的工程结构能极大提升开发效率和维护性。我的项目结构大致如下:
assets/ ├── scripts/ # 所有TypeScript脚本 │ ├── manager/ # 管理器类(GameManager, AudioManager, UIManager) │ ├── block/ # 方块相关逻辑(Block数据类,BlockController组件) │ ├── ui/ # UI面板控制组件(StartPanel, GamePanel, OverPanel) │ └── utils/ # 工具函数(本地存储,数学工具) ├── resources/ # 动态加载的资源(预制体,音效) │ ├── prefabs/ # 预制体(方块预制体,特效预制体) │ └── audio/ # 游戏音效 ├── scenes/ # 游戏场景(Start, Game) └── textures/ # 精灵纹理,UI图片资源管理心得:
- 预制体(Prefab)是核心:方块、UI元素、特效都应做成预制体。这样可以在代码中动态实例化(
instantiate),并且修改预制体后,所有实例都会更新。我的方块预制体上挂载了一个BlockController组件,负责控制该方块的显示(数字、颜色)和播放移动、合并动画。 - 合理使用
resources目录:需要动态加载的资源(如游戏进行中才需要的音效、特效预制体)放在resources下,通过resources.load加载。而场景直接引用的资源(如背景图、常驻UI)可以直接拖拽引用。 - 图集(Sprite Atlas)优化:虽然2048资源不多,但养成好习惯。将多个小图(如数字文字、按钮图标)打包成一个图集,可以减少Draw Call,对H5平台性能提升尤其明显。Cocos Creator内置了自动图集功能。
4. UI/UX设计与动画效果实现
好的体验能让简单的游戏增色不少。2048的UI主要包括开始界面、游戏主界面(含分数、最高分、重新开始按钮)和结束弹窗。
4.1 自适应布局与多端适配
Cocos Creator的Widget(对齐挂件)和Layout(布局组件)是解决适配问题的利器。我的做法是:
- 确定安全区域:将Canvas的Design Resolution(设计分辨率)设为1080x1920(竖屏)或1920x1080(横屏,根据你的设计)。为Canvas节点添加Widget组件,设置上下左右对齐,确保背景铺满。
- 游戏棋盘居中:棋盘作为一个节点,使用Widget使其水平垂直居中。或者使用Layout组件中的Vertical/Horizontal Layout。
- 分数和按钮布局:分数文本和按钮通常放在顶部或底部,使用Widget的Top对齐,并设置合适的边距(Top, Left/Right)。
多端差异处理:PC端通常用鼠标点击和键盘,移动端是触摸。Cocos Creator的EventTarget系统统一了输入事件。对于按钮,直接使用Button组件即可。对于滑动检测,我监听Canvas节点的touchMove事件,计算触摸起始和结束的位移向量,根据向量的x和y分量绝对值大小来判断是横向滑动还是纵向滑动。
// 输入控制简化示例 private touchStartPos: Vec2 = new Vec2(); private minSwipeDistance: number = 50; // 最小滑动判定距离 onLoad() { this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this); this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); } onTouchStart(event: EventTouch) { const touches = event.getTouches(); this.touchStartPos.set(touches[0].getLocation()); } onTouchEnd(event: EventTouch) { const touches = event.getTouches(); const endPos = touches[0].getLocation(); const delta = endPos.subtract(this.touchStartPos); if (delta.length() < this.minSwipeDistance) return; // 判断方向 if (Math.abs(delta.x) > Math.abs(delta.y)) { // 横向滑动 if (delta.x > 0) this.moveRight(); else this.moveLeft(); } else { // 纵向滑动 if (delta.y > 0) this.moveUp(); else this.moveDown(); } }4.2 方块动画与特效
平滑的动画能极大提升手感。主要涉及两种动画:
- 移动动画:当方块从A格移到B格,不是直接设置位置,而是使用
tween(补间动画)系统。import { tween } from 'cc'; scheduleMoveAnimation(fromBlock: Block, toBlock: Block) { if (!fromBlock.node) return; const targetWorldPos = this.getWorldPositionByGrid(toBlock.row, toBlock.col); tween(fromBlock.node) .to(0.15, { worldPosition: targetWorldPos }, { easing: 'sineOut' }) .start(); // 动画结束后,更新节点关联关系 toBlock.node = fromBlock.node; fromBlock.node = null; } - 合并动画:合并时,目标方块可以播放一个短暂的“放大-缩小”弹性动画,并伴随一个粒子爆发效果(ParticleSystem),增加爽快感。
- 新方块出现动画:新生成的方块可以从透明逐渐显现(
opacity从0到255),或者从小放大。
性能注意:避免在同一帧创建大量tween或粒子。对于2048,同时移动的方块最多12个,完全在可控范围内。如果动画复杂,可以考虑使用对象池(NodePool)来管理方块节点,避免频繁的创建和销毁。
5. 多平台打包发布实战
这是将你的作品交付到玩家手中的最后一步,也是坑最多的一步。Cocos Creator的构建系统很强大,但不同平台需要不同的配置。
5.1 PC Web平台(HTML5)
这是最简单的。在构建面板中,选择Web Mobile或Web Desktop(取决于你的游戏是横屏还是竖屏)。
- 发布路径:选择一个空文件夹。
- 主包压缩类型:选择
合并所有JSON,可以减少请求数量。 - 内联所有SpriteFrame:如果图片资源不多,可以勾选,将所有图片打包进一个文件中,进一步减少HTTP请求。但对于2048这样的小游戏,不勾选也没问题。
- MD5 Cache:建议勾选,会给资源文件名加上哈希值,有利于浏览器缓存。
点击构建,完成后点击运行,即可在浏览器中预览。你可以将这个build目录下的所有文件部署到任何静态网站服务器(如Nginx, GitHub Pages, Vercel)上。
5.2 安卓(Android)平台
安卓打包需要先配置好开发环境。
- 安装JDK:确保安装了Java Development Kit (JDK) 8或以上版本,并配置好
JAVA_HOME环境变量。 - 安装Android SDK:可以通过Android Studio安装,或者单独下载SDK Command-line Tools。需要配置
ANDROID_HOME环境变量。 - 安装NDK(可选,但某些原生功能需要):从Android官网下载,并在Cocos Creator的
偏好设置->原生开发环境中配置路径。 - 在构建面板配置:
- 选择
Android平台。 Package Name:填写你的应用包名,如com.yourcompany.game2048,这是应用的唯一标识。Target API Level和Minimum API Level:根据你的目标用户设备设置,一般Minimum设为21(Android 5.0)可以覆盖绝大多数设备。App ABI:通常选择armeabi-v7a和arm64-v8a即可覆盖主流ARM架构手机。如果为了减小包体,可以只选arm64-v8a。- 签名:发布到应用市场必须使用签名。你可以生成一个正式的签名密钥文件(.keystore),并在构建时勾选
使用调试版本(开发测试)或填入你的正式签名信息。
- 选择
踩坑实录:
cannot read property 'uuid' of null错误。这个错误在升级Cocos Creator版本或项目迁移后偶尔会出现。它通常是因为资源meta文件损坏或引用丢失。解决方法:尝试项目->刷新资源管理器和项目->重新导入资源。如果不行,检查控制台报错的具体资源路径,去assets目录下找到对应的.meta文件,将其删除,然后刷新项目,让引擎重新生成。务必提前备份。
点击构建后,会生成一个.apk文件(在build/android目录下),可以安装到手机或模拟器上测试。
5.3 H5小游戏平台(以微信小游戏为例)
微信小游戏有自己特殊的运行环境和API,Cocos Creator提供了很好的支持。
- 安装并打开微信开发者工具。
- 在Cocos Creator构建面板:
- 选择
微信小游戏平台。 开放数据域:如果你的游戏有排行榜等需要隔离的数据,才需要配置。2048不需要。appid:填写你在微信公众平台申请的小游戏AppID。如果只是测试,可以勾选使用测试appid。远程服务器地址:小游戏首次启动后,资源会从这个地址下载。开发阶段可以留空,资源会打包在主包内。上线前需要配置为你的CDN地址。
- 选择
- 构建完成后,用微信开发者工具打开
build/wechatgame目录。你需要在开发者工具中点击详情->本地设置,勾选不校验合法域名...(开发阶段),才能正常请求本地资源或测试服务器。 - 适配微信API:比如分享、登录、激励视频广告等。Cocos Creator提供了
WX全局对象,你可以通过if (typeof wx !== 'undefined')来判断环境并调用微信API。例如,将游戏分数提交到微信开放数据域用于排行榜:if (typeof wx !== 'undefined') { wx.getOpenDataContext().postMessage({ command: 'updateScore', score: this.currentScore }); }
将游戏打包为单HTML文件:这是一个常见需求,用于在个别特殊渠道分发。Cocos Creator默认的Web构建会生成一堆文件。要实现单HTML,需要勾选构建面板中的内联所有SpriteFrame,并且手动修改index.html,将main.js等脚本内容以内联<script>标签的形式插入到HTML中,同时将图片资源转换为Base64编码内联。但请注意,这会使HTML文件变得非常大,不适合网络加载。社区有一些插件或脚本可以实现自动化,但官方并未直接支持。
6. 性能优化与常见问题排查
即使对于2048这样的小游戏,优化也能让体验更流畅,尤其是在低端手机或Web平台。
6.1 性能优化要点
- Draw Call优化:Draw Call是CPU向GPU发起绘制命令的次数,越少越好。2048的瓶颈通常在于UI和方块。
- 使用图集:将所有的数字图片、按钮图标打包进一个图集。
- 合并静态UI:将不变化的UI元素(如背景、静态按钮)尽可能放在同一个渲染批次中。可以尝试使用
UI Batch组件(Cocos Creator 3.x中相关功能已集成在渲染流程中,合理组织节点层级即可)。 - 动态合批:对于大量相同材质的动态方块,引擎会自动尝试动态合批。确保它们的材质、纹理相同。
- 节点管理:
- 使用节点池(NodePool):对于频繁创建和销毁的方块节点,使用节点池可以避免内存抖动和GC(垃圾回收)压力。当方块被合并“消失”时,不是
destroy,而是放回池子;需要新方块时,从池子中取出复用。
// 简化的节点池示例 import { NodePool, Prefab } from 'cc'; export class BlockPool { private pool: NodePool = new NodePool('Block'); init(prefab: Prefab, count: number) { for (let i = 0; i < count; i++) { this.pool.put(instantiate(prefab)); } } get(): Node { if (this.pool.size() > 0) { return this.pool.get(); } return instantiate(this._prefab); // 池空则新建 } put(node: Node) { this.pool.put(node); } } - 使用节点池(NodePool):对于频繁创建和销毁的方块节点,使用节点池可以避免内存抖动和GC(垃圾回收)压力。当方块被合并“消失”时,不是
- JavaScript/TypeScript性能:
- 避免在
update等高频函数中创建临时对象(如new Vec2())。可以在类属性中预先声明并复用。 - 对于频繁调用的函数(如判断游戏结束的
canMove),如果计算量大,可以考虑缓存结果。
- 避免在
6.2 常见问题与解决方案实录
问题:在微信小游戏平台,图片加载失败或显示为黑块。
- 排查:首先检查构建后的
res/import目录下图片资源是否存在。然后检查微信开发者工具控制台是否有网络错误。 - 解决:大概率是
远程服务器地址未正确配置,但代码中尝试从远程加载。确保开发阶段远程服务器地址为空,或者正确指向了包含资源的服务器地址。另外,微信小游戏要求所有网络请求的域名都必须在小游戏管理后台配置,并在开发者工具中开启不校验域名选项进行测试。
- 排查:首先检查构建后的
问题:安卓打包后,安装到手机运行崩溃(闪退)。
- 排查:这是最头疼的问题。首先连接手机,通过
adb logcat命令查看Android系统日志,过滤Unity、cocos或你的包名相关的Fatal、Error信息。 - 常见原因与解决:
- 原生库不兼容:如果你的项目包含了C++原生插件(.so文件),请检查
App ABI设置是否与插件支持的架构匹配。对于纯TypeScript的2048,一般不会有此问题。 - 内存不足:低端设备上,如果资源加载过多可能导致OOM。检查构建时是否勾选了不必要的资源,尝试压缩图片(使用
.webp格式,在Cocos Creator中配置压缩选项)。 - 启动黑屏时间长:可能是首包资源太大。在构建面板的
压缩纹理选项中,为Android选择合适的压缩格式(如ASTC),并勾选分包选项,将部分资源放到子包中按需加载。
- 原生库不兼容:如果你的项目包含了C++原生插件(.so文件),请检查
- 排查:这是最头疼的问题。首先连接手机,通过
问题:游戏在PC浏览器运行正常,但打包后触摸/点击失灵。
- 排查:检查UI按钮的
Interactable属性是否为true,以及是否被其他全屏节点遮挡(如一个透明的、未设置按钮的节点覆盖了整个屏幕,会吞掉点击事件)。 - 解决:确保点击事件绑定正确。在移动端,确认触摸事件监听是否添加到了正确的节点(通常是Canvas)。使用Cocos Creator提供的
Button组件或EventTarget系统通常能避免此问题。
- 排查:检查UI按钮的
问题:如何实现本地存档(保存最高分、游戏进度)?
- 解决:使用
sys.localStorage。这是一个跨平台的本地存储接口。
// 保存 sys.localStorage.setItem('game_high_score', this.highScore.toString()); // 读取 const saved = sys.localStorage.getItem('game_high_score'); this.highScore = saved ? parseInt(saved) : 0;- 注意:在微信小游戏等平台,
sys.localStorage有容量限制(约10MB)。对于2048的简单数据完全够用。
- 解决:使用
这个2048项目虽然基础,但完整走通了Cocos Creator开发小游戏的核心链路。从数据建模、算法实现、UI交互到多平台发布,每一步都藏着只有亲手做过才会知道的细节。特别是平台发布环节,耐心阅读官方文档,善用开发者工具调试,大部分问题都能找到答案。源码的价值不在于它实现了2048,而在于它提供了一个干净、可扩展的框架,你可以基于它轻松地修改规则(比如5x5的棋盘)、更换皮肤,或者添加道具、成就系统,把它变成你自己的独特作品。