1. 项目概述:从源码到可运营的飞机大战
最近有不少朋友在找Cocos Creator开发的飞机大战小游戏源码,而且特别强调要“支持流量主”和“可编译各大平台”。这其实反映了一个很实际的需求:大家不再满足于仅仅学习一个游戏Demo,而是希望拿到一个能直接跑起来、能接入广告变现、并且能一键发布到微信小游戏、抖音小游戏、原生平台等渠道的完整项目。我手头正好有一个基于Cocos Creator 3.x版本迭代了多次的飞机大战项目,今天就来把它彻底拆解一遍,从代码结构、核心玩法实现,到流量主接入和全平台发布,把里里外外都讲清楚。
这个项目麻雀虽小,五脏俱全。它不仅仅是一个教学范例,更是一个可以直接上线的产品原型。核心玩法是经典的纵版卷轴射击:玩家控制飞机躲避敌机弹幕并击落它们,获取分数和道具。但在此基础上,我们集成了完整的游戏框架:资源管理、场景切换、UI系统、音效管理、数据持久化(本地存档)、以及最重要的——广告(流量主)接入模块。使用Cocos Creator 3.8.1开发,确保了对OpenHarmony、微信小游戏、字节小游戏、Web、iOS、Android等平台的良好支持。无论你是想学习Cocos Creator的实战开发,还是想快速拥有一个能上线运营的小游戏基底,这个项目都能提供直接的参考。
2. 项目整体架构与设计思路
拿到一个源码项目,第一步不是直接看代码,而是先理解它的整体架构。一个好的架构能让后续的功能扩展、问题排查和平台适配事半功倍。我们这个飞机大战项目采用了在Cocos Creator社区中比较流行且实用的模块化设计。
2.1 核心目录结构解析
打开项目资源管理器,你会看到类似下面的结构。我强烈建议在开始任何修改前,先熟悉这个结构。
assets/ ├── scripts/ # 所有游戏逻辑脚本 │ ├── manager/ # 管理器模块(单例) │ │ ├── GameManager.ts # 游戏总控:状态、分数、生命周期 │ │ ├── AudioManager.ts # 音效统一管理 │ │ ├── UIManager.ts # UI面板打开/关闭、弹窗管理 │ │ ├── PoolManager.ts # 对象池(用于飞机、子弹、特效) │ │ └── AdManager.ts # 广告管理(封装各平台差异) │ ├── entity/ # 游戏实体 │ │ ├── Player.ts # 玩家飞机逻辑(移动、射击、受击) │ │ ├── Enemy.ts # 敌机基类,包含不同敌机类型子类 │ │ ├── Bullet.ts # 子弹基类(玩家子弹、敌机子弹) │ │ └── Prop.ts # 道具(加生命、火力升级等) │ ├── ui/ # UI面板控制脚本 │ │ ├── GameUI.ts # 游戏内HUD(分数、生命值) │ │ ├── StartPanel.ts # 开始界面 │ │ ├── OverPanel.ts # 结束界面 │ │ └── SettingPanel.ts # 设置界面 │ └── utils/ # 工具类 │ ├── Constants.ts # 全局常量(标签、层、事件名) │ └── StorageUtil.ts # 本地存储封装 ├── resources/ # 动态加载的资源(预制体、音效等) ├── scenes/ # 游戏场景(如:start, game, loading) └── ... (其他Cocos标准目录)这种分层的模块化设计有几个明显好处。首先,高内聚低耦合:GameManager负责游戏流程,AudioManager只管声音,AdManager专注广告。修改一个模块(比如广告接口变了)不会轻易影响到游戏核心逻辑。其次,易于维护和协作:新人接手能快速定位功能对应的脚本。最后,便于平台适配:比如AdManager.ts里会用条件编译或动态加载来区分微信小游戏和抖音小游戏的广告API,游戏逻辑完全不用关心这些。
2.2 对象池(PoolManager)的设计考量
飞机大战中,子弹、敌机、爆炸特效都是高频创建和销毁的对象。如果每一发子弹、每一个敌机都走完整的instantiate(实例化)和destroy(销毁)流程,在低端手机或Web平台很快就会引起严重的性能卡顿和内存抖动。因此,对象池是这类射击游戏的性能生命线。
我们的PoolManager是一个通用的单例管理器。它的核心思路是“回收再利用”。当需要创建一个敌机时,不是直接new,而是向对象池“申请”。池子里如果有之前回收的、闲置的敌机节点,就把它拿出来,重置状态(位置、血量、速度)后“激活”使用;如果池子空了,才真正实例化一个新的。当敌机被击毁或飞出屏幕,不是直接销毁它,而是调用“回收”方法,将其隐藏、重置并放回池子,等待下次使用。
实操心得:在设计对象池时,一个常见的坑是忘记彻底重置回收对象的状态。比如敌机被回收时,如果它的速度、血量、当前动画状态没有重置,下次取出来就会带着上一次的“残影”继续运行,导致诡异的Bug。我们的做法是在每个实体(如
Enemy.ts)内部提供一个reset()方法,在回收和取出时由PoolManager调用。
2.3 事件驱动通信
游戏里各个部分需要通信:玩家得分了要通知UI更新,飞机死了要弹出结束界面,点击复活按钮要触发广告并通知游戏逻辑。如果让这些模块互相直接引用、调用方法,代码会变成一团乱麻,耦合度极高。我们采用了基于Cocos Creator内置的EventTarget系统构建的全局事件中心(通常集成在GameManager或一个单独的EventManager中)。
例如,在Player.ts中,当玩家击中敌机:
// Player.ts this.node.emit(Constants.EventType.ADD_SCORE, {score: 100}); // 发射加分事件在GameUI.ts中,监听这个事件:
// GameUI.ts - onLoad 方法中 this.node.on(Constants.EventType.ADD_SCORE, this.updateScore, this);这样,Player完全不知道GameUI的存在,它只负责“广播”事件。GameUI自己决定是否监听以及如何响应。这种模式让增加新功能(比如击中敌机时播放一个音效)变得非常容易,只需要在AudioManager里再监听同一个事件即可,无需修改Player的代码。
3. 核心玩法模块深度拆解
理解了架构,我们深入到游戏最核心的部分:玩家控制、敌机逻辑、碰撞与伤害。这部分代码写得好不好,直接决定了游戏的手感和体验。
3.1 玩家控制与手感调优
玩家飞机的控制代码在Player.ts中,核心是update方法里的移动逻辑。我们通常支持两种操作:触摸拖拽(移动端)和键盘控制(PC端/调试)。
// Player.ts - update 方法片段 update(deltaTime: number) { // 移动端触摸控制 if (Input.isMousePressed) { let touchPos = view.getUIMousePosition(); // 获取触摸点UI坐标 let worldPos = this.uiCamera.screenToWorld(touchPos); // 转换到世界坐标 // 使用缓动(Lerp)让飞机移动更平滑,而不是瞬间跳过去 this.node.position = Vec3.lerp(new Vec3(), this.node.position, worldPos, this.moveLerpFactor); } // 键盘控制(WASD或方向键) let moveDir = Vec3.ZERO; if (Input.isKeyPressed(KeyCode.KEY_A)) moveDir.x -= 1; if (Input.isKeyPressed(KeyCode.KEY_D)) moveDir.x += 1; if (Input.isKeyPressed(KeyCode.KEY_W)) moveDir.y += 1; if (Input.isKeyPressed(KeyCode.KEY_S)) moveDir.y -= 1; if (!moveDir.equals(Vec3.ZERO)) { moveDir.normalize(); // 归一化,避免斜向移动更快 this.node.position.add(moveDir.multiplyScalar(this.moveSpeed * deltaTime)); } // 边界限制:防止飞机飞出屏幕 this.clampPosition(); }手感调优关键点:
- 移动平滑度:直接设置
position到触摸点会显得生硬。使用Vec3.lerp(线性插值)进行平滑过渡,moveLerpFactor参数(如0.1)控制跟随快慢。值越小,跟随越平滑但有延迟;值越大,响应越快但可能抖动。需要根据游戏风格反复测试。- 移动速度与deltaTime:一定要用
this.moveSpeed * deltaTime来计算帧间位移。这能保证在不同帧率(30FPS或60FPS)的设备上,飞机的移动速度是恒定的。- 边界处理:
clampPosition()函数确保飞机的x, y坐标始终在屏幕可视范围内。计算时要注意考虑飞机精灵的锚点(anchor)和尺寸,确保是机身中心还是机头不超出边界。
3.2 敌机生成与行为树(简化版)
敌机系统是游戏趣味性的来源。我们设计了多种敌机类型(普通机、快速机、Boss),每种有不同的血量、移动模式、开火方式。在EnemySpawner(敌机生成器)脚本中,我们通常使用一个计时器配合波次配置表来生成敌机。
更高级的做法是引入一个简化的行为树或状态机来控制单个敌机的行为。例如,一个Boss敌机可能的行为序列是:入场 -> 盘旋 -> 发射扇形弹幕 -> 冲锋 -> 发射追踪导弹 -> 循环。我们在EnemyBoss.ts中可以用一个状态枚举和update逻辑来实现:
// EnemyBoss.ts enum BossState { ENTER, CIRCLE, FAN_SHOOT, CHARGE, HOMING_SHOOT } private _state: BossState = BossState.ENTER; private _stateTimer: number = 0; update(deltaTime: number) { this._stateTimer += deltaTime; switch (this._state) { case BossState.ENTER: // 从屏幕上方移动到指定位置 if (this._stateTimer > 2.0) { // 2秒后进入下一状态 this.changeState(BossState.CIRCLE); } break; case BossState.CIRCLE: // 实现圆周运动 this.node.position = this._circleCenter.add(new Vec3(Math.cos(this._stateTimer), Math.sin(this._stateTimer)).multiplyScalar(this._circleRadius)); if (this._stateTimer > 5.0) { this.changeState(BossState.FAN_SHOOT); } break; // ... 其他状态 } }注意事项:敌机的行为逻辑不要写得太“死”。尽量将行为参数化,比如圆周运动的中心、半径、速度,弹幕的子弹数量、角度、发射间隔等,做成可在编辑器里调整的属性(用
@property装饰器)。这样策划或你自己调整关卡难度时,不需要修改代码,只需在Cocos Creator的属性检查器中拖拖滑块就能完成。
3.3 碰撞检测与伤害系统
碰撞是射击游戏的核心。Cocos Creator提供了物理引擎和碰撞组件两种方式。对于飞机大战这种2D游戏,使用碰撞组件(Collider)更轻量、更直观。我们为玩家、敌机、子弹、道具都添加相应的碰撞组件(如BoxCollider2D或CircleCollider2D),并设置好分组(Group)。
关键在于Player.ts、Enemy.ts、Bullet.ts中的onCollisionEnter回调函数。
// Bullet.ts (玩家子弹) onCollisionEnter(other: Collider2D, self: Collider2D) { // 判断碰撞到的对象分组 if (other.group === Constants.CollisionGroup.ENEMY) { let enemy = other.node.getComponent(Enemy); if (enemy) { enemy.takeDamage(this._damage); // 调用敌机的受伤方法 this.recycleToPool(); // 子弹命中后回收进对象池 // 播放命中特效 EffectManager.instance.playHitEffect(this.node.position); } } }深度解析:伤害计算与传递:为什么是
enemy.takeDamage(this._damage),而不是直接在子弹脚本里减少敌机血量?这体现了关注点分离原则。子弹只负责通知“我打中你了,伤害值是X”。具体怎么处理伤害(减血、播放受击动画、判断死亡)是敌机自己的职责。这样设计后,未来如果想增加“无敌状态”、“伤害吸收盾”等效果,只需要修改Enemy.takeDamage方法,子弹逻辑完全不用动。同理,_damage这个值也可以根据玩家当前的火力等级进行动态计算,实现丰富的成长体系。
4. 流量主(广告)系统集成实战
支持流量主是这个小游戏源码的核心价值之一。广告接入的难点不在于调用API,而在于如何优雅地设计,使其对游戏代码的侵入性最小,并且能灵活适配不同平台。
4.1 广告管理器(AdManager)抽象设计
我们的目标是:游戏逻辑(比如GameManager)只需要调用AdManager.instance.showRewardedVideo('revive'),就能播放一个激励视频,并在广告播放成功后,由AdManager通知游戏逻辑“revive(复活)这个动作完成了”。至于这个广告是微信的、抖音的、还是其他平台的,游戏逻辑不关心。
为此,我们设计了一个抽象层。AdManager.ts作为总入口,内部根据编译平台条件,动态加载或实例化不同的平台适配器。
// AdManager.ts export class AdManager { private static _instance: AdManager = null; private _adapter: IPlatformAdAdapter; // 平台适配器接口 public static get instance(): AdManager { if (!this._instance) { this._instance = new AdManager(); this._instance.init(); } return this._instance; } private init() { // 根据平台初始化不同的适配器 #if WECHAT_MINI_GAME this._adapter = new WeChatAdAdapter(); #elif BYTEDANCE_MINI_GAME this._adapter = new ByteDanceAdAdapter(); #else this._adapter = new DefaultAdAdapter(); // 用于PC调试的空实现 #endif this._adapter.initialize(); } public showRewardedVideo(adId: string, onSuccess: Function, onFail?: Function) { if (!this._adapter.isRewardedVideoLoaded()) { // 如果广告未加载好,可以给用户提示,或者直接调用onFail UIManager.instance.showToast('广告加载中,请稍候'); this._adapter.loadRewardedVideo(() => { this._adapter.showRewardedVideo(adId, onSuccess, onFail); }); return; } this._adapter.showRewardedVideo(adId, onSuccess, onFail); } // ... 其他广告类型方法:showBanner, showInterstitial等 } // 平台适配器接口 interface IPlatformAdAdapter { initialize(): void; isRewardedVideoLoaded(): boolean; loadRewardedVideo(callback?: Function): void; showRewardedVideo(adId: string, onSuccess: Function, onFail?: Function): void; // ... 其他广告方法 }4.2 微信小游戏广告接入详解
以微信小游戏平台为例,我们实现WeChatAdAdapter。微信的广告API是全局的wx.createRewardedVideoAd。
// WeChatAdAdpater.ts export class WeChatAdAdpater implements IPlatformAdAdapter { private _rewardedVideoAd: any = null; private _videoAdId: string = 'your_ad_unit_id_here'; // 从微信后台获取 initialize() { // 注意:微信小游戏环境判断 if (typeof wx !== 'undefined' && wx.createRewardedVideoAd) { this._rewardedVideoAd = wx.createRewardedVideoAd({ adUnitId: this._videoAdId }); // 监听广告错误 this._rewardedVideoAd.onError((err: any) => { console.error('激励视频广告加载/播放失败', err); }); // 预加载 this.loadRewardedVideo(); } } isRewardedVideoLoaded(): boolean { return this._rewardedVideoAd !== null; } loadRewardedVideo(callback?: Function) { if (this._rewardedVideoAd) { this._rewardedVideoAd.load().then(() => { callback && callback(); }).catch((err: any) => { console.error('预加载激励视频失败', err); }); } } showRewardedVideo(adId: string, onSuccess: Function, onFail?: Function) { if (!this._rewardedVideoAd) { onFail && onFail('广告未初始化'); return; } // 监听用户看完广告获得奖励 this._rewardedVideoAd.onClose((res: any) => { // 注意:res.isEnded 表示用户是否完整观看了视频 if (res && res.isEnded) { onSuccess(); // 观看完成,触发成功回调 } else { // 用户中途关闭了广告 onFail && onFail('广告未播放完毕'); } }); this._rewardedVideoAd.show().catch(() => { // 如果show失败,尝试重新加载后再展示 this._rewardedVideoAd.load() .then(() => this._rewardedVideoAd.show()) .catch((err: any) => { onFail && onFail('广告展示失败'); }); }); } }避坑指南:广告加载与异常处理:
- 广告单元ID:每个平台的广告后台(如微信小游戏后台、字节跳动开发者平台)都需要创建广告位,获得唯一的
adUnitId。切记不要在代码中硬编码,应该做成配置表或从服务器动态获取,方便后期更换。- 加载时机:广告加载需要时间。最佳实践是在游戏启动时、关卡加载间隙等时机预加载激励视频和插屏广告。调用
show之前,一定要用isRewardedVideoLoaded()检查状态,如果未加载好,先调用load并给用户友好提示(如“广告加载中”),加载成功后再自动触发展示。直接调用show在未加载好时会失败。- 关闭回调与奖励发放:
onClose回调是发放奖励的唯一依据。必须判断res.isEnded(微信)或等效字段。只有用户完整看完广告,才能发放游戏内奖励(如复活、金币)。绝对不能在广告一打开就发放奖励,否则会被平台判定为违规,导致广告权限被关闭。- 测试与正式环境:各平台都提供测试专用的广告ID,在开发阶段使用它们,不会产生实际计费,也能看到完整的广告流程。
4.3 广告场景与用户体验平衡
接入广告不是为了赶走用户,而是为了在提供免费游戏的同时获得合理收益。如何放置广告至关重要。
激励视频(Rewarded Video):用于正向激励。经典场景:
- 游戏复活:玩家飞机被击毁后,提供“观看广告复活”选项。
- 关卡结算奖励翻倍:通关后,奖励金币或道具,提供“看广告翻倍”选项。
- 获取稀缺资源:每日免费宝箱次数用完后,看广告可额外开启。
技巧:复活广告的按钮要醒目,但不要强制弹出遮挡画面。通常是在游戏结束界面,作为一个与“重新开始”、“返回主页”并列的选项。
插屏广告(Interstitial Ad):用于自然间隔点。经典场景:
- 游戏关卡之间:从一关切换到下一关的加载界面之后。
- 返回主菜单时:从游戏内退出到开始界面时。
技巧:频率不能太高,避免每次死亡或返回都弹插屏,会引起用户反感。可以设计为“每玩三局弹出一次”或“仅在分数超过一定阈值后弹出”。
Banner广告(横幅广告):用于长期曝光。
- 通常放在游戏主页面的顶部或底部。
技巧:在核心游戏进行中(即
Game场景)不要显示Banner广告,它会遮挡游戏区域,影响操作和体验。只在非游戏的主界面、商店界面等地方显示。
在我们的源码中,UIManager和GameManager会协作管理这些广告触发点。例如,在OverPanel.ts(结束界面)的show方法中,会根据一个随机数或局数计数,决定是否展示插屏广告。而复活按钮的点击事件,则直接调用AdManager.instance.showRewardedVideo('revive', this.onReviveSuccess.bind(this))。
5. 多平台编译与发布流程
“可编译各大平台”是另一个硬性要求。Cocos Creator的强大之处就在于其“一次开发,多平台发布”的能力。但“能编译”和“能顺利上线”之间,还有不少配置和适配工作。
5.1 平台相关配置要点
在Cocos Creator的项目设置和构建发布面板中,需要针对不同平台进行配置。
微信/抖音小游戏:
- AppID:必须填写从对应平台后台获取的正确AppID。
- 游戏包名:通常与AppID关联,需保持一致。
- 小游戏启动路径:一般保持默认即可。
- **注意:这些平台的构建产物是一个
.wx或.bytedance的目录,需要使用对应平台的开发者工具(微信开发者工具、抖音开发者工具)打开这个目录,进行进一步的预览、调试和上传。
原生平台(iOS/Android):
- 包名(Bundle Identifier / Package Name):格式必须正确,如
com.yourcompany.planewar。这是应用的唯一标识,上架商店的关键。 - 应用名称、版本号、图标:根据商店要求设置。
- 构建模板:选择
default或link(纯引擎)。对于简单小游戏,default模板即可,它包含了必要的原生壳。 - 加密脚本:如果担心代码被反编译,可以勾选“加密脚本”,并设置一个密钥。务必保管好密钥,丢失后无法恢复。
- 包名(Bundle Identifier / Package Name):格式必须正确,如
Web Mobile:
- 标题:浏览器标签页上显示的名字。
- 服务器地址:如果游戏资源需要从服务器加载,在此填写。本地调试可留空。
- 内嵌数据:建议勾选,将脚本和资源打包成一个单一的
.html文件,部署最简单。
5.2 条件编译处理平台差异
不同平台的API、能力、屏幕适配规则可能不同。我们使用Cocos Creator的条件编译功能来编写平台特定的代码。这在之前的AdManager中已经看到(#if WECHAT_MINI_GAME)。
另一个常见场景是分享功能。微信小游戏有wx.shareAppMessage,抖音小游戏有tt.share。
// ShareManager.ts public shareGame() { #if WECHAT_MINI_GAME wx.shareAppMessage({ title: '超好玩的飞机大战,快来挑战!', imageUrl: 'assets/share.jpg' // 分享图路径 }); #elif BYTEDANCE_MINI_GAME tt.share({ channel: 'video', title: '一起来玩飞机大战', // ... 抖音分享参数 }); #else // 其他平台(如Web),可以复制链接或提示 console.log('分享功能仅在特定平台可用'); #endif }构建发布实操步骤:
- 菜单栏 -> 项目 -> 构建发布,打开构建面板。
- 在左上角下拉框选择目标平台(如“微信小游戏”)。
- 填写该平台必填的配置项(如AppID)。
- 点击构建。构建完成后,点击生成(对于小游戏平台)或编译(对于原生平台,需要提前配置好Android SDK/NDK或Xcode环境)。
- 对于小游戏:用对应开发者工具打开生成目录进行调试和上传。
- 对于原生平台:编译后会生成Xcode项目或Android APK/ABI,可进行真机调试和商店发布。
5.3 屏幕适配与性能优化
不同平台的设备分辨率碎片化严重。Cocos Creator的Canvas组件提供了Fit Height,Fit Width等适配策略。对于飞机大战这种竖屏游戏,通常选择Fit Height模式,并设计一个固定的设计分辨率宽度(如750px)。这样在不同宽高比的手机上,游戏内容都能在垂直方向上完整显示,水平方向两侧可能会有黑边或扩展背景,这是可接受的。
性能优化是保证低端机流畅运行的关键:
- Draw Call合并:确保UI和游戏场景中,材质、纹理相同的静态精灵尽量放在同一个节点下,或者使用自动图集(Auto Atlas)功能,将碎图打包。
- 减少透明重叠:大量半透明的Sprite(如爆炸特效)重叠会显著增加Overdraw。控制同时出现的爆炸数量,或使用粒子系统替代序列帧动画。
- 对象池:如前所述,这是必须的。
- 纹理压缩:针对Android和iOS平台,在构建时启用对应的纹理压缩格式(如ASTC, PVRTC, ETC2),能大幅减少包体和内存占用。
- 释放无用资源:在场景切换时(如从游戏场景回到开始菜单),手动释放游戏场景中加载的、不再需要的资源(如敌机预制体、背景纹理),调用
resources.release或assetManager.release。
6. 常见问题排查与调试技巧
在实际开发和编译过程中,你肯定会遇到各种问题。这里记录一些高频问题的排查思路。
6.1 编译与运行问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 构建微信小游戏后,用开发者工具打开白屏/报错 | 1. 未在微信开发者工具中设置“详情->本地设置->不校验合法域名”。 2. 项目路径包含中文或特殊字符。 3. 构建时勾选了“分离引擎”,但代码中有依赖引擎模块的动态加载。 | 1. 勾选“不校验合法域名”。 2. 将项目移到纯英文路径下。 3. 取消勾选“分离引擎”,或检查动态加载代码。 |
| 原生平台(Android/iOS)编译失败 | 1. 环境未配置好(SDK, NDK, Xcode)。 2. Cocos Creator版本与NDK版本不兼容。 3. 包名格式错误。 | 1. 检查Cocos Creator偏好设置中的原生开发环境路径。 2. 查阅官方文档,使用推荐的NDK版本(如r21e)。 3. 检查包名是否符合规范(如 com.xxx.xxx)。 |
| 游戏在真机上非常卡顿 | 1. 未使用对象池,大量Instantiate/Destroy。 2. 单帧内Draw Call过高。 3. 脚本中存在耗时操作(如复杂计算)在 update中每帧执行。 | 1. 确保所有频繁生成的对象都使用了对象池。 2. 使用Cocos Creator的“分析器”查看Draw Call和性能瓶颈。 3. 优化算法,或将耗时操作分散到多帧执行。 |
6.2 广告相关问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 激励视频广告无法加载或播放 | 1. 广告单元ID填写错误或未生效。 2. 未在对应平台后台开通广告位或账户异常。 3. 测试设备不在广告测试白名单中(仅限测试阶段)。 4. 网络问题。 | 1. 仔细核对广告单元ID,确保从正确平台的后台复制。 2. 登录平台开发者后台,检查广告功能状态和账户状态。 3. 在后台将测试设备的ID(如微信的OpenID)加入测试名单。 4. 检查设备网络连接。 |
| 广告播放成功,但奖励未发放 | 1. 广告关闭回调中,未正确判断isEnded字段。2. 发放奖励的代码逻辑有误或未执行。 3. 游戏状态在广告播放期间被重置。 | 1. 确保只在isEnded为true时才调用奖励回调。2. 在奖励回调函数内加日志,确认函数被触发并执行。 3. 广告播放前暂停游戏逻辑,播放后恢复,防止状态冲突。 |
| Banner广告不显示或位置错误 | 1. 容器节点尺寸为0或未激活。 2. 平台API调用时机不对(如在onLoad中调用,但Canvas未适配完成)。 3. 样式设置(如top, left)错误。 | 1. 确保放置Banner的节点有正确尺寸且active为true。 2. 在 start或第一个update中,或监听Canvas的size-changed事件后再创建Banner。3. 参考平台文档,确认样式参数的单位和含义。 |
6.3 游戏逻辑与体验问题
问题:碰撞检测不灵敏或穿透
- 排查:首先在Cocos Creator编辑器中,打开“场景”面板上的“物理调试”或“碰撞检测调试”绘制,查看碰撞体的形状和位置是否与精灵显示一致。常见原因是碰撞体大小设置不当,或者精灵的锚点(Anchor)导致其视觉中心与碰撞体中心不重合。
- 解决:调整碰撞组件(Collider)的Offset(偏移)和Size(尺寸)属性,使其匹配精灵的可见区域。对于高速移动的物体(如子弹),可以考虑在
update中使用射线检测(Raycast)进行更精确的帧间碰撞判断。
问题:游戏在不同设备上速度不一致
- 根本原因:移动、动画等逻辑没有乘以
deltaTime(帧时间差)。deltaTime是上一帧到当前帧的时间间隔,在高帧率(如60FPS)设备上值小,低帧率(如30FPS)设备上值大。用deltaTime参与计算,可以保证“距离=速度*时间”这个公式在任何帧率下都成立。 - 检查:搜索所有
update(dt)方法,检查飞机移动速度、子弹速度、敌机移动、动画播放速度等,是否都使用了* dt或+= dt。
问题:本地存储(存档)在部分安卓手机失效
- 原因:Cocos Creator的
sys.localStorage在Web和小游戏平台是稳定的,但在某些原生Android系统上,如果应用被强制清除数据或权限异常,可能会失败。 - 加固方案:对存储的数据增加校验和备份机制。例如,存储时不仅存数据,还存一个简单的校验码(如对数据字符串做MD5哈希的前几位)。读取时先校验,如果校验失败,尝试读取一个备份存档。关键数据(如最高分)可以考虑在游戏正常退出时主动同步一次。