
1. 从零到一为什么一个人也能做微信小游戏去年年底我把自己关在书房里整整三周用Cocos Creator加TypeScript撸了一个微信小游戏出来。不是那种“Hello World”级别的Demo而是真正上线、能跑通广告变现、日活稳定在几百的小产品。整个过程只有我一个人策划、美术、程序、音效、运营全包。这篇文章就是那次实战的完整复盘我会把技术选型的逻辑、踩过的坑、以及那些文档里不会写的细节全部倒出来。如果你是一个独立开发者或者想尝试用业余时间做点小游戏副业那这篇内容应该能帮你省下至少两周的试错时间。核心关键词就几个微信小游戏、Cocos Creator、TypeScript、Canvas。我不会讲太多虚的直接上干货。先说结论一个人做微信小游戏技术栈选Cocos Creator TypeScript是目前最稳的组合。为什么不是Unity为什么不是Phaser下面我会逐一拆解。2. 技术选型Cocos Creator、Unity还是Phaser2.1 为什么我最终选了Cocos Creator做微信小游戏引擎选择是第一道分水岭。市面上主流方案有三个Cocos Creator、Unity、Phaser。我三个都试过最后留在Cocos Creator原因很实际。Unity的微信小游戏打包流程虽然已经比较成熟但包体是个硬伤。Unity引擎本身运行时的体积就摆在那里即便做了裁剪首包也很难压到微信要求的4MB以内。你可能需要做分包、做资源远程加载对于一个人来说光是调通打包流程就能耗掉一周。而且Unity的WebGL导出在低端安卓机上的性能表现说实话不太乐观。Phaser是纯JavaScript的2D引擎轻量、上手快Canvas和WebGL双模式。但问题在于Phaser没有可视化的编辑器。你写个小游戏场景搭建、UI布局、动画编辑全靠代码硬写。一个人做项目时间是最贵的没有编辑器意味着每个像素的调整都要改代码、刷新、看效果效率太低。而且Phaser的TypeScript支持虽然可以配但类型定义不够完善写起来经常要自己补类型声明。Cocos Creator就不一样了。它自带完整的编辑器场景编辑、UI布局、动画曲线、粒子效果都是可视化的。TypeScript是一等公民引擎的API全部有类型定义写起来非常顺手。最关键的是Cocos Creator对微信小游戏的导出支持是官方级别的一键打包自动处理了大部分适配问题。包体方面一个简单的2D游戏首包控制在2-3MB完全没问题。提示如果你之前用过Unity转Cocos Creator大概需要两三天适应。主要差异在组件系统和节点树的设计上但概念是相通的。2.2 TypeScript带来的开发体验提升我早期用JavaScript写过小游戏那体验就像在黑暗中走钢丝。变量名打错了运行时才报错函数参数传错了调试半天才发现。TypeScript把这些错误提前到了编译阶段对于一个人做项目来说这简直是救命稻草。举个例子微信小游戏的API调用经常涉及回调函数。用JavaScript写的时候你根本不知道回调里该接收什么参数。TypeScript有完整的wx命名空间类型定义你敲一个wx.编辑器自动补全所有可用API参数类型一目了然。这省下的查文档时间累积起来非常可观。另外TypeScript的接口和泛型在管理游戏状态时特别好用。比如我定义了一个GameState接口所有游戏状态都实现这个接口切换场景时类型安全不会出现“某个字段忘了初始化”这种低级错误。interface GameState { score: number; level: number; isPaused: boolean; playerPosition: { x: number; y: number }; } class GameManager { private state: GameState; constructor() { this.state { score: 0, level: 1, isPaused: false, playerPosition: { x: 0, y: 0 } }; } updateScore(delta: number): void { this.state.score delta; this.checkLevelUp(); } private checkLevelUp(): void { const threshold this.state.level * 100; if (this.state.score threshold) { this.state.level; } } }上面这段代码TypeScript会在编译时检查所有类型updateScore只能传数字playerPosition必须有x和y。这种约束在项目变大之后价值会指数级上升。2.3 Canvas渲染与性能考量微信小游戏的渲染底层是Canvas但Cocos Creator会根据设备能力自动选择WebGL或Canvas 2D渲染模式。WebGL模式下渲染性能好很多但兼容性需要关注。Canvas 2D模式兼容性最好但绘制大量精灵时帧率会下降。我的策略是优先WebGL降级Canvas。Cocos Creator的构建选项里可以配置渲染模式我一般选“自动”让引擎根据设备判断。实测下来2019年以后的中端安卓机跑WebGL都没问题帧率稳定在55-60fps。但有一个坑要注意Canvas 2D模式下motionStreak拖尾效果这类依赖混合模式的组件表现会不一样。我在一个跑酷游戏里用了MotionStreak做角色拖尾WebGL下效果很炫Canvas 2D下直接不显示。后来查文档才发现MotionStreak依赖gl.BLENDCanvas 2D不支持。解决办法是降级时用序列帧动画替代或者干脆去掉拖尾效果。注意如果你的游戏大量使用了混合模式、Shader特效务必在低端机上测试Canvas 2D降级后的表现。不要等到上线后才发现部分用户看不到特效。3. 项目架构一个人如何管理代码3.1 目录结构设计一个人做项目最容易犯的错误就是“随手放”。今天把脚本扔在assets/scripts根目录明天把预制体放在assets/resources下面过两周自己都找不到文件。我吃过这个亏所以后来定了一套严格的目录规范。assets/ ├── scripts/ │ ├── core/ # 核心框架事件管理、状态机、对象池 │ ├── game/ # 游戏逻辑玩家控制、敌人AI、关卡管理 │ ├── ui/ # UI逻辑弹窗、HUD、按钮事件 │ ├── utils/ # 工具函数数学计算、时间格式化 │ └── config/ # 配置数据关卡参数、数值表 ├── prefabs/ # 预制体 ├── textures/ # 图片资源 ├── audio/ # 音效和背景音乐 ├── animations/ # 动画剪辑 └── scenes/ # 场景文件这个结构的好处是职责清晰。找代码的时候先想“这是游戏逻辑还是UI”然后直接进对应目录。core目录放的是与具体游戏无关的通用模块比如事件总线、对象池这些代码在下一个项目里可以直接复制过去。3.2 事件驱动与模块解耦小游戏里模块之间的通信我强烈建议用事件驱动而不是直接引用。早期我写代码UI按钮直接调用GameManager.instance.startGame()结果后来想改游戏流程发现UI和逻辑缠在一起改一处崩三处。后来我引入了一个简单的事件总线type EventCallback (...args: any[]) void; class EventBus { private static events: Mapstring, EventCallback[] new Map(); static on(event: string, callback: EventCallback): void { if (!this.events.has(event)) { this.events.set(event, []); } this.events.get(event)!.push(callback); } static off(event: string, callback: EventCallback): void { const callbacks this.events.get(event); if (callbacks) { const index callbacks.indexOf(callback); if (index ! -1) { callbacks.splice(index, 1); } } } static emit(event: string, ...args: any[]): void { const callbacks this.events.get(event); if (callbacks) { callbacks.forEach(cb cb(...args)); } } }UI按钮点击时只负责EventBus.emit(game-start)游戏管理器监听这个事件开始游戏。UI完全不知道游戏逻辑怎么实现的游戏逻辑也不关心UI长什么样。这种解耦在后期加功能时特别爽比如我想加一个“每日挑战”模式只需要新增一个监听器UI那边加个按钮发事件就行不用动原有代码。3.3 对象池性能优化的第一课微信小游戏运行在手机浏览器环境里内存和CPU都有限。频繁地instantiate和destroy节点会导致垃圾回收频繁触发游戏卡顿。对象池是必须的。Cocos Creator有内置的NodePool但我更喜欢自己封装一个通用的对象池因为NodePool只支持节点而游戏里还需要池化其他对象比如数据对象、子弹逻辑等。class ObjectPoolT { private pool: T[] []; private factory: () T; private reset: (obj: T) void; constructor(factory: () T, reset: (obj: T) void, initialSize: number 10) { this.factory factory; this.reset reset; for (let i 0; i initialSize; i) { this.pool.push(factory()); } } get(): T { if (this.pool.length 0) { return this.pool.pop()!; } return this.factory(); } put(obj: T): void { this.reset(obj); this.pool.push(obj); } }子弹、敌人、飘字这些频繁创建销毁的对象全部走对象池。实测下来用了对象池之后游戏在低端机上的帧率波动从±15fps降到了±5fps以内。实操心得对象池的初始大小不要设太大否则启动时会有明显卡顿。我一般设10-20个运行时按需增长。另外reset函数一定要把对象的所有状态重置干净否则会出现“复用的子弹还带着上次的伤害值”这种诡异bug。4. 核心玩法实现从Canvas绘制到游戏逻辑4.1 角色控制与物理系统我的小游戏是一个2D平台跳跃类玩法角色需要跑、跳、踩敌人。Cocos Creator内置了物理系统但对于这种简单的平台跳跃用物理引擎反而麻烦。我选择了自己写一个轻量级的AABB碰撞检测。角色的移动逻辑很简单水平方向由输入控制垂直方向受重力影响。每一帧更新位置然后检测与地面的碰撞。class PlayerController extends Component { private velocity: Vec2 new Vec2(0, 0); private gravity: number -980; // 像素/秒² private moveSpeed: number 200; private jumpForce: number 450; private isGrounded: boolean false; update(dt: number): void { // 水平输入 let horizontal 0; if (Input.isKeyPressed(KeyCode.KEY_A)) horizontal - 1; if (Input.isKeyPressed(KeyCode.KEY_D)) horizontal 1; this.velocity.x horizontal * this.moveSpeed; // 重力 this.velocity.y this.gravity * dt; // 跳跃 if (Input.isKeyPressed(KeyCode.SPACE) this.isGrounded) { this.velocity.y this.jumpForce; this.isGrounded false; } // 更新位置 const pos this.node.position; this.node.setPosition( pos.x this.velocity.x * dt, pos.y this.velocity.y * dt ); // 碰撞检测 this.checkCollision(); } private checkCollision(): void { // 与地面和平台的AABB检测 // 省略具体实现 } }这里有个细节重力值我设的是-980这是模拟真实重力加速度9.8m/s²换算到像素单位的结果。但实际调的时候我发现这个值让跳跃手感偏“重”后来改成了-1200跳跃更轻快。数值调优没有标准答案多试几次找到自己觉得舒服的值。4.2 关卡设计与数据驱动一个人做游戏关卡设计是最耗时的部分。如果每个关卡都硬编码在代码里改一个数字就要重新编译效率太低。我的做法是关卡数据用JSON配置运行时加载。{ levelId: 1, platforms: [ { x: 0, y: -200, width: 800, height: 40 }, { x: 300, y: -100, width: 200, height: 20 }, { x: 600, y: 0, width: 200, height: 20 } ], enemies: [ { type: slime, x: 400, y: -150, patrolRange: 100 } ], coins: [ { x: 350, y: -50 }, { x: 650, y: 50 } ], goal: { x: 750, y: 100 } }关卡编辑器我用的是Tiled Map Editor导出JSON然后在游戏里解析。Tiled支持图层、对象组可视化编辑比在代码里写坐标高效十倍。解析逻辑大概一百行代码一次性投入后面所有关卡都受益。提示Tiled导出的JSON坐标系和Cocos Creator的坐标系可能不一致注意做转换。Tiled的y轴向下Cocos Creator的y轴向上解析的时候要取反。4.3 微信小游戏API接入微信小游戏的API是wx命名空间下的Cocos Creator构建时会自动注入类型定义。常用的API包括wx.createUserInfoButton获取用户信息wx.onShareAppMessage分享wx.createRewardedVideoAd激励视频广告wx.setStorageSync本地存储激励视频广告是变现的核心。接入流程不复杂但有几个坑第一广告实例要提前创建不要等到用户点击按钮时才创建否则加载时间会让用户等太久。我一般在游戏启动时就创建好广告实例预加载。第二广告播放完成后的回调要处理好。用户看完广告你要给奖励但也要考虑用户中途关闭的情况。onClose回调里会告诉你isEnded只有isEnded为true才给奖励。class AdManager { private rewardedAd: any null; init(): void { if (typeof wx undefined) return; this.rewardedAd wx.createRewardedVideoAd({ adUnitId: your-ad-unit-id }); this.rewardedAd.onError((err: any) { console.error(广告加载失败, err); }); } showRewardedAd(onReward: () void): void { if (!this.rewardedAd) { onReward(); // 降级处理直接给奖励 return; } this.rewardedAd.show().catch(() { // 加载失败重新加载后再试 this.rewardedAd.load().then(() { this.rewardedAd.show(); }).catch(() { onReward(); // 仍然失败降级给奖励 }); }); this.rewardedAd.onClose((res: any) { if (res res.isEnded) { onReward(); } }); } }注意广告的onClose回调是全局的每次调用show之前要确保没有重复绑定。我一开始没注意导致用户看一次广告触发了三次奖励差点被判定为作弊。后来改成每次show之前先offClose再onClose。5. 打包发布与性能调优5.1 微信小游戏打包流程Cocos Creator的构建面板里选择“微信小游戏”填好AppID点击构建。构建完成后用微信开发者工具打开build/wechatgame目录预览、上传。但构建只是第一步后面还有几个必须做的优化包体压缩。微信小游戏首包限制4MB超过就要做分包。我的做法是首包只放核心玩法和必要资源关卡数据、后续关卡的图片走远程加载。Cocos Creator支持配置资源远程服务器地址构建时会把超过阈值的资源自动放到远程。代码混淆。构建选项里开启“压缩代码”和“混淆代码”能减小包体也能防止别人轻易反编译。但注意混淆后如果线上出bug堆栈信息会很难看。我的做法是保留一份sourcemap只在本地调试用不上传。纹理压缩。微信小游戏支持ASTC、ETC2等压缩纹理格式。开启后图片资源体积能减少50%以上。但要注意不同机型支持的格式不同Cocos Creator会自动做兼容处理构建时勾选“自动压缩纹理”即可。5.2 性能监控与帧率优化游戏上线后性能问题会通过用户反馈暴露出来。我在游戏里内置了一个简单的性能监控每5秒记录一次帧率如果连续低于30fps就上报到服务器。class PerformanceMonitor { private frameCount: number 0; private lastTime: number 0; private lowFpsCount: number 0; update(dt: number): void { this.frameCount; const now Date.now(); if (now - this.lastTime 5000) { const fps this.frameCount / ((now - this.lastTime) / 1000); if (fps 30) { this.lowFpsCount; if (this.lowFpsCount 3) { this.reportLowFps(fps); this.lowFpsCount 0; } } else { this.lowFpsCount 0; } this.frameCount 0; this.lastTime now; } } private reportLowFps(fps: number): void { // 上报到服务器记录设备型号、场景等信息 } }根据上报数据我发现低端机上的主要瓶颈是粒子特效和DrawCall过高。优化手段包括减少同屏粒子数量、合并纹理图集、使用cc.Macro.CLEANUP_IMAGE_CACHE清理图片缓存。5.3 常见问题排查速查表问题现象可能原因排查方法解决方案游戏启动黑屏首包资源加载失败查看开发者工具Console检查资源路径确保首包包含必要资源帧率突然下降内存泄漏或GC频繁使用Performance面板检查对象池使用避免频繁创建销毁广告无法加载广告单元ID错误或网络问题查看onError回调检查ID增加降级逻辑部分机型闪退内存溢出查看设备内存占用压缩纹理减少同屏对象数量触摸事件无响应节点层级或触摸区域问题检查节点树和碰撞盒调整节点层级确保触摸区域正确音效播放异常音频格式不支持查看音频格式使用mp3或ogg格式避免wav实操心得微信开发者工具的“真机调试”功能一定要用。模拟器上跑得好好的真机上可能完全不一样。特别是触摸事件和音频播放模拟器和真机差异很大。我每次发版前至少在三台不同档次的安卓机上真机测试。6. 变现与运营一个人如何让游戏活下去6.1 广告变现的节奏控制小游戏的变现主要靠广告激励视频、插屏广告、Banner。但广告不能乱放放多了用户直接卸载。我的策略是激励视频放在“复活”、“双倍奖励”、“解锁新角色”这些用户有明确需求的地方。用户主动选择看广告接受度高。插屏广告放在关卡结束后的结算界面但频率要控制。我设置的是每三关弹一次而且如果用户刚看过激励视频这次插屏就跳过。Banner广告放在主菜单底部不影响游戏操作。但Banner的收益很低主要是填充。注意微信小游戏对广告频率有规定过于频繁的广告弹出可能导致审核不通过。具体限制可以在微信官方文档里查到这里不展开。6.2 用户留存的小技巧一个人做运营没预算买量只能靠产品本身。我做了几件事来提升留存每日签到。连续签到奖励递增第七天给一个稀有角色。这个功能开发成本很低但留存效果明显。我的数据是加了签到后次日留存从25%提升到了35%。排行榜。好友排行榜利用微信的开放数据域展示好友分数。攀比心理是留存的重要驱动力。但要注意开放数据域的渲染和主域是隔离的需要用wx.getOpenDataContext来操作。分享奖励。分享给好友双方各得一次复活机会。这个功能要小心微信对诱导分享打击很严。我的做法是分享按钮不强制用户可以选择分享或看广告两者选其一。6.3 数据埋点与分析没有数据运营就是盲人摸象。我在游戏里埋了几个关键事件游戏启动关卡开始/结束广告展示/完成分享点击签到每个事件带上用户ID、时间戳、关卡ID等参数上报到自己的服务器。然后用简单的图表工具分析。比如我发现第三关的流失率特别高达到60%。去玩了一下发现第三关的难度曲线突然变陡很多用户卡在这里。后来调整了第三关的敌人数量和平台间距流失率降到了35%。7. 独立开发者的时间管理一个人做游戏最大的敌人不是技术难题而是时间。白天要上班晚上和周末才能写代码。我的经验是固定时间块。每天晚上9点到11点雷打不动写代码。不要等“有灵感”再写灵感是写出来的。任务拆分。把大功能拆成小任务每个任务控制在2小时内能完成。完成一个划掉一个有成就感也能看到进度。先完成再完美。美术资源先用占位图音效先用免费素材等玩法跑通了再替换。我见过太多人卡在“找一个完美的角色立绘”上结果游戏永远没做完。每周发一个版本。哪怕只是修了一个bug也要发版。保持节奏避免项目烂尾。实操心得我用Trello管理任务分“待办”、“进行中”、“已完成”三列。每周日晚上规划下周任务每天睡前更新进度。这个习惯让我在三个月内完成了第一个上线版本。8. 后续扩展方向这个项目跑通之后我总结了一套可复用的框架事件总线、对象池、关卡数据驱动、广告管理、性能监控。下一个游戏我只需要换美术资源和玩法逻辑框架层直接复用。目前正在做的是一个合成类小游戏预计开发周期能压缩到一个月以内。另外TypeScript的类型系统在大型项目里的价值越来越明显。我最近在尝试用TypeScript的装饰器来简化组件注册虽然Cocos Creator的装饰器支持还有限但方向是对的。等这个合成游戏上线后我会再写一篇关于装饰器实战的分享。如果你也在做微信小游戏或者准备入坑我的建议是先做一个最小的可玩版本上线看数据再迭代。不要憋大招不要追求完美。小游戏的窗口期很短快速验证比什么都重要。