using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using BaseGames.Core; using BaseGames.Core.Assets; using BaseGames.Core.Events; namespace BaseGames.UI.MainMenu { /// /// 数据驱动主菜单控制器( 的表驱动版,非破坏性并存)。 /// /// 按钮列表据 生成(标签/图标/顺序/动作纯数据,策划改表零代码); /// 子面板开关、存档槽流程、入场动画、状态锁定等编排仍在本控制器(场景耦合,不下放配置表)。 /// 动作派发:内置 NewGame/Continue/OpenSettings/OpenCredits/LoadScene/Quit + 事件频道 RaiseEvent。 /// 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("子面板")] [SerializeField] private GameObject _saveSlotPanel; [SerializeField] private BaseGames.UI.Menus.SaveSlotController _saveSlotController; [SerializeField] private GameObject _settingsPanel; [SerializeField] private GameObject _creditsPanel; [Header("子面板关闭按钮(可选)")] [SerializeField] private Button _btnCloseSaveSlot; [SerializeField] private Button _btnCloseSettings; [SerializeField] private 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 void Awake() { if (_buttonPrefab != null) _buttonPrefab.gameObject.SetActive(false); _btnCloseSaveSlot?.onClick.AddListener(() => CloseSubPanel(_saveSlotPanel)); _btnCloseSettings?.onClick.AddListener(() => CloseSubPanel(_settingsPanel)); _btnCloseCredits? .onClick.AddListener(() => CloseSubPanel(_creditsPanel)); if (_mainButtonsRect != null) _buttonsPanelOriginalPos = _mainButtonsRect.anchoredPosition; SetPanel(_saveSlotPanel, false); SetPanel(_settingsPanel, false); SetPanel(_creditsPanel, false); SetButtonsGroupVisible(false); BuildMenu(); } private void OnEnable() { _onGameStateChanged?.Subscribe(HandleGameStateChanged).AddTo(_subs); _onSlotConfirmed? .Subscribe(HandleSlotConfirmed).AddTo(_subs); } private void OnDisable() => _subs.Clear(); private void Start() => StartCoroutine(PlayEntryAnimation()); // ── 据表建菜单 ──────────────────────────────────────────────────────── /// 据配置重建主菜单按钮列表(public 以便编辑器/测试验证)。 public void BuildMenu() { foreach (var (_, view) in _buttons) if (view != null) Destroy(view.gameObject); _buttons.Clear(); _firstButton = null; 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(); } /// 根据存档存在性刷新 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: _saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.NewGame); OpenSubPanel(_saveSlotPanel); break; case MainMenuAction.Continue: _saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.Continue); OpenSubPanel(_saveSlotPanel); break; case MainMenuAction.OpenSettings: OpenSubPanel(_settingsPanel); break; case MainMenuAction.OpenCredits: OpenSubPanel(_creditsPanel); if (_btnCloseCredits != null) EventSystem.current?.SetSelectedGameObject(_btnCloseCredits.gameObject); break; case MainMenuAction.LoadScene: _onSceneLoadRequest?.Raise(new SceneLoadRequest { SceneName = string.IsNullOrEmpty(item.sceneKey) ? _firstGameSceneKey : item.sceneKey, TransitionType = TransitionType.Scene, ShowLoadingScreen = true, }); break; case MainMenuAction.Quit: Application.Quit(); break; case MainMenuAction.RaiseEvent: item.eventChannel?.Raise(); break; } } // ── 子面板编排 ──────────────────────────────────────────────────────── private void OpenSubPanel(GameObject panel) { SetMainButtonsInteractable(false); SetPanel(panel, true); } private void CloseSubPanel(GameObject panel) { SetPanel(panel, false); SetMainButtonsInteractable(true); if (_firstButton != null) EventSystem.current?.SetSelectedGameObject(_firstButton.Button.gameObject); } private void SetMainButtonsInteractable(bool on) { if (_mainButtonsGroup == null) return; _mainButtonsGroup.interactable = on; _mainButtonsGroup.blocksRaycasts = on; } // ── 存档槽确认(与 MainMenuController 一致)──────────────────────────── 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; if (_mainButtonsGroup != null) { _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 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; } } }