MenuController.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. import { _decorator, Component, Node, Button, find } from 'cc';
  2. import { PopUPAni } from '../../Animations/PopUPAni';
  3. import EventBus, { GameEvents } from '../../Core/EventBus';
  4. import { GameManager, AppState } from '../../LevelSystem/GameManager';
  5. import { SaveDataManager } from '../../LevelSystem/SaveDataManager';
  6. import { GameStartMove } from '../../Animations/GameStartMove';
  7. import { SoundController } from './SoundController';
  8. import { Audio } from '../../AudioManager/AudioManager';
  9. const { ccclass, property } = _decorator;
  10. /**
  11. * 菜单系统控制器
  12. * 负责管理菜单UI的显示和隐藏
  13. */
  14. @ccclass('MenuController')
  15. export class MenuController extends Component {
  16. // UI节点引用
  17. @property(Node) menuUI: Node = null; // Canvas/MenuUI
  18. @property(Button) menuButton: Button = null; // Canvas/MenuButton (主界面使用)
  19. @property(Button) menuButton1: Button = null; // Canvas/MenueButton-001 (游戏中使用)
  20. @property(Button) closeButton: Button = null; // Canvas/MenuUI中的关闭按钮
  21. @property(Button) backButton: Button = null; // Canvas/MenuUI/Buttons/BackButton 退出游戏按钮
  22. @property(Button) continueButton: Button = null; // Canvas/MenuUI/Buttons/ContinueButton 继续游戏按钮
  23. // 需要检测的UI节点引用
  24. @property(Node) skillUpUI: Node = null; // Canvas/SkillUpUI
  25. @property(Node) upgradeUI: Node = null; // Canvas/UpgradeUI
  26. @property(Node) shopUI: Node = null; // Canvas/ShopUI
  27. // 动画控制器
  28. @property(PopUPAni) popupAni: PopUPAni = null; // Canvas/MenuUI上的PopUPAni组件
  29. // 音频控制器
  30. @property(SoundController) soundController: SoundController = null; // Canvas/MenuUI上的SoundController组件
  31. // 游戏管理器引用
  32. @property(GameManager) gameManager: GameManager = null; // GameManager组件引用
  33. // 菜单状态
  34. private isMenuOpen: boolean = false;
  35. // GameStartMove组件引用,用于重置镜头位置
  36. private gameStartMoveComponent: GameStartMove = null;
  37. onLoad() {
  38. this.bindEvents();
  39. // 初始化时隐藏菜单 - 使用PopUPAni的场景外位置隐藏方法
  40. if (this.popupAni) {
  41. this.popupAni.hidePanelImmediate();
  42. }
  43. // 设置界面状态监听
  44. this.setupUIStateListeners();
  45. // 初始化菜单按钮显示状态
  46. this.updateMenuButtonVisibility();
  47. }
  48. start() {
  49. // 初始化菜单按钮状态
  50. this.updateMenuButtonVisibility();
  51. }
  52. /**
  53. * 绑定事件
  54. */
  55. private bindEvents() {
  56. // 绑定菜单按钮点击事件 (主界面使用)
  57. if (this.menuButton) {
  58. this.menuButton.node.on(Button.EventType.CLICK, this.onMenuButtonClick, this);
  59. }
  60. // 绑定菜单按钮1点击事件 (游戏中使用)
  61. if (this.menuButton1) {
  62. this.menuButton1.node.on(Button.EventType.CLICK, this.onMenuButtonClick, this);
  63. }
  64. // 绑定关闭按钮点击事件
  65. if (this.closeButton) {
  66. this.closeButton.node.on(Button.EventType.CLICK, this.onCloseButtonClick, this);
  67. }
  68. // 绑定退出游戏按钮点击事件
  69. if (this.backButton) {
  70. this.backButton.node.on(Button.EventType.CLICK, this.onBackButtonClick, this);
  71. }
  72. // 绑定继续游戏按钮点击事件
  73. if (this.continueButton) {
  74. this.continueButton.node.on(Button.EventType.CLICK, this.onContinueButtonClick, this);
  75. }
  76. }
  77. /**
  78. * 设置UI状态监听器
  79. */
  80. private setupUIStateListeners() {
  81. const eventBus = EventBus.getInstance();
  82. // 监听界面切换事件(如果有的话)
  83. // 这里可以根据实际的界面切换事件来更新菜单按钮显示状态
  84. eventBus.on(GameEvents.RETURN_TO_MAIN_MENU, this.updateMenuButtonVisibility, this);
  85. eventBus.on(GameEvents.UI_PANEL_SWITCHED, this.updateMenuButtonVisibility, this);
  86. // 监听应用状态切换事件
  87. eventBus.on(GameEvents.APP_STATE_CHANGED, this.onAppStateChanged, this);
  88. }
  89. /**
  90. * 检测当前是否在需要隐藏菜单按钮的界面
  91. */
  92. private isInHiddenMenuUI(): boolean {
  93. // 检查SkillUpUI、UpgradeUI、ShopUI是否激活
  94. const isSkillUpActive = this.skillUpUI && this.skillUpUI.active;
  95. const isUpgradeActive = this.upgradeUI && this.upgradeUI.active;
  96. const isShopActive = this.shopUI && this.shopUI.active;
  97. return isSkillUpActive || isUpgradeActive || isShopActive;
  98. }
  99. /**
  100. * 更新菜单按钮的显示状态
  101. */
  102. private updateMenuButtonVisibility() {
  103. // 检查当前应用状态
  104. const currentAppState = this.gameManager ? this.gameManager.getCurrentAppState() : null;
  105. // 检查是否为第一关
  106. const isFirstLevel = (() => {
  107. try {
  108. const save = SaveDataManager.getInstance();
  109. return save && typeof save.getCurrentLevel === 'function' && save.getCurrentLevel() === 1;
  110. } catch {
  111. return false;
  112. }
  113. })();
  114. // 在游戏外界面中,检查是否在需要隐藏菜单按钮的界面
  115. if (currentAppState !== AppState.IN_GAME && this.isInHiddenMenuUI()) {
  116. // 在SkillUpUI、UpgradeUI、ShopUI界面中隐藏所有菜单按钮
  117. if (this.menuButton) this.menuButton.node.active = false;
  118. if (this.menuButton1) this.menuButton1.node.active = false;
  119. console.log('[MenuController] 在游戏外界面中隐藏菜单按钮');
  120. } else if (currentAppState === AppState.IN_GAME) {
  121. // 第一关隐藏游戏中的菜单按钮
  122. if (isFirstLevel) {
  123. if (this.menuButton) this.menuButton.node.active = false;
  124. if (this.menuButton1) this.menuButton1.node.active = false;
  125. console.log('[MenuController] 第一关隐藏MenueButton-001');
  126. } else {
  127. // 非第一关:游戏中显示menuButton1,隐藏menuButton
  128. if (this.menuButton) this.menuButton.node.active = false;
  129. if (this.menuButton1) this.menuButton1.node.active = true;
  130. console.log('[MenuController] 游戏中显示menuButton1');
  131. }
  132. } else {
  133. // 主界面显示menuButton,隐藏menuButton1
  134. if (this.menuButton) this.menuButton.node.active = true;
  135. if (this.menuButton1) this.menuButton1.node.active = false;
  136. console.log('[MenuController] 主界面显示menuButton');
  137. }
  138. }
  139. /**
  140. * 菜单按钮点击事件
  141. */
  142. private async onMenuButtonClick() {
  143. // 播放UI点击音效
  144. Audio.playUISound('data/弹球音效/ui play');
  145. if (this.isMenuOpen) {
  146. await this.closeMenu();
  147. } else {
  148. await this.openMenu();
  149. }
  150. }
  151. /**
  152. * 关闭按钮点击事件
  153. */
  154. private async onCloseButtonClick() {
  155. // 播放UI点击音效
  156. Audio.playUISound('data/弹球音效/ui play');
  157. await this.closeMenu();
  158. }
  159. /**
  160. * 继续游戏按钮点击事件
  161. */
  162. private async onContinueButtonClick() {
  163. // 播放UI点击音效
  164. Audio.playUISound('data/弹球音效/ui play');
  165. console.log('[MenuController] 继续游戏按钮被点击');
  166. // 检查GameManager组件引用
  167. if (!this.gameManager) {
  168. console.error('[MenuController] GameManager组件未通过装饰器挂载,请在Inspector中拖拽GameManager组件');
  169. await this.closeMenu();
  170. return;
  171. }
  172. const currentAppState = this.gameManager.getCurrentAppState();
  173. console.log(`[MenuController] 当前应用状态: ${currentAppState}`);
  174. if (currentAppState === AppState.IN_GAME) {
  175. // 在游戏中点击继续,关闭菜单并恢复游戏
  176. console.log('[MenuController] 游戏中点击继续,关闭菜单并恢复游戏');
  177. await this.closeMenu();
  178. } else {
  179. // 游戏外点击继续,直接关闭菜单
  180. console.log('[MenuController] 游戏外点击继续,直接关闭菜单');
  181. await this.closeMenu();
  182. }
  183. }
  184. /**
  185. * 退出游戏按钮点击事件
  186. * 只在游戏中有效,游戏外界面不显示菜单按钮
  187. */
  188. private async onBackButtonClick() {
  189. // 播放UI点击音效
  190. Audio.playUISound('data/弹球音效/ui play');
  191. console.log('[MenuController] 退出游戏按钮被点击');
  192. // 检查GameManager组件引用
  193. if (!this.gameManager) {
  194. console.error('[MenuController] GameManager组件未通过装饰器挂载,请在Inspector中拖拽GameManager组件');
  195. return;
  196. }
  197. const currentAppState = this.gameManager.getCurrentAppState();
  198. console.log(`[MenuController] 当前应用状态: ${currentAppState}`);
  199. // 只处理游戏中的退出逻辑,游戏外界面不显示菜单按钮
  200. if (currentAppState === AppState.IN_GAME) {
  201. // 在游戏中退出,应该触发游戏失败事件显示GameEnd面板
  202. console.log('[MenuController] 游戏中退出,触发GAME_DEFEAT事件显示游戏失败UI');
  203. // 先触发游戏失败事件,让GameEnd面板显示
  204. const eventBus = EventBus.getInstance();
  205. eventBus.emit(GameEvents.GAME_DEFEAT);
  206. // 关闭菜单(不触发GAME_RESUME事件,避免事件冲突)
  207. await this.closeMenuWithoutResume();
  208. console.log('[MenuController] 菜单退出处理完成,已触发GAME_DEFEAT事件');
  209. } else {
  210. // 游戏外不应该显示菜单按钮,如果意外触发则只关闭菜单
  211. console.log('[MenuController] 游戏外意外触发退出按钮,仅关闭菜单');
  212. await this.closeMenu();
  213. }
  214. }
  215. /**
  216. * 重置镜头位置到原始位置
  217. * 确保从游戏中退出时镜头位置正常
  218. */
  219. private resetCameraPosition() {
  220. // 如果还没有获取GameStartMove组件,尝试获取
  221. if (!this.gameStartMoveComponent) {
  222. const cameraNode = find('Canvas/Main Camera');
  223. if (cameraNode) {
  224. this.gameStartMoveComponent = cameraNode.getComponent(GameStartMove);
  225. }
  226. }
  227. // 如果成功获取到组件,重置镜头位置
  228. if (this.gameStartMoveComponent) {
  229. console.log('[MenuController] 重置镜头位置到原始位置');
  230. this.gameStartMoveComponent.resetCameraToOriginalPosition(0.3);
  231. } else {
  232. console.warn('[MenuController] 未找到GameStartMove组件,无法重置镜头位置');
  233. }
  234. }
  235. /**
  236. * 打开菜单
  237. */
  238. public async openMenu(): Promise<void> {
  239. if (this.isMenuOpen) return;
  240. this.isMenuOpen = true;
  241. // 检查是否在游戏中,如果是则通过事件系统触发游戏暂停
  242. if (this.gameManager && this.gameManager.getCurrentAppState() === AppState.IN_GAME) {
  243. console.log('[MenuController] 游戏中打开菜单,通过事件系统触发游戏暂停');
  244. const eventBus = EventBus.getInstance();
  245. eventBus.emit(GameEvents.GAME_PAUSE);
  246. }
  247. // 使用动画显示菜单面板
  248. if (this.popupAni) {
  249. await this.popupAni.showPanel();
  250. }
  251. // 注意:不再有fallback逻辑设置active,菜单面板始终保持active=true
  252. console.log('菜单已打开');
  253. }
  254. /**
  255. * 关闭菜单
  256. */
  257. public async closeMenu(): Promise<void> {
  258. if (!this.isMenuOpen) return;
  259. this.isMenuOpen = false;
  260. // 使用动画隐藏菜单面板
  261. if (this.popupAni) {
  262. await this.popupAni.hidePanel();
  263. }
  264. // 注意:不再有fallback逻辑设置active,菜单面板始终保持active=true
  265. // 检查是否在游戏中,如果是则通过事件系统触发游戏恢复
  266. if (this.gameManager && this.gameManager.getCurrentAppState() === AppState.IN_GAME) {
  267. console.log('[MenuController] 游戏中关闭菜单,通过事件系统触发游戏恢复');
  268. const eventBus = EventBus.getInstance();
  269. eventBus.emit(GameEvents.GAME_RESUME);
  270. }
  271. console.log('菜单已关闭');
  272. }
  273. /**
  274. * 关闭菜单但不触发游戏恢复事件(用于游戏失败退出)
  275. */
  276. private async closeMenuWithoutResume(): Promise<void> {
  277. if (!this.isMenuOpen) return;
  278. this.isMenuOpen = false;
  279. // 使用动画隐藏菜单面板
  280. if (this.popupAni) {
  281. await this.popupAni.hidePanel();
  282. }
  283. // 注意:不再有fallback逻辑设置active,菜单面板始终保持active=true
  284. // 不触发GAME_RESUME事件,避免与GAME_DEFEAT事件冲突
  285. console.log('菜单已关闭(未触发游戏恢复)');
  286. }
  287. /**
  288. * 切换菜单状态
  289. */
  290. public async toggleMenu(): Promise<void> {
  291. if (this.isMenuOpen) {
  292. await this.closeMenu();
  293. } else {
  294. await this.openMenu();
  295. }
  296. }
  297. /**
  298. * 获取菜单状态
  299. */
  300. public getMenuState(): boolean {
  301. return this.isMenuOpen;
  302. }
  303. /**
  304. * 立即关闭菜单(无动画)
  305. */
  306. public closeMenuImmediate(): void {
  307. this.isMenuOpen = false;
  308. if (this.popupAni) {
  309. this.popupAni.hidePanelImmediate();
  310. }
  311. // 注意:不再有fallback逻辑设置active,菜单面板始终保持active=true
  312. console.log('菜单已立即关闭');
  313. }
  314. /**
  315. * 立即打开菜单(无动画)
  316. */
  317. public openMenuImmediate(): void {
  318. this.isMenuOpen = true;
  319. if (this.popupAni) {
  320. this.popupAni.showPanelImmediate();
  321. }
  322. // 注意:不再有fallback逻辑设置active,菜单面板始终保持active=true
  323. console.log('菜单已立即打开');
  324. }
  325. /**
  326. * 应用状态切换事件处理
  327. */
  328. private onAppStateChanged(): void {
  329. console.log('[MenuController] 应用状态切换,更新菜单按钮显示状态');
  330. this.updateMenuButtonVisibility();
  331. }
  332. /**
  333. * 公共方法:更新菜单按钮显示状态
  334. * 供外部组件调用,例如在界面切换时
  335. */
  336. public updateMenuButtonState(): void {
  337. this.updateMenuButtonVisibility();
  338. }
  339. onDestroy() {
  340. // 解绑按钮事件
  341. if (this.menuButton) {
  342. this.menuButton.node.off(Button.EventType.CLICK, this.onMenuButtonClick, this);
  343. }
  344. if (this.menuButton1) {
  345. this.menuButton1.node.off(Button.EventType.CLICK, this.onMenuButtonClick, this);
  346. }
  347. if (this.closeButton) {
  348. this.closeButton.node.off(Button.EventType.CLICK, this.onCloseButtonClick, this);
  349. }
  350. if (this.backButton) {
  351. this.backButton.node.off(Button.EventType.CLICK, this.onBackButtonClick, this);
  352. }
  353. if (this.continueButton) {
  354. this.continueButton.node.off(Button.EventType.CLICK, this.onContinueButtonClick, this);
  355. }
  356. // 解绑事件监听器
  357. const eventBus = EventBus.getInstance();
  358. eventBus.off(GameEvents.RETURN_TO_MAIN_MENU, this.updateMenuButtonVisibility, this);
  359. eventBus.off(GameEvents.UI_PANEL_SWITCHED, this.updateMenuButtonVisibility, this);
  360. eventBus.off(GameEvents.APP_STATE_CHANGED, this.onAppStateChanged, this);
  361. }
  362. }