SaveDataManager的奖励计算逻辑完全正确,能够正确读取配置并给予奖励GameEnd.ts中的currentRewards被意外重置为{money: -1, diamonds: -1}事件时序冲突:当用户点击返回主菜单按钮时,GameManager.onMainMenuClick()方法会触发RESET_UI_STATES事件,这会导致GameEnd.onResetUI()方法被调用,将currentRewards重置为{money: -1, diamonds: -1},覆盖了之前正确计算的奖励显示。
根据控制台输出和代码分析,发现游戏结束后存在重复执行的问题:
触发事件: GAME_SUCCESS 或 GAME_DEFEAT
负责组件: GameEnd.ts
职责范围:
奖励计算和显示
SaveDataManager.giveCompletionRewards() 或 giveFailureRewards()UI状态管理
游戏状态设置
不负责的事项:
触发事件: CONTINUE_CLICK → RETURN_TO_MAIN_MENU
负责组件: GameManager.ts → NavBarController.ts → MainUIController.ts
职责范围:
游戏数据清理
inGameManager.triggerGameDataCleanup()关卡进度管理
应用状态切换
镜头重置
界面切换
UI面板管理
数据重置和刷新
在 GameEnd.ts 中添加状态标志 hasProcessedGameEnd:
private hasProcessedGameEnd: boolean = false;
private async calculateAndShowRewards() {
if (this.hasProcessedGameEnd) {
console.log('[GameEnd] 游戏结束已处理过,跳过重复计算');
return;
}
this.hasProcessedGameEnd = true;
// ... 奖励计算逻辑
}
GameEnd.onResetUI()方法在用户点击返回主菜单时被调用,会将currentRewards重置为{money: -1, diamonds: -1},覆盖正确的奖励显示。
A. 移除onResetUI中的currentRewards重置
private onResetUI() {
console.log('[GameEnd] 重置UI状态');
// 停止所有动画
this.stopAllAnimations();
// 直接重置到隐藏状态,不使用动画
this.initializeHiddenState();
// 重置所有状态,为下一局游戏做准备
this.hasDoubledReward = false;
// 注意:不重置currentRewards,保持奖励显示直到下次游戏开始
// this.currentRewards = {money: -1, diamonds: -1}; // 移除这行
this.isGameSuccess = false;
this.hasProcessedGameEnd = false;
}
B. 添加游戏开始时的奖励重置
// 在setupEventListeners中添加GAME_START事件监听
eventBus.on(GameEvents.GAME_START, this.onGameStart, this);
// 新增onGameStart方法
private onGameStart() {
console.log('[GameEnd] 收到游戏开始事件,重置奖励显示');
// 重置奖励显示,为新游戏做准备
this.currentRewards = {money: 0, diamonds: 0};
this.hasProcessedGameEnd = false;
this.hasDoubledReward = false;
this.isGameSuccess = false;
}
C. 完善事件监听器清理
// 在onDisable和onDestroy中添加GAME_START事件注销
eventBus.off(GameEvents.GAME_START, this.onGameStart, this);
// 在onGameSuccess和onGameDefeat中添加重复检查
if (this.isGameSuccess && this.currentRewards.money > 0) {
console.log('[GameEnd] 游戏成功事件已处理过,跳过重复执行');
return;
}
// 在calculateAndShowRewards中添加重复计算检查
if (this.currentRewards.money > 0 || this.currentRewards.diamonds > 0) {
console.log('[GameEnd] 奖励已计算过,跳过重复计算');
this.updateRewardDisplay();
// 确保面板显示
this.showEndPanelWithAnimation();
return;
}
分析结论: 敌人波次和数量的重置在关卡数据加载时会自动覆盖,无需在游戏结束时特别处理。
详细分析:
this.levelWaves = levelConfig.wavesapplyLevelConfig() 中重新设置 levelWaves、currentWave、levelTotalEnemiesstartWave() 中重置 currentWave、totalWaves、currentWaveTotalEnemies、currentWaveEnemiesSpawned、currentWaveEnemyConfigsRESET_ENEMY_CONTROLLER 事件结论:
游戏结束触发
↓
[阶段一] UI弹出阶段
↓
GAME_SUCCESS/GAME_DEFEAT → GameEnd.ts
↓
- 奖励计算和显示
- UI面板显示
- 音效播放
- 状态设置
↓
用户点击继续按钮
↓
[阶段二] 真正结束阶段
↓
CONTINUE_CLICK → GameManager.onMainMenuClick()
↓
- 游戏数据清理
- 关卡进度管理
- 应用状态切换
↓
RETURN_TO_MAIN_MENU → NavBarController + MainUIController
↓
- 镜头重置
- UI面板切换
- 数据刷新
↓
返回主界面完成
测试步骤:
预期结果:
[GameEnd] 游戏失败事件处理 日志[GameEnd] 奖励计算完成 日志测试步骤:
预期结果:
GAME_DEFEAT 事件CONTINUE_CLICK 事件测试步骤:
预期结果:
[GameEnd] 游戏成功事件处理 日志[GameEnd] 奖励计算完成 日志[GameEnd] 开始播放GameEnd面板弹出动画 日志控制台日志检查
[GameEnd] 相关日志不应重复出现[GameEnd] 开始播放GameEnd面板弹出动画 应只出现一次[GameEnd] 奖励计算完成 应只出现一次[TopBarController] 刷新货币显示 不应频繁重复[SaveDataManager] 奖励相关日志应只出现一次UI状态检查
数据一致性检查
通过测试脚本验证了问题的根本原因:
currentRewards在onResetUI()中被重置为{money: -1, diamonds: -1}RESET_UI_STATES事件,导致奖励显示被覆盖使用test_game_end_fix.js测试脚本验证修复效果:
=== 测试修复后的游戏结束流程 ===
--- 步骤1: 游戏开始 ---
游戏开始后currentRewards: { money: 0, diamonds: 0 }
--- 步骤2: 游戏成功 ---
游戏成功后currentRewards: { money: 300, diamonds: 20 }
--- 步骤3: 点击返回主菜单 ---
重置UI后currentRewards: { money: 300, diamonds: 20 } // 保持不变!
--- 步骤4: 下一局游戏开始 ---
新游戏开始后currentRewards: { money: 0, diamonds: 0 } // 正确重置
✅ 修复成功!奖励显示在重置UI后仍然保持正确
✅ 新游戏开始时奖励正确重置为0
通过test_unified_defeat_flow.js测试脚本验证统一流程:
1. 测试墙体血量为0的失败处理
[Wall] 墙体被摧毁,触发游戏失败
[EventBus] 触发事件: WALL_DESTROYED
[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致
[EventBus] 触发事件: GAME_DEFEAT
[GameEnd] 接收到GAME_DEFEAT事件 (第1次)
2. 测试菜单退出的失败处理
[MenuController] 退出游戏按钮被点击
[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI
[EventBus] 触发事件: GAME_DEFEAT
[GameEnd] 接收到GAME_DEFEAT事件 (第1次)
3. 流程一致性验证
✅ 流程一致性验证通过
✅ 墙体血量为0和菜单退出都触发了相同数量的GAME_DEFEAT事件
✅ 两种失败处理流程已统一
通过test_menu_defeat_animation.js测试脚本验证:
=== 测试菜单退出GameEnd动画显示修复 ===
[GameEnd] 已注册GAME_DEFEAT事件监听器
1. 测试菜单退出流程(修复后):
[MenuController] 退出游戏按钮被点击
[MenuController] 当前应用状态: in_game
[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI
[EventBus] 触发事件: GAME_DEFEAT
[GameEnd] 接收到GAME_DEFEAT事件
[GameEnd] 游戏失败事件处理,开始统一处理流程
[GameEnd] 计算和显示奖励(包含面板动画显示)
2. 测试墙体血量为0流程:
[Wall] 墙体被摧毁
[Wall] 直接触发GAME_DEFEAT事件,与菜单退出失败处理流程一致
[EventBus] 触发事件: GAME_DEFEAT
[GameEnd] 接收到GAME_DEFEAT事件
[GameEnd] 游戏失败事件处理,开始统一处理流程
[GameEnd] 计算和显示奖励(包含面板动画显示)
=== 事件冲突检查 ===
GAME_DEFEAT事件数量: 2
GAME_RESUME事件数量: 0
✅ 修复成功:菜单退出时不再触发GAME_RESUME事件
✅ 验证通过:墙体血量为0和菜单退出都正确触发GameEnd动画
统一流程验证结果:
GAME_DEFEAT事件GAME_DEFEAT事件GameEnd.ts统一处理游戏失败逻辑问题描述:
解决方案:
统一事件处理流程:
优化动画调用逻辑:
改进日志输出:
问题描述:
!this.isGameSuccess && this.currentRewards.money >= 0在失败时会阻止面板显示解决方案:
添加专用处理状态标志:
hasProcessedGameEnd标志来跟踪游戏结束事件的处理状态修正奖励数据初始化:
currentRewards初始值设为{money: -1, diamonds: -1}>= 0判断是否已计算完善状态重置机制:
onResetUI方法中重置hasProcessedGameEnd标志问题描述: 游戏成功时奖励计算未成功,显示奖励为0。分析发现:
calculateAndShowRewards方法中使用currentRewards.money >= 0判断是否已计算奖励onGameSuccess和onGameDefeat中存在重复的hasProcessedGameEnd检查和设置解决方案:
统一防重复逻辑
calculateAndShowRewards方法中hasProcessedGameEnd标志而不是奖励金额来判断是否已处理onGameSuccess和onGameDefeat中的重复检查修正判断逻辑
优化后的统一流程:
游戏结束事件触发 (GAME_SUCCESS/GAME_DEFEAT)
↓
1. 清理敌人 (clearAllEnemies)
↓
2. 设置游戏状态 (SUCCESS/DEFEAT)
↓
3. 播放音效 (胜利/失败音效)
↓
4. 设置EndLabel文本 (SUCCESS/DEFEAT)
↓
5. 设置成功/失败标志 (isGameSuccess)
↓
6. 计算和显示奖励 (calculateAndShowRewards)
↓
7. 显示GameEnd面板动画 (showEndPanelWithAnimation)
✅ 核心问题已彻底解决:游戏成功时奖励显示异常的问题已完全修复
本次优化通过以下关键措施解决了游戏结束事件的重复执行问题:
RESET_UI_STATES事件导致的奖励显示重置问题问题描述: 墙体血量为0的失败处理与菜单退出的失败处理流程不一致:
GAME_DEFEAT事件WALL_DESTROYED事件 → IN_game.ts检查游戏状态 → 触发GAME_DEFEAT事件统一方案:
让墙体血量为0的失败处理参考菜单退出的处理,两者都直接触发GAME_DEFEAT事件。
具体修改:
修改Wall.ts的onWallDestroyed方法:
// 修改前:只触发WALL_DESTROYED事件,由IN_game.ts处理
eventBus.emit(GameEvents.WALL_DESTROYED, {...});
// 修改后:保留WALL_DESTROYED事件,同时直接触发GAME_DEFEAT事件
eventBus.emit(GameEvents.WALL_DESTROYED, {...});
eventBus.emit(GameEvents.GAME_DEFEAT); // 与菜单退出保持一致
修改IN_game.ts的相关方法:
onWallDestroyedEvent:不再调用checkGameState,因为墙体已直接触发失败事件checkGameState:移除墙体摧毁检查逻辑,只保留敌人击败检查统一后的失败处理流程:
失败触发源 → 直接触发GAME_DEFEAT事件 → GameEnd.ts统一处理
无论是墙体血量为0还是菜单退出,都遵循相同的事件流程,确保处理逻辑的一致性。
问题发现:
在统一失败处理流程后,发现菜单退出时虽然触发了 GAME_DEFEAT 事件,但GameEnd动画没有正常显示。通过分析用户提供的日志发现:
[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI
[MenuController] 游戏中关闭菜单,通过事件系统触发游戏恢复
[EnemyController] 接收到游戏恢复事件,恢复所有敌人
[GameManager] 接收到游戏失败事件,执行失败处理
问题根源:
MenuController.ts 在触发 GAME_DEFEAT 事件前先调用了 closeMenu(),而 closeMenu() 会触发 GAME_RESUME 事件,导致事件冲突。
修复方案:
修改 MenuController.ts 的 onBackButtonClick 方法,先触发 GAME_DEFEAT 事件,再关闭菜单:
if (currentAppState === AppState.IN_GAME) {
console.log('[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI');
// 先触发游戏失败事件,让GameEnd面板显示
const eventBus = EventBus.getInstance();
eventBus.emit(GameEvents.GAME_DEFEAT);
// 关闭菜单(不触发GAME_RESUME事件,避免事件冲突)
await this.closeMenuWithoutResume();
console.log('[MenuController] 菜单退出处理完成,已触发GAME_DEFEAT事件');
}
添加 closeMenuWithoutResume 方法,避免触发 GAME_RESUME 事件:
private async closeMenuWithoutResume(): Promise<void> {
if (!this.isMenuOpen) return;
this.isMenuOpen = false;
// 使用动画隐藏菜单面板
if (this.popupAni) {
await this.popupAni.hidePanel();
}
// 不触发GAME_RESUME事件,避免与GAME_DEFEAT事件冲突
console.log('菜单已关闭(未触发游戏恢复)');
}
修复效果:
GAME_RESUME 事件,后触发 GAME_DEFEAT 事件,事件冲突导致GameEnd动画显示异常GAME_DEFEAT 事件,不触发 GAME_RESUME 事件,避免冲突,GameEnd动画正常显示,与墙体血量为0的处理完全一致RESET_UI_STATES事件导致的奖励显示重置问题currentRewards的重置时机GAME_START事件监听,确保新游戏开始时状态正确重置GAME_DEFEAT事件所有测试用例通过,确认问题已彻底解决:
优化后的流程更加稳定、高效,游戏胜利和失败的处理完全统一,用户体验得到显著提升。