using System.Collections; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using BaseGames.Core; using BaseGames.Core.Assets; using BaseGames.Core.Events; namespace BaseGames.UI.MainMenu { /// /// 主菜单 UI 控制器(挂载在 Scene_MainMenu 的根 Canvas 上)。 /// /// 面板结构(按 Inspector 绑定): /// ├── MainButtonsPanel — 主按钮组(新游戏 / 继续 / 设置 / 制作团队 / 退出) /// ├── SaveSlotPanel — 存档槽选择(新游戏 & 继续共用) /// ├── SettingsPanel — 设置面板 /// └── CreditsPanel — 制作团队面板 /// /// 入场动画:主按钮组从下方滑入(代码驱动,无需 Animator)。 /// /// 流程: /// 玩家选择存档槽(SaveSlotController 发布 _onSlotConfirmed) /// → 关闭存档槽面板 → 发布 SceneLoadRequest(目标游戏场景) /// → GameManager 响应,进入 LoadingScene 状态,显示加载画面,最终切换到 Gameplay。 /// public class MainMenuController : MonoBehaviour { // ── 面板引用 ────────────────────────────────────────────────────────── [Header("面板")] [SerializeField] private CanvasGroup _mainButtonsGroup; [SerializeField] private RectTransform _mainButtonsRect; // 用于滑入动画 [SerializeField] private GameObject _saveSlotPanel; [Tooltip("存档槽面板控制器。打开前调用 SetMode 区分新游戏 / 继续语境。")] [SerializeField] private BaseGames.UI.Menus.SaveSlotController _saveSlotController; [SerializeField] private GameObject _settingsPanel; [SerializeField] private GameObject _creditsPanel; // ── 按钮引用 ────────────────────────────────────────────────────────── [Header("主菜单按钮")] [SerializeField] private Button _btnNewGame; [SerializeField] private Button _btnContinue; [SerializeField] private Button _btnSettings; [SerializeField] private Button _btnCredits; [SerializeField] private Button _btnQuit; // ── 按钮(子面板关闭)──────────────────────────────────────────────── [Header("子面板关闭按钮(可选)")] [SerializeField] private Button _btnCloseSaveSlot; [SerializeField] private Button _btnCloseSettings; [SerializeField] private Button _btnCloseCredits; // ── 入场动画参数 ────────────────────────────────────────────────────── [Header("入场动画")] [Tooltip("按钮组初始偏移(像素,向下)")] [SerializeField] private float _entrySlideOffset = 80f; [Tooltip("入场动画持续时间(秒)")] [SerializeField] private float _entryDuration = 0.55f; // ── 游戏场景 ────────────────────────────────────────────────────────── [Header("场景")] [Tooltip("新游戏 / 继续后进入的第一个游戏场景(Addressable Key)")] [SerializeField] private string _firstGameSceneKey = AddressKeys.SceneGameChapter1; // ── Event Channels ──────────────────────────────────────────────────── [Header("Event Channels - Listen")] [SerializeField] private GameStateEventChannelSO _onGameStateChanged; [Tooltip("SaveSlotController 完成选槽后发布(携带槽索引)")] [SerializeField] private IntEventChannelSO _onSlotConfirmed; [Header("Event Channels - Raise")] [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; // ── 内部状态 ────────────────────────────────────────────────────────── private readonly CompositeDisposable _subs = new(); private Vector2 _buttonsPanelOriginalPos; // ── 生命周期 ────────────────────────────────────────────────────────── private void Awake() { // 按钮绑定 _btnNewGame? .onClick.AddListener(OnNewGameClicked); _btnContinue?.onClick.AddListener(OnContinueClicked); _btnSettings?.onClick.AddListener(OnSettingsClicked); _btnCredits? .onClick.AddListener(OnCreditsClicked); _btnQuit? .onClick.AddListener(Application.Quit); _btnCloseSaveSlot?.onClick.AddListener(() => CloseSubPanel(_saveSlotPanel, _btnNewGame)); _btnCloseSettings?.onClick.AddListener(() => CloseSubPanel(_settingsPanel, _btnSettings)); _btnCloseCredits? .onClick.AddListener(() => CloseSubPanel(_creditsPanel, _btnCredits)); // 记录按钮组原始位置(供动画使用) if (_mainButtonsRect != null) _buttonsPanelOriginalPos = _mainButtonsRect.anchoredPosition; // 初始状态:隐藏子面板,主按钮组不可见(等待入场动画) SetPanel(_saveSlotPanel, false); SetPanel(_settingsPanel, false); SetPanel(_creditsPanel, false); SetButtonsGroupVisible(false); // 刷新"继续"按钮可用性(需要至少一个有效存档) RefreshContinueButton(); } private void OnEnable() { _onGameStateChanged?.Subscribe(HandleGameStateChanged).AddTo(_subs); _onSlotConfirmed? .Subscribe(HandleSlotConfirmed).AddTo(_subs); } private void OnDisable() => _subs.Clear(); private void Start() { // 场景初始化完成后播放入场动画 StartCoroutine(PlayEntryAnimation()); } // ── 入场动画 ───────────────────────────────────────────────────────── private IEnumerator PlayEntryAnimation() { if (_mainButtonsGroup == null) yield break; Vector2 startPos = _buttonsPanelOriginalPos - new Vector2(0f, _entrySlideOffset); if (_mainButtonsRect != null) _mainButtonsRect.anchoredPosition = startPos; float elapsed = 0f; while (elapsed < _entryDuration) { float t = Mathf.SmoothStep(0f, 1f, elapsed / _entryDuration); _mainButtonsGroup.alpha = t; if (_mainButtonsRect != null) _mainButtonsRect.anchoredPosition = Vector2.Lerp(startPos, _buttonsPanelOriginalPos, t); elapsed += Time.unscaledDeltaTime; yield return null; } _mainButtonsGroup.alpha = 1f; if (_mainButtonsRect != null) _mainButtonsRect.anchoredPosition = _buttonsPanelOriginalPos; _mainButtonsGroup.interactable = true; _mainButtonsGroup.blocksRaycasts = true; // 手柄导航:入场动画完成后将焦点置于第一个按钮 EventSystem.current?.SetSelectedGameObject(_btnNewGame?.gameObject); } // ── 按钮回调 ───────────────────────────────────────────────────────── private void OnNewGameClicked() { _saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.NewGame); OpenSubPanel(_saveSlotPanel); } private void OnContinueClicked() { _saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.Continue); OpenSubPanel(_saveSlotPanel); } private void OnSettingsClicked() => OpenSubPanel(_settingsPanel); // SettingsPanelController 自行设焦点 private void OnCreditsClicked() { OpenSubPanel(_creditsPanel); // Credits 面板无独立控制器,打开时把焦点交给返回按钮(键盘 / 手柄可直接退出) if (_btnCloseCredits != null) EventSystem.current?.SetSelectedGameObject(_btnCloseCredits.gameObject); } /// 打开子面板:禁用主按钮组交互,避免键盘/手柄导航"穿透"到背后的主菜单按钮。 private void OpenSubPanel(GameObject panel) { SetMainButtonsInteractable(false); SetPanel(panel, true); } /// 关闭子面板:恢复主按钮组交互,并把焦点恢复到对应主菜单按钮(导航连续性)。 private void CloseSubPanel(GameObject panel, Button focusAfter) { SetPanel(panel, false); SetMainButtonsInteractable(true); if (focusAfter != null) EventSystem.current?.SetSelectedGameObject(focusAfter.gameObject); } private void SetMainButtonsInteractable(bool on) { if (_mainButtonsGroup == null) return; _mainButtonsGroup.interactable = on; _mainButtonsGroup.blocksRaycasts = on; } // ── 存档槽确认 ─────────────────────────────────────────────────────── private void HandleSlotConfirmed(int _) { SetPanel(_saveSlotPanel, false); // 继续游戏:存档已记录检查点场景时加载该场景并落在存档点出生位; // 否则(新游戏 / 存档尚无检查点)加载首关。 var svc = ServiceLocator.GetOrDefault(); string checkpointScene = svc?.LastCheckpointScene; bool hasCheckpoint = !string.IsNullOrEmpty(checkpointScene); _onSceneLoadRequest?.Raise(new SceneLoadRequest { SceneName = hasCheckpoint ? checkpointScene : _firstGameSceneKey, EntryTransitionId = hasCheckpoint ? svc.LastCheckpointSpawnId : null, TransitionType = TransitionType.Scene, ShowLoadingScreen = true, }); } // ── 游戏状态响应 ───────────────────────────────────────────────────── private void HandleGameStateChanged(GameStateId state) { bool isMainMenu = state == GameStates.MainMenu; // 离开 MainMenu(加载游戏中)时锁定所有交互,防止重复点击 if (_mainButtonsGroup != null) { _mainButtonsGroup.interactable = isMainMenu; _mainButtonsGroup.blocksRaycasts = isMainMenu; } } // ── 工具方法 ───────────────────────────────────────────────────────── private void RefreshContinueButton() { if (_btnContinue == null) return; var saveService = ServiceLocator.GetOrDefault(); bool hasAny = saveService != null && (saveService.HasSave(0) || saveService.HasSave(1) || saveService.HasSave(2)); _btnContinue.interactable = hasAny; } private static void SetPanel(GameObject panel, bool active) { if (panel != null) panel.SetActive(active); } private void SetButtonsGroupVisible(bool visible) { if (_mainButtonsGroup == null) return; _mainButtonsGroup.alpha = visible ? 1f : 0f; _mainButtonsGroup.interactable = visible; _mainButtonsGroup.blocksRaycasts = visible; } } }