using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using BaseGames.Core; using BaseGames.Core.Assets; using BaseGames.Core.Events; using BaseGames.UI; using BaseGames.UI.Menus; namespace BaseGames.UI.MainMenu { /// /// 数据驱动主菜单控制器(表驱动按钮列表 + 子面板编排)。 /// /// 按钮列表据 生成(标签/图标/顺序/动作纯数据,策划改表零代码)。 /// 子面板(存档槽 / 设置 / 制作人员)经统一 压栈:栈式回退、ESC 逐层关闭、 /// 焦点恢复均由导航器负责,本控制器不再自管取消键与面板显隐。主按钮组作为"栈底上下文" /// (不入栈),据 在有子面板打开时屏蔽自身交互。 /// public class DataDrivenMainMenuController : MonoBehaviour { [Header("数据表 / 按钮列表")] [SerializeField] private MainMenuConfigSO _config; [Tooltip("按钮的父节点(通常挂 VerticalLayoutGroup,即主按钮组)。")] [SerializeField] private Transform _container; [SerializeField] private MainMenuButtonView _buttonPrefab; [Header("主按钮组(入场动画 / 栈底屏蔽)")] [SerializeField] private CanvasGroup _mainButtonsGroup; [SerializeField] private RectTransform _mainButtonsRect; [Header("子面板(导航器压栈对象,整面板根须挂 UIPanelBase)")] [SerializeField] private SaveSlotController _saveSlotPanel; [Tooltip("设置面板根(UISimplePanel;内含数据驱动设置实例)。")] [SerializeField] private UIPanelBase _settingsPanel; [Tooltip("制作人员面板根(UISimplePanel)。")] [SerializeField] private UIPanelBase _creditsPanel; [Header("子面板关闭按钮(可选,等价于 ESC:出栈一层)")] [SerializeField] private UnityEngine.UI.Button _btnCloseSaveSlot; [SerializeField] private UnityEngine.UI.Button _btnCloseSettings; [SerializeField] private UnityEngine.UI.Button _btnCloseCredits; [Header("入场动画")] [SerializeField] private float _entrySlideOffset = 80f; [SerializeField] private float _entryDuration = 0.55f; [Header("场景")] [SerializeField] private string _firstGameSceneKey = AddressKeys.SceneGameChapter1; [Header("Event Channels - Listen")] [SerializeField] private GameStateEventChannelSO _onGameStateChanged; [SerializeField] private IntEventChannelSO _onSlotConfirmed; [Header("Event Channels - Raise")] [SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest; private readonly CompositeDisposable _subs = new(); private readonly List<(MainMenuConfigSO.Item item, MainMenuButtonView view)> _buttons = new(); private Vector2 _buttonsPanelOriginalPos; private MainMenuButtonView _firstButton; private IUINavigator _nav; // ── 生命周期 ────────────────────────────────────────────────────────── private void Awake() { if (_mainButtonsRect != null) _buttonsPanelOriginalPos = _mainButtonsRect.anchoredPosition; _btnCloseSaveSlot?.onClick.AddListener(() => Nav?.Pop()); _btnCloseSettings?.onClick.AddListener(() => Nav?.Pop()); _btnCloseCredits? .onClick.AddListener(() => Nav?.Pop()); SetButtonsGroupVisible(false); BuildMenu(); } private void OnEnable() { _onGameStateChanged?.Subscribe(HandleGameStateChanged).AddTo(_subs); _onSlotConfirmed? .Subscribe(HandleSlotConfirmed).AddTo(_subs); _nav = ServiceLocator.GetOrDefault(); if (_nav != null) _nav.StackChanged += HandleStackChanged; } private void OnDisable() { _subs.Clear(); if (_nav != null) _nav.StackChanged -= HandleStackChanged; } private void Start() => StartCoroutine(PlayEntryAnimation()); // ── 据表建菜单 ──────────────────────────────────────────────────────── /// 据配置重建主菜单按钮列表(public 以便编辑器/测试验证)。 public void BuildMenu() { ClearMenu(); if (_config == null || _container == null || _buttonPrefab == null) return; foreach (var item in _config.Items) { var view = Instantiate(_buttonPrefab, _container); view.gameObject.SetActive(true); var captured = item; view.Bind(item.labelKey, item.icon, () => Dispatch(captured)); _buttons.Add((item, view)); if (_firstButton == null) _firstButton = view; } RefreshConditional(); } /// 清空菜单按钮(容器全部子节点)。编辑器预览与运行时重建共用。 public void ClearMenu() { _buttons.Clear(); _firstButton = null; if (_container == null) return; for (int i = _container.childCount - 1; i >= 0; i--) { var child = _container.GetChild(i).gameObject; if (Application.isPlaying) Destroy(child); else DestroyImmediate(child); } } /// 根据存档存在性刷新 requiresSave 按钮的可用性(如"继续")。 public void RefreshConditional() { bool hasSave = HasAnySave(); foreach (var (item, view) in _buttons) if (item.requiresSave && view != null) view.SetInteractable(hasSave); } // ── 动作派发 ────────────────────────────────────────────────────────── private void Dispatch(MainMenuConfigSO.Item item) { switch (item.action) { case MainMenuAction.NewGame: if (_saveSlotPanel != null) { _saveSlotPanel.SetMode(SaveSlotPanelMode.NewGame); Nav?.Push(_saveSlotPanel); } break; case MainMenuAction.Continue: if (_saveSlotPanel != null) { _saveSlotPanel.SetMode(SaveSlotPanelMode.Continue); Nav?.Push(_saveSlotPanel); } break; case MainMenuAction.OpenSettings: if (_settingsPanel != null) Nav?.Push(_settingsPanel); break; case MainMenuAction.OpenCredits: if (_creditsPanel != null) Nav?.Push(_creditsPanel); break; case MainMenuAction.LoadScene: _onSceneLoadRequest?.Raise(new SceneLoadRequest { SceneName = string.IsNullOrEmpty(item.sceneKey) ? _firstGameSceneKey : item.sceneKey, TransitionType = TransitionType.Scene, }); break; case MainMenuAction.Quit: Application.Quit(); break; case MainMenuAction.RaiseEvent: item.eventChannel?.Raise(); break; } } // ── 栈底按钮组屏蔽(有子面板打开时禁用主按钮,避免方向键穿透)────────── private IUINavigator Nav => _nav ??= ServiceLocator.GetOrDefault(); private void HandleStackChanged() { bool anyOpen = Nav != null && Nav.Depth > 0; if (_mainButtonsGroup != null) { _mainButtonsGroup.interactable = !anyOpen; _mainButtonsGroup.blocksRaycasts = !anyOpen; } if (!anyOpen && _firstButton != null) EventSystem.current?.SetSelectedGameObject(_firstButton.Button.gameObject); } // ── 存档槽确认 ──────────────────────────────────────────────────────── private void HandleSlotConfirmed(int _) { Nav?.PopToRoot(); // 关闭存档槽(及其上任何子对话框),准备进场景 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, }); } private void HandleGameStateChanged(GameStateId state) { bool isMainMenu = state == GameStates.MainMenu; if (_mainButtonsGroup != null && Nav != null && Nav.Depth == 0) { _mainButtonsGroup.interactable = isMainMenu; _mainButtonsGroup.blocksRaycasts = isMainMenu; } } // ── 入场动画 ────────────────────────────────────────────────────────── 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; if (_firstButton != null) EventSystem.current?.SetSelectedGameObject(_firstButton.Button.gameObject); } // ── 工具 ────────────────────────────────────────────────────────────── private static bool HasAnySave() { var s = ServiceLocator.GetOrDefault(); return s != null && (s.HasSave(0) || s.HasSave(1) || s.HasSave(2)); } private void SetButtonsGroupVisible(bool visible) { if (_mainButtonsGroup == null) return; _mainButtonsGroup.alpha = visible ? 1f : 0f; _mainButtonsGroup.interactable = visible; _mainButtonsGroup.blocksRaycasts = visible; } } }