
1. 项目概述从零到一构建消除手游最近用 Cocos Creator 3.8 完整走通了一个消除类手游的开发流程从核心算法设计到炫酷动效实现再到微信小游戏的打包与适配。整个过程踩了不少坑也积累了一些实战心得。消除游戏看似简单但要做好玩、流畅、适配多端里面的门道其实不少。这篇文章我就以一个实战项目的视角拆解其中的关键技术点分享我的实现方案和避坑经验。无论你是刚接触 Cocos 的新手还是想优化现有项目的开发者相信都能从中找到一些可以直接“抄作业”的干货。这个项目最终的目标是产出能在微信小游戏平台流畅运行的消除游戏。因此我们的技术栈非常明确Cocos Creator 3.8 作为核心开发引擎TypeScript 作为主要编程语言同时需要深度考虑微信小游戏平台的特性和限制。整个开发流程可以拆解为三个核心模块游戏核心逻辑与算法、视觉表现与动效、平台适配与发布。接下来我会逐一深入不仅告诉你“怎么做”更会解释“为什么这么做”。2. 核心玩法与算法设计消除游戏的核心乐趣在于“连消”和“策略性”。我们首先要搭建一个稳固的底层逻辑框架。2.1 数据结构与棋盘生成消除游戏的棋盘本质上是一个二维网格。在 Cocos Creator 中我们通常用一个二维数组number[][]或自定义的Block[][]在内存中维护这个逻辑棋盘而场景中的节点Node是其视觉表现。数据结构定义我定义了一个Block类来封装每个格子的信息它不直接继承cc.Component而是一个纯数据类。这样做是为了将逻辑与渲染分离便于测试和优化。// Block.ts - 逻辑数据类 export class Block { type: number; // 方块类型如0红色1蓝色... row: number; col: number; node: cc.Node | null null; // 关联的渲染节点 isCleared: boolean false; // 是否已被消除 constructor(type: number, row: number, col: number) { this.type type; this.row row; this.col col; } }棋盘管理器然后创建一个BoardManager单例来管理整个棋盘。它的职责包括初始化棋盘、检查消除、处理消除后的下落和新块填充。// BoardManager.ts export class BoardManager { private static _instance: BoardManager; public static get instance(): BoardManager { if (!this._instance) { this._instance new BoardManager(); } return this._instance; } private _width: number 8; private _height: number 8; private _board: Block[][]; private _blockTypes: number 5; // 5种基础方块 initBoard(): void { this._board []; // 生成初始棋盘需避免开局即出现可消除组合 do { for (let r 0; r this._height; r) { this._board[r] []; for (let c 0; c this._width; c) { // 随机生成类型但需要做“无解”检查 let type Math.floor(Math.random() * this._blockTypes); this._board[r][c] new Block(type, r, c); } } } while (this.checkBoardHasMatches()); // 如果开局就有可消除的重新生成 this.createBoardNodes(); // 根据逻辑数据创建视觉节点 } }注意开局生成时一定要加入“无解检查”。否则玩家一进来就看到一堆可以消除的方块体验很糟糕。我的做法是生成后立刻调用一次消除检测算法如果发现任何可消除组合就重新生成整个棋盘直到生成一个“干净”的初始局面。虽然有小概率会多循环几次但在8x8的棋盘和5种方块的配置下通常一次就能成功。2.2 消除检测算法DFS与并查集的选择检测相连的同色方块是消除算法的核心。常见的有深度优先搜索DFS和并查集Union-Find两种思路。1. 深度优先搜索DFS这是最直观的方法。从某个方块出发向上下左右四个方向递归探索收集所有相连的同类型方块。实现简单适合初学者理解。private getConnectedBlocksDFS(startRow: number, startCol: number): Block[] { const targetType this._board[startRow][startCol].type; const visited: boolean[][] Array.from({length: this._height}, () new Array(this._width).fill(false)); const result: Block[] []; const dfs (r: number, c: number) { if (r 0 || r this._height || c 0 || c this._width) return; if (visited[r][c]) return; if (this._board[r][c].type ! targetType || this._board[r][c].isCleared) return; visited[r][c] true; result.push(this._board[r][c]); // 四方向搜索 dfs(r - 1, c); // 上 dfs(r 1, c); // 下 dfs(r, c - 1); // 左 dfs(r, c 1); // 右 }; dfs(startRow, startCol); return result.length 3 ? result : []; // 只有数量3才算有效消除 }2. 并查集Union-Find对于需要频繁进行连通性检查的场景并查集在理论上的平均时间复杂度更优。它的思想是先将每个格子视为独立的集合然后遍历棋盘如果相邻格子同色就将它们“合并”到同一个集合中。最后统计每个集合的大小。class UnionFind { private parent: number[]; private rank: number[]; constructor(size: number) { /* 初始化 */ } find(x: number): number { /* 查找根节点带路径压缩 */ } union(x: number, y: number): void { /* 按秩合并 */ } } // 在棋盘检测中的应用 private checkMatchesWithUnionFind(): Block[][] { const size this._width * this._height; const uf new UnionFind(size); // 第一次遍历横向连接 for (let r 0; r this._height; r) { for (let c 0; c this._width - 1; c) { if (this._board[r][c].type this._board[r][c 1].type) { uf.union(r * this._width c, r * this._width c 1); } } } // 第二次遍历纵向连接 for (let r 0; r this._height - 1; r) { for (let c 0; c this._width; c) { if (this._board[r][c].type this._board[r 1][c].type) { uf.union(r * this._width c, (r 1) * this._width c); } } } // 收集所有大小3的集合 const groups new Mapnumber, Block[](); for (let r 0; r this._height; r) { for (let c 0; c this._width; c) { const idx r * this._width c; const root uf.find(idx); if (!groups.has(root)) groups.set(root, []); groups.get(root)!.push(this._board[r][c]); } } // 过滤并返回 const matches: Block[][] []; for (const [_, blocks] of groups) { if (blocks.length 3) { matches.push(blocks); } } return matches; }选择哪个DFS实现更简单在消除游戏这种棋盘规模固定且不大的情况下比如8x8性能完全足够代码也更易读和维护。我最终选择了DFS。并查集在算法竞赛或棋盘非常大的情况下有优势但代码稍复杂。对于手游消除DFS是更务实的选择。实操心得无论用哪种方法一定要把检测逻辑和消除、下落、填充逻辑解耦。我的BoardManager提供了findAllMatches(): Block[][]方法它只负责返回当前棋盘上所有可消除的组合不产生任何副作用。这样结构清晰也便于后续实现“提示Hint”功能即寻找一个可交换位置产生消除。2.3 交换、消除与连锁反应处理玩家操作的核心是交换两个相邻方块。交换后需要立即检测是否形成有效消除3个同色相连。交换逻辑在内存的_board数组中交换两个Block对象的位置row, col属性。播放两个方块节点交换的动画见动效部分。动画结束后调用findAllMatches()检测。如果没有形成消除则回退交换包括数据回退和播放反向动画并给玩家一个“无效操作”的反馈比如轻微震动。消除与下落Cascade逻辑这是消除游戏爽感的关键必须处理得流畅且有条理。标记消除将检测到的所有可消除方块标记为isCleared true。播放消除动画所有被标记的方块同时播放爆炸、缩放消失等动画见动效部分。计算下落消除后上方未消除的方块需要下落填补空缺。我采用列优先的下落计算方式private calculateFalls(): {block: Block, targetRow: number}[] { const fallInfo: {block: Block, targetRow: number}[] []; for (let c 0; c this._width; c) { let fallDistance 0; // 从下往上遍历每一列 for (let r this._height - 1; r 0; r--) { const block this._board[r][c]; if (block.isCleared) { fallDistance; // 遇到一个空位下落距离1 } else if (fallDistance 0) { // 如果这个方块需要下落记录它的目标行 fallInfo.push({block, targetRow: r fallDistance}); // 更新逻辑棋盘数据 this._board[r fallDistance][c] block; block.row r fallDistance; // 原位置置为空可以用null或一个特殊标记 this._board[r][c] null!; } } // 下落完成后顶部会产生新的空缺需要生成新方块填充 // 这部分逻辑在 fillNewBlocks 中处理 } return fallInfo; }播放下落动画根据fallInfo提供的信息让每个方块节点移动到新的位置。这里的关键是动画要错开时间产生“波浪”下落的效果而不是同时僵硬地掉下来。可以为每一行或每个方块添加一个渐增的延迟。填充新方块所有下落动画完成后为顶部的空缺生成新方块并让它们从屏幕上方“掉落”进来。检测连锁消除新方块填充后立即再次调用findAllMatches()。如果还有可消除的重复步骤1-5直到棋盘稳定。每次连锁消除可以给玩家额外的分数倍数奖励。踩坑记录最初我把数据更新和动画播放耦合在一起导致状态管理混乱特别是处理连锁消除时。后来我严格遵循“先更新逻辑数据再驱动视觉动画”的原则。所有棋盘状态的变化交换、消除标记、下落计算、填充都在BoardManager中同步完成然后生成一个“操作指令列表”如{type: ‘swap’, from, to},{type: ‘remove’, blocks},{type: ‘fall’, infos},{type: ‘fill’, newBlocks}由一个独立的AnimationManager按顺序播放这些指令对应的动画。这样逻辑清晰也易于做回放或断线重连。3. 视觉动效与粒子系统消除游戏的“手感”和“爽感”一半来自动效。Cocos Creator 3.x 的动画系统和粒子系统非常强大。3.1 方块基础动画每个方块至少需要以下几种动画状态我使用 Cocos Creator 的Animation Clip配合代码控制来实现。Idle待机轻微的呼吸感或颜色微动。可以用关键帧动画调整scale或color属性。Select选中玩家点击或触摸时放大并提高亮度。我通常将scale从1动画到1.1同时调整color的亮度或饱和度。// 在方块的控制组件中 onTouchStart() { cc.tween(this.node) .to(0.1, { scale: cc.v3(1.1, 1.1, 1) }) .start(); }Swap交换两个方块相互移动位置。使用cc.tween或cc.Animation播放position变化。playSwapAnimation(nodeA: cc.Node, posA: cc.Vec3, nodeB: cc.Node, posB: cc.Vec3): Promisevoid { return new Promise((resolve) { cc.tween(nodeA).to(0.2, { position: posB }).start(); cc.tween(nodeB).to(0.2, { position: posA }).call(resolve).start(); }); }Match消除这是重点。简单的可以是缩放消失 (scaleTo 0)但我更推荐组合动画先稍微放大 (scaleTo 1.2)再快速缩小并淡出 (scaleTo 0, opacityTo 0)整个过程约0.3秒。可以为不同类型的方块设计不同的消除特效比如冰块碎裂、火焰爆炸等这需要美术资源支持。3.2 粒子特效增强打击感粒子特效是提升视觉冲击力的利器。Cocos Creator 3.8 内置了功能完善的粒子系统组件 (cc.ParticleSystem)。消除特效创建粒子资产在资源管理器右键创建Particle System。配置参数这是核心。以爆炸特效为例发射器类型 (Emitter Mode):Burst(爆发式)在消除瞬间爆发。总量 (Total Particles):15-30个太多会卡顿。生命周期 (Life):0.5 - 1.0秒不要太长。起始大小/结束大小 (Start/End Size):可以配置为从大到小模拟能量消散。起始颜色/结束颜色 (Start/End Color):设置为你消除的方块的颜色结束时可以淡出到透明。重力模式 (Gravity Mode):可以开启给粒子一个向下的加速度模拟碎屑下落。纹理 (Sprite Frame):使用一个小的圆形或星形纹理或者美术提供的特效切片。代码触发在消除时在每个被消除方块的位置动态生成或复用对象池一个粒子节点。playExplosionAt(pos: cc.Vec3, color: cc.Color) { // 从对象池获取粒子节点 let particleNode this.particlePool.get(); if (!particleNode) { particleNode cc.instantiate(this.explosionParticlePrefab); } particleNode.setPosition(pos); particleNode.parent this.effectLayer; // 放在一个专门的特效层 const particleSys particleNode.getComponent(cc.ParticleSystem); particleSys.startColor color; // 动态设置粒子颜色 particleSys.resetSystem(); // 重启粒子播放 // 播放完后回收入对象池 this.scheduleOnce(() { particleSys.stopSystem(); this.particlePool.put(particleNode); }, 1.0); }连消特效当发生连锁消除时可以播放更华丽的全局特效比如屏幕边缘闪光、摄像机轻微震动、连击数字弹出等。摄像机震动可以通过cc.tween快速改变主摄像机节点position的x, y值来实现。性能警告粒子虽好但过量是性能杀手尤其在微信小游戏平台。必须使用对象池严格管理所有动态生成的特效节点。同时控制单个特效的粒子数量通常不超过50并避免同时播放过多特效。在低端机上可以考虑提供一个“简化特效”的选项。3.3 拖尾与滑动效果为了提升玩家滑动交换方块时的手感可以添加拖尾效果。简单实现在玩家手指/鼠标按下并移动时在移动路径上间隔生成半透明的方块精灵并快速淡出。这可以用cc.graphics画线或者用序列帧动画实现。进阶实现使用 Cocos 的MotionStreak组件在2.x中常见3.x中可能需要自己实现或寻找社区组件。它的原理是让一个纹理随着节点的移动而留下渐隐的痕迹。我的方案由于项目要求轻量我采用了一种简化方案。在方块交换的动画中不是让两个方块直线移动而是使用了cc.tween的bezierBy贝塞尔曲线动作让方块移动路径带一点轻微的弧形模拟“抛掷”感再配合一点缩放手感比直线移动好很多。4. 微信小游戏适配与优化这是让游戏真正能上线运行的关键一步。Cocos Creator 虽然提供了“一键发布”但默认配置往往无法应对小游戏平台的严格限制。4.1 项目构建配置要点在 Cocos Creator 的项目 - 项目设置中有几个关键面板需要仔细配置功能裁剪在功能裁剪面板勾选掉你绝对用不到的引擎模块。例如如果你的游戏是2D俯视角可以裁剪掉3D物理、3D粒子、3D阴影等。每裁剪一个模块都能减少一些包体大小。纹理压缩在资源服务器和小游戏等平台的分组下设置纹理压缩格式。对于微信小游戏强烈推荐使用 ASTC如果目标设备支持或ETC2对于OpenGL ES 3.0作为压缩格式。这能极大减少纹理内存占用和下载体积。可以在项目设置 - 资源数据库 - 默认纹理压缩格式中设置。MD5缓存勾选MD5 Cache。这会给构建后的资源文件名加上哈希值有利于小游戏平台的缓存更新机制。主包压缩类型选择小游戏平台在配置里将主包压缩类型设置为zip。这会将首包资源压缩进一步减少初始下载大小。4.2 首包体积与资源加载策略微信小游戏对主包体积有严格限制目前是4M/8M/12M根据游戏类型不同。我们必须把游戏资源精细化管理。资源分类首包资源游戏启动Logo、加载界面UI、必要的配置表如关卡基础数据、核心游戏脚本、第一关必需的音效和纹理。这些必须放在resources目录或直接放在assets根目录会被打包进主包。远程资源大量的关卡图集、背景音乐、非关键特效、后续关卡的配置。这些应该上传到你的服务器或微信的云存储。使用 Asset BundleCocos Creator 的 Asset Bundle 是管理远程资源的利器。在资源管理器中创建新的 Asset Bundle例如level1,level2,music。将对应资源拖入相应的 Bundle 文件夹。构建时这些 Bundle 会生成独立的.json和资源文件不会进入主包。在代码中动态加载// 加载某个Asset Bundle const bundle await assetManager.loadBundle(‘level1’, { onFileProgress: (loaded, total) { // 更新加载进度条 }}); // 从Bundle中加载具体资源 const texture await bundle.load(‘level1_bg’, cc.Texture2D);纹理图集Sprite Atlas务必把大量的小碎图打包成图集。这不仅能减少Draw Call提升渲染性能还能优化文件数量和体积。在 Cocos Creator 中将碎图放入一个文件夹然后在该文件夹上右键选择创建 - Sprite Atlas即可。4.3 小游戏平台API接入微信小游戏有自己的一套API用于登录、支付、广告、数据上报等。Cocos Creator 通过wx这个全局对象来暴露这些API。环境判断// 判断是否在小游戏环境 if (typeof wx ! ‘undefined’) { // 小游戏环境 console.log(‘运行在微信小游戏’); // 此时可以安全调用 wx.xxx 接口 } else { // PC浏览器或模拟器环境 console.log(‘运行在Web平台’); }常用API封装示例登录async wxLogin(): Promiseany { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { // 将code发送到自己服务器换取openid和session_key resolve(res.code); } else { reject(‘登录失败’ res.errMsg); } }, fail: (err) reject(err) }); }); }分享setupShareMenu(): void { // 启用分享功能 wx.showShareMenu({ withShareTicket: true }); // 监听用户点击分享按钮 wx.onShareAppMessage(() { return { title: ‘超好玩的消除游戏快来挑战’, imageUrl: ‘assets/share.jpg’, // 分享图片路径 }; }); }激励视频广告用于复活、获取道具let rewardedVideoAd: any null; initRewardedVideoAd(adUnitId: string): void { if (!wx.createRewardedVideoAd) return; rewardedVideoAd wx.createRewardedVideoAd({ adUnitId }); rewardedVideoAd.onLoad(() {}); rewardedVideoAd.onError((err: any) console.error(‘广告加载错误’, err)); rewardedVideoAd.onClose((res: any) { if (res res.isEnded) { // 正常播放结束发放奖励 this.grantReward(); } else { // 用户中途关闭不给奖励 wx.showToast({ title: ‘看完广告才能获得奖励哦~’ }); } }); } showRewardedVideo(): void { if (rewardedVideoAd) { rewardedVideoAd.show().catch(() { // 失败重试 rewardedVideoAd.load().then(() rewardedVideoAd.show()); }); } }避坑指南微信小游戏的API是异步回调风格的但在TypeScript中我们可以用Promise将其封装成更易用的异步函数。另外广告组件一定要做好错误处理特别是load和show失败的情况要给予用户明确的提示并设计好降级方案比如展示一个激励按钮但点击后提示“广告加载中请稍后”。4.4 性能优化实战小游戏平台设备性能参差不齐必须把优化做到极致。Draw Call 优化核心手段使用纹理图集确保同一图集的精灵在渲染队列中连续绘制。检查工具在 Cocos Creator 编辑器中开启调试 - 显示 Draw Call在游戏中查看Draw Call数量。理想情况下静态UI部分应合并到少数几个Draw Call内。动态合批Cocos Creator 会自动对相同材质的静态节点进行合批。对于动态节点如不断移动的方块确保它们使用相同的材质和纹理也有机会被动态合批。节点树优化减少节点数量每个节点都有管理开销。避免创建大量只有空节点的层级。使用节点池对于频繁创建和销毁的对象如方块、特效、子弹必须使用cc.NodePool。我在项目中为方块、消除特效、粒子系统都建立了独立的对象池。静态节点设置static对于永远不会移动、旋转、缩放的节点如背景图在属性检查器中勾选static引擎会对其进行优化。内存与GC优化避免在update中频繁创建对象如new cc.Vec3(),new Array()。可以在函数外预先创建好对象进行复用。及时销毁不再使用的资源使用assetManager.releaseAsset(texture)释放远程加载的纹理和音效。但注意打包在包体内的资源不要手动释放除非你确定不再需要且后续也不会动态加载。Profile工具在微信开发者工具中使用性能面板 - Memory查看内存快照追踪内存泄漏。帧率稳定逻辑与渲染帧率解耦对于消除游戏逻辑更新如输入检测、消除计算可以固定在30帧而渲染保持60帧。这可以在项目设置 - 功能裁剪 - 自定义帧率中配置。减少不必要的update回调很多组件不需要每帧都执行逻辑。可以用schedule或setTimeout来替代。5. 常见问题与调试技巧在开发过程中我遇到了不少典型问题这里记录下排查思路和解决方法。5.1 构建与运行问题问题现象可能原因解决方案构建后白屏控制台无报错1. 首包资源加载失败2. 启动场景未正确设置1. 检查构建日志确认资源是否成功打包。2. 在项目设置 - 通用设置中确认启动场景是否正确。3. 在微信开发者工具中查看网络面板确认game.js和资源是否成功加载状态码200。微信开发者工具报错“xxx.js 未找到”Asset Bundle 路径配置错误或资源未上传1. 检查main.js中settings.js的路径是否正确通常应为”settings.js”。2. 确认远程 Asset Bundle 的config.json和资源文件是否已上传到服务器且服务器配置了正确的 MIME 类型.json文件应为application/json。真机上画面错乱或闪烁纹理压缩格式不兼容1. 检查目标设备的 GL 版本。对于低端机仅支持 GL ES 2.0不能使用 ETC2/ASTC应使用RGB565或RGBA4444等格式。2. 在项目设置中为不同平台配置合适的默认压缩格式。5.2 游戏逻辑与性能问题问题现象可能原因解决方案交换动画卡顿不流畅1. 在动画过程中频繁进行逻辑检测2. 使用了阻塞主线程的操作1. 确保动画播放和逻辑计算分离。等交换动画完全结束后再触发消除检测。2. 使用cc.tween或cc.Animation的callback或Promise来同步流程避免用setTimeout猜时间。连续快速点击导致状态错乱玩家输入未做防抖或状态锁在BoardManager中设置一个isProcessing布尔锁。在开始处理一次交换、消除、下落流程前检查如果为true则忽略新的输入流程结束后再设为false。游戏运行一段时间后越来越卡内存泄漏节点或对象未正确回收1.务必使用对象池管理所有动态生成的节点。2. 检查事件监听在节点销毁时 (onDestroy) 移除所有自定义事件监听。3. 在微信开发者工具Memory面板定期拍快照对比查找不断增长的对象。消除检测在复杂形状时漏判DFS 或并查集实现有误特别是斜角问题消除游戏通常只检测上下左右四方向连通。确认你的算法没有错误地包含斜对角。检查visited标记数组是否正确重置。5.3 微信平台特有问题问题现象可能原因解决方案音频播放无效或延迟微信小游戏音频需要用户交互触发且有限制1. 在游戏开始如一个开始按钮点击事件里创建并播放一个无声的音频上下文以解锁音频权限。2. 使用wx.createInnerAudioContext()注意其autoplay在iOS上可能受限最好在onCanplay回调里手动play()。3. 背景音乐使用一个实例音效使用另一个实例避免互相打断。分享图片不显示分享图片路径错误或图片未加载1.imageUrl必须是小程序包内路径如”images/share.jpg”或有效的网络图片URL。2. 包内图片需放在resources目录或主包内并确保已打包。3. 网络图片需要配置下载域名。从后台切回前台游戏状态异常游戏未正确处理onHide和onShow事件在main.js或游戏主逻辑中监听typescriptbrwx.onHide(() {br // 暂停游戏逻辑、音乐br cc.director.pause();br});brwx.onShow(() {br // 恢复游戏br cc.director.resume();br});br调试技巧善用cc.debug在项目设置 - 功能裁剪中开启调试模式可以在构建后保留cc.log,cc.warn,cc.error的输出。远程调试真机在微信开发者工具中点击远程调试用手机扫码即可在电脑上查看手机端的Console和Network信息 invaluable。性能面板微信开发者工具的性能面板可以记录一段时间内的CPU、内存、帧率变化是定位性能瓶颈的利器。6. 项目架构与代码组织心得经过这个项目我总结了一套适用于中小型 Cocos 项目的代码组织方式核心思想是“高内聚低耦合”和“数据驱动”。单例管理器模式对于全局唯一的管理器如BoardManager棋盘、AudioManager音频、UIManager界面、DataManager数据我使用单例模式。方便在任何地方访问但要注意避免变成“上帝对象”。事件通信管理器之间、组件之间的通信尽量使用 Cocos 内置的EventTarget或自定义的全局事件派发器而不是直接互相引用。例如当棋盘消除完成时BoardManager派发一个’MATCH_COMPLETE’事件UIManager监听这个事件来更新分数。这极大地减少了模块间的依赖。配置数据分离所有可配置的数据如方块类型颜色、关卡目标、道具属性都放在独立的 JSON 或 ScriptableObject 文件中。通过cc.resources.load加载。这样策划调整数值时不需要修改代码也不需要重新构建。状态机游戏主逻辑可以使用一个简单的状态机来管理比如GameState枚举IDLE等待输入、SWAPPING交换中、MATCHING检测消除、FALLING下落中、FILLING填充中。这能让复杂的连锁反应流程变得清晰可控。最后关于 Cocos Creator 3.8 本身它对比 2.x 版本在3D渲染和工作流上提升巨大但对于我们做2D消除游戏来说最明显的感受是编辑器更稳定TypeScript 支持更完善以及构建到小游戏的流程更顺畅。社区资源虽然不如2.x时期海量但也在快速增长遇到问题多查官方文档和论坛大部分都能找到解决方案。这个项目从零到上线的过程让我深刻体会到把基础打牢算法、数据结构、把体验做细动效、手感、把适配做足平台特性、性能才是做出一个合格手游的关键。