Cocos Creator 3.x 复刻微信飞机大战:从场景搭建到微信小游戏适配全流程

发布时间:2026/9/26 17:18:11
Cocos Creator 3.x 复刻微信飞机大战:从场景搭建到微信小游戏适配全流程 简介这是一份基于CocosCreator引擎复刻微信经典飞机大战的完整项目源码面向希望学习游戏开发的小白与进阶者可直接用作毕设、课程设计、大作业或工程实训。项目充分运用CC数据驱动优势敌机生成频率、速度、子弹频率与速度等参数均可灵活调整子弹与UFO种类也易于扩展敌机、子弹、UFO均采用对象池管理历史分数通过本地存储记录结构上由main统一控制游戏主逻辑与暂停、继续、得分下分敌机组、子弹组、UFO组分别管理生成逻辑。压缩包共133个文件包含14个js脚本、13个prefab预制体、13个mp3音效、6个anim动画、6张png与5张jpg图片、5个json配置及4个fire场景文件等整体约1.15MB目录清晰便于检索。目前已有229人学习适合想掌握CocosCreator实战与对象池、数据驱动设计的开发者参考。1. 从零复刻微信经典飞机大战Cocos Creator 选型与整体思路微信经典飞机大战这个玩法很多人第一次接触是在微信 5.0 那个年代手指按住屏幕拖动飞机子弹自动发射敌机一波波往下压吃道具、躲弹幕、拼分数。放到今天用 Cocos Creator 把它重新做一遍依然是微信小程序游戏开发里最合适的练手项目之一——玩法闭环短、资源量小、核心机制清晰但该踩的坑一个不少碰撞检测、对象池、微信小游戏分包、触摸事件坐标转换全都能在这个项目里过一遍。这篇文章面向两类人一类是刚接触 Cocos Creator、想找一个完整项目把引擎基本用法串起来的开发者另一类是想把 H5 小游戏搬到微信小游戏平台、但不确定打包和适配环节有哪些坑的熟手。我会按「引擎选型 → 场景搭建 → 核心玩法实现 → 微信小游戏适配 → 避坑 → 进阶优化」的顺序把每一步的参数、代码和失败排查讲清楚。你跟着走完能拿到一个在微信开发者工具里跑得起来、真机上不卡顿的飞机大战。Cocos Creator 目前主流是 3.x 版本2.x 也有大量存量项目。飞机大战这种 2D 竖版射击两个版本都能做但 3.x 的 TypeScript 支持、节点系统、资源管理更现代新项目建议直接上 3.x。下面所有代码和配置以 3.x 为准2.x 的差异我会在关键处点出来。2. 场景搭建与资源组织把飞机大战的骨架立起来2.1 项目初始化与目录结构打开 Cocos Creator 3.x新建一个空项目选择 2D 模板。建完之后先别急着拖节点把目录结构规划好后面资源一多才不会乱。我一般会这样分assets/ scenes/ # 场景文件 Game.scene scripts/ # 所有 TypeScript 脚本 Player.ts Enemy.ts Bullet.ts GameManager.ts PoolManager.ts textures/ # 图片资源 player/ enemy/ bullet/ bg/ audio/ # 音效 prefabs/ # 预制体 Bullet.prefab Enemy.prefab Explosion.prefab这个结构不是强制的但把预制体单独放一个目录是因为飞机大战里子弹和敌机都是高频生成销毁的对象必须做成预制体配合对象池否则频繁instantiate和destroy会让微信小游戏的真机帧率掉得很难看。初始化项目时有一个参数要注意在「项目设置 → 项目数据」里设计分辨率设成 640×1136 或者 720×1280适配模式选「Fit Height」。飞机大战是竖版高度适配能保证不同机型上下不裁切宽度方向留黑边或者用背景图铺满都行。微信小游戏的屏幕比例差异很大从 16:9 到 20:9 都有Fit Height 是最省心的选择。2.2 节点树与层级规划场景里的节点层级直接决定了渲染顺序和碰撞逻辑的复杂度。飞机大战的节点树我一般这样搭Canvas bgLayer # 背景层两张背景图循环滚动 bg1 bg2 gameLayer # 游戏层所有飞机子弹都在这 player bulletContainer enemyContainer explosionContainer uiLayer # UI 层分数、暂停按钮、结算面板 scoreLabel pauseBtn gameOverPanel分层的意义在于背景层单独滚动不影响游戏层子弹和敌机各自放在容器节点下方便统一管理和批量回收UI 层永远在最上面不会被游戏元素遮挡。这里有个容易翻车的地方——Cocos Creator 3.x 的渲染顺序是按节点树从上到下、同级按 zIndex 排的如果你把 UI 和游戏元素放同一层结算面板可能被子弹盖住排查半天以为是代码问题其实是层级没分对。2.3 背景无限滚动的实现背景滚动是飞机大战最基础的动效但新手经常写成「一张图滚到底再瞬移回来」结果就是肉眼可见的跳帧。正确做法是两张背景图首尾相接同时向下移动当一张完全移出屏幕就把它挪到另一张上面。import { _decorator, Component, Node, Vec3, UITransform } from cc; const { ccclass, property } _decorator; ccclass(BgScroll) export class BgScroll extends Component { property(Node) bg1: Node null; property(Node) bg2: Node null; property speed: number 200; // 像素/秒 private bgHeight: number 0; start() { // 取背景图高度用于判断何时循环 this.bgHeight this.bg1.getComponent(UITransform).height; } update(dt: number) { const moveY this.speed * dt; this.bg1.setPosition(this.bg1.position.x, this.bg1.position.y - moveY); this.bg2.setPosition(this.bg2.position.x, this.bg2.position.y - moveY); // bg1 完全移出屏幕挪到 bg2 上方 if (this.bg1.position.y -this.bgHeight) { this.bg1.setPosition(this.bg1.position.x, this.bg2.position.y this.bgHeight); } // bg2 完全移出屏幕挪到 bg1 上方 if (this.bg2.position.y -this.bgHeight) { this.bg2.setPosition(this.bg2.position.x, this.bg1.position.y this.bgHeight); } } }dt是引擎每帧传进来的时间增量用speed * dt而不是固定值是为了保证不同帧率下滚动速度一致。bgHeight从 UITransform 组件取不要硬编码因为不同分辨率下背景图的实际高度可能被适配策略改变。判断条件是 -bgHeight意思是整张图完全移出屏幕底部才回收如果写成 0就会在图片刚碰到屏幕底部时就跳走出现闪烁。3. 核心玩法实现玩家控制、子弹发射与敌机生成3.1 玩家触摸拖动与坐标转换微信小游戏是触摸操作玩家按住屏幕任意位置拖动飞机跟随手指水平移动。这里最大的坑是坐标系转换——触摸事件给的是屏幕坐标而节点位置是 Canvas 下的本地坐标两者原点不同直接赋值飞机就会飞到屏幕外。import { _decorator, Component, Node, EventTouch, Vec3, UITransform, input, Input } from cc; const { ccclass, property } _decorator; ccclass(Player) export class Player extends Component { property speed: number 600; private minX: number 0; private maxX: number 0; private targetX: number 0; start() { const ui this.node.parent.getComponent(UITransform); const halfWidth ui.width / 2; const playerHalf this.node.getComponent(UITransform).width / 2; this.minX -halfWidth playerHalf; this.maxX halfWidth - playerHalf; this.targetX this.node.position.x; input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); input.on(Input.EventType.TOUCH_START, this.onTouchMove, this); } onTouchMove(event: EventTouch) { const uiTransform this.node.parent.getComponent(UITransform); // 把屏幕触摸点转换到父节点本地坐标 const worldPos event.getUILocation(); const localPos uiTransform.convertToNodeSpaceAR(new Vec3(worldPos.x, worldPos.y, 0)); this.targetX Math.min(this.maxX, Math.max(this.minX, localPos.x)); } update(dt: number) { // 平滑插值避免飞机瞬移 const cur this.node.position; const nextX cur.x (this.targetX - cur.x) * Math.min(1, dt * 15); this.node.setPosition(nextX, cur.y, cur.z); } }convertToNodeSpaceAR是把世界坐标转成节点本地坐标的关键 APIAR 表示以锚点为原点。minX和maxX用父节点宽度减去飞机半宽算出来保证飞机不会移出屏幕。update里用插值而不是直接赋值是因为手指移动是离散的触摸事件直接赋值会有顿挫感插值让飞机跟手更顺滑。dt * 15里的 15 是插值系数调大跟手更紧调小更飘一般 10 到 20 之间。3.2 子弹对象池与发射节奏子弹是高频对象一秒可能发射十几发用对象池复用是必须的。Cocos Creator 3.x 提供了NodePool但更推荐自己封装一个简单的池因为飞机大战里子弹、敌机、爆炸特效都要池化统一管理更方便。import { _decorator, Component, Node, Prefab, instantiate, NodePool } from cc; const { ccclass, property } _decorator; ccclass(PoolManager) export class PoolManager { private pools: Mapstring, NodePool new Map(); get(prefab: Prefab, key: string): Node { if (!this.pools.has(key)) { this.pools.set(key, new NodePool()); } const pool this.pools.get(key); if (pool.size() 0) { return pool.get(); } return instantiate(prefab); } put(node: Node, key: string) { if (!this.pools.has(key)) { this.pools.set(key, new NodePool()); } this.pools.get(key).put(node); } }get方法先看池里有没有空闲节点有就取出来复用没有才instantiate新建。put方法把用完的节点放回池里注意放回前要把节点从父节点移除、重置位置和状态否则下次取出来还带着上次的残留数据。子弹发射节奏用计时器控制// 在 Player.ts 的 update 里 this.fireTimer dt; if (this.fireTimer this.fireInterval) { this.fireTimer 0; this.fireBullet(); } fireBullet() { const bullet PoolManager.instance.get(this.bulletPrefab, bullet); bullet.setParent(this.bulletContainer); bullet.setPosition(this.node.position.x, this.node.position.y 40, 0); bullet.active true; }fireInterval一般设 0.15 到 0.25 秒太快子弹密集到看不清太慢打不死敌机。子弹的移动逻辑挂在 Bullet 脚本上每帧向上移动超出屏幕顶部就PoolManager.put回收。3.3 敌机生成与碰撞检测敌机生成用定时器加随机数每隔一段时间在屏幕上方随机 x 位置生成一架。难度曲线通过缩短生成间隔和增加敌机血量来实现。// GameManager.ts 中 spawnEnemy() { const enemy PoolManager.instance.get(this.enemyPrefab, enemy); enemy.setParent(this.enemyContainer); const ui this.enemyContainer.getComponent(UITransform); const halfWidth ui.width / 2 - 40; const x Math.random() * halfWidth * 2 - halfWidth; enemy.setPosition(x, ui.height / 2 60, 0); enemy.active true; }碰撞检测飞机大战里最省事的是圆形碰撞因为飞机和子弹都近似圆形用距离判断比矩形相交更符合直觉计算量也小。// 每帧检测子弹和敌机 checkCollision() { const bullets this.bulletContainer.children; const enemies this.enemyContainer.children; for (let i bullets.length - 1; i 0; i--) { const b bullets[i]; if (!b.active) continue; for (let j enemies.length - 1; j 0; j--) { const e enemies[j]; if (!e.active) continue; const dx b.position.x - e.position.x; const dy b.position.y - e.position.y; const dist Math.sqrt(dx * dx dy * dy); if (dist 30) { // 30 是子弹和敌机半径之和 this.onBulletHitEnemy(b, e); break; } } } }半径阈值 30 是经验值具体要看你的美术资源尺寸。子弹和敌机贴图大小不一样阈值要按实际半径之和调。调太小会出现「明明打中了却没反应」调太大又会出现「隔着老远就爆炸」这个只能对着实际资源试。4. 微信小游戏适配从 Cocos Creator 到真机跑通4.1 构建配置与分包策略在 Cocos Creator 里点「项目 → 构建发布」平台选「微信小游戏」。几个关键参数参数建议值说明设备方向portrait竖版飞机大战必须选竖屏渲染后端WebGL微信小游戏不支持 Canvas 2D 渲染首屏包体积勾选主包控制在 4MB 以内资源服务器地址按需填有远程资源才填纯本地资源留空MD5 Cache勾选资源缓存减少重复下载微信小游戏主包限制 4MB总包 20MB。飞机大战的美术资源如果超过 4MB必须做分包。分包在 Cocos Creator 里通过「资源分包」配置把非首屏必需的资源比如后续关卡的敌机贴图、音效放到子包主包只留首屏加载的。构建完成后用微信开发者工具打开build/wechatgame目录。第一次打开可能会报「app.json 未找到」这是因为构建时没有勾选「生成 app.json」在构建面板的「微信小游戏」选项里勾上就行。4.2 触摸事件在真机上的差异在浏览器预览时触摸事件正常真机上却可能出现「按了没反应」或者「飞机位置偏移」。常见原因是微信小游戏的触摸事件坐标系和浏览器不同event.getUILocation()在真机上返回的是物理像素坐标而设计分辨率是逻辑像素两者之间差一个devicePixelRatio。解决办法是在onTouchMove里不要直接用getUILocation而是用 Cocos Creator 封装好的getLocation配合UITransform.convertToNodeSpaceAR引擎内部已经处理了像素比转换。如果你用的是自己写的坐标转换记得除以window.devicePixelRatio。另一个真机差异是触摸事件的触发频率。浏览器上TOUCH_MOVE可能一帧触发多次真机上频率更低导致飞机移动不跟手。这时候把update里的插值系数调大或者改用TOUCH_MOVE的坐标直接赋值加平滑不要依赖事件频率。4.3 性能优化对象池、合批与帧率微信小游戏真机上最容易出现的问题是帧率掉到 30 以下尤其是敌机和子弹多的时候。三个优化方向第一对象池必须用。前面已经讲了子弹和敌机不要频繁instantiate和destroy全部走池。爆炸特效也一样特效节点池化后复用避免每次爆炸都新建。第二合批渲染。Cocos Creator 的 2D 渲染会自动合批但前提是相邻节点的材质和贴图相同。如果你的子弹和敌机用了不同的图集合批会断。解决办法是把所有小图打进同一张图集Atlas在「资源管理器」里选中多张图右键「创建 → 自动图集」构建时会自动合并。第三帧率设置。在GameManager的start里调用game.frameRate 60微信小游戏默认可能是 30改成 60 后画面更流畅但耗电增加。如果低端机上 60 帧不稳定可以动态降帧检测到连续掉帧就切回 30。5. 避坑与排查飞机大战开发中最容易翻车的 5 个点5.1 子弹回收后再次取出位置错乱现象子弹打出去后回收下次从池里取出来位置还在上次消失的地方或者直接出现在屏幕中间。原因NodePool.put只是把节点从场景树移除节点的position属性没有重置。下次get出来直接setParent位置还是旧的。解决在put之前或者get之后显式重置节点位置和状态。我一般在get之后统一做const bullet PoolManager.instance.get(this.bulletPrefab, bullet); bullet.setPosition(0, 0, 0); bullet.active true;5.2 敌机生成时重叠导致瞬间秒杀现象两架敌机生成在同一位置玩家子弹打中一架另一架也判定命中或者玩家飞机同时撞两架直接游戏结束。原因随机 x 位置没有做间隔判断Math.random()可能连续生成相近的坐标。解决生成时记录上一次的 x 位置如果新位置和上次距离小于一个阈值比如 80 像素就重新随机或者偏移。更简单的做法是把屏幕宽度分成若干列每次随机选不同的列生成。5.3 微信开发者工具正常真机白屏现象在微信开发者工具里跑得好好的传到真机上打开一片白没有任何报错。原因通常是资源加载失败或者分包配置错误。真机上资源是从网络加载的如果资源服务器地址配错、或者分包路径不对首屏资源加载不出来就白屏。解决打开微信开发者工具的「真机调试」看 Console 里有没有 404 或者加载超时。检查构建面板里的「资源服务器地址」是否填了正确的域名域名要在微信公众平台的「开发设置 → 服务器域名」里配置白名单。如果是纯本地资源确认「不填资源服务器地址」时构建选项里勾了「内联所有资源」。5.4 触摸拖动时飞机抖动现象手指按住拖动飞机一卡一卡地抖不跟手。原因TOUCH_MOVE事件触发频率不稳定加上update里的插值系数太小导致飞机在目标位置附近来回震荡。解决把插值系数调大dt * 20以上或者改用「目标位置 最大速度限制」的方式让飞机以固定速度追目标位置而不是插值。另外确认update里没有同时做其他重计算帧率稳定了抖动自然消失。5.5 音效播放延迟或重叠现象发射子弹的音效延迟半秒才响或者连续发射时音效叠在一起很吵。原因微信小游戏的音频播放有延迟尤其是首次播放。连续发射时每次都play同一个音效会叠加。解决音效预加载在游戏开始时把常用音效preload一遍。连续音效做节流比如子弹音效设置一个最小间隔 0.1 秒间隔内不重复播放。背景音乐用loop播放不要手动循环。6. 进阶技巧用数据驱动难度曲线与分数系统飞机大战玩到后面玩家能不能留下来全看难度曲线和分数反馈。我一般会把难度参数抽成一张配置表而不是硬编码在代码里。这样调数值不用改代码改表就行。// difficulty.json { levels: [ { score: 0, spawnInterval: 1.2, enemySpeed: 150, enemyHp: 1 }, { score: 500, spawnInterval: 1.0, enemySpeed: 180, enemyHp: 1 }, { score: 1500, spawnInterval: 0.8, enemySpeed: 220, enemyHp: 2 }, { score: 3000, spawnInterval: 0.6, enemySpeed: 260, enemyHp: 3 }, { score: 5000, spawnInterval: 0.45,enemySpeed: 300, enemyHp: 4 } ] }游戏运行时根据当前分数查表取对应的参数。spawnInterval是敌机生成间隔enemySpeed是敌机下落速度enemyHp是敌机血量。分数每跨过一个档位参数平滑过渡不要瞬间跳变否则玩家会感觉「突然变难」。分数系统本身也有讲究。击毁一架敌机给多少分要按敌机血量来定血量高的给高分这样玩家才有动力打硬敌机。连击奖励可以加但不要复杂化飞机大战的爽感来自「打爆」的即时反馈不是复杂的连击计算。验证难度曲线是否合理我一般会自己玩三遍第一遍看前 30 秒是否太无聊第二遍看 1 到 2 分钟是否开始有压力第三遍看 3 分钟后是否还能撑住。如果第一遍就死了说明前期太难如果三遍都轻松过 5000 分说明后期太简单。这个只能靠手感调没有公式。最后说一个我踩过的坑分数 Label 每帧更新会导致 DrawCall 增加因为文字重新渲染会打断合批。解决办法是分数变化时才更新 Label不要放在update里每帧刷。这个细节在真机上影响很明显尤其是分数跳动频繁的时候。希望帮到你。本文还有配套的精品资源点击获取