This commit is contained in:
2026-06-07 11:49:55 +08:00
parent ff0f3bde54
commit 1897658a00
98 changed files with 9903 additions and 13907 deletions

View File

@@ -1,20 +1,22 @@
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;
using BaseGames.UI;
using BaseGames.UI.Menus;
namespace BaseGames.UI.MainMenu
{
/// <summary>
/// 数据驱动主菜单控制器(<see cref="MainMenuController"/> 的表驱动版,非破坏性并存)。
/// 数据驱动主菜单控制器(表驱动按钮列表 + 子面板编排)。
///
/// 按钮列表据 <see cref="MainMenuConfigSO"/> 生成(标签/图标/顺序/动作纯数据,策划改表零代码)
/// 子面板开关、存档槽流程、入场动画、状态锁定等编排仍在本控制器(场景耦合,不下放配置表)。
/// 动作派发:内置 NewGame/Continue/OpenSettings/OpenCredits/LoadScene/Quit + 事件频道 RaiseEvent。
/// 按钮列表据 <see cref="MainMenuConfigSO"/> 生成(标签/图标/顺序/动作纯数据,策划改表零代码)
/// 子面板(存档槽 / 设置 / 制作人员)经统一 <see cref="IUINavigator"/> 压栈栈式回退、ESC 逐层关闭、
/// 焦点恢复均由导航器负责,本控制器不再自管取消键与面板显隐。主按钮组作为"栈底上下文"
/// (不入栈),据 <see cref="IUINavigator.Depth"/> 在有子面板打开时屏蔽自身交互。
/// </summary>
public class DataDrivenMainMenuController : MonoBehaviour
{
@@ -24,20 +26,21 @@ namespace BaseGames.UI.MainMenu
[SerializeField] private Transform _container;
[SerializeField] private MainMenuButtonView _buttonPrefab;
[Header("主按钮组(入场动画)")]
[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("子面板(导航器压栈对象,整面板根须挂 UIPanelBase")]
[SerializeField] private SaveSlotController _saveSlotPanel;
[Tooltip("设置面板根UISimplePanel内含数据驱动设置实例。")]
[SerializeField] private UIPanelBase _settingsPanel;
[Tooltip("制作人员面板根UISimplePanel。")]
[SerializeField] private UIPanelBase _creditsPanel;
[Header("子面板关闭按钮(可选)")]
[SerializeField] private Button _btnCloseSaveSlot;
[SerializeField] private Button _btnCloseSettings;
[SerializeField] private Button _btnCloseCredits;
[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;
@@ -57,24 +60,19 @@ namespace BaseGames.UI.MainMenu
private readonly List<(MainMenuConfigSO.Item item, MainMenuButtonView view)> _buttons = new();
private Vector2 _buttonsPanelOriginalPos;
private MainMenuButtonView _firstButton;
private IUINavigator _nav;
// ── 生命周期 ──────────────────────────────────────────────────────────
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);
_btnCloseSaveSlot?.onClick.AddListener(() => Nav?.Pop());
_btnCloseSettings?.onClick.AddListener(() => Nav?.Pop());
_btnCloseCredits? .onClick.AddListener(() => Nav?.Pop());
SetButtonsGroupVisible(false);
BuildMenu();
}
@@ -82,9 +80,16 @@ namespace BaseGames.UI.MainMenu
{
_onGameStateChanged?.Subscribe(HandleGameStateChanged).AddTo(_subs);
_onSlotConfirmed? .Subscribe(HandleSlotConfirmed).AddTo(_subs);
_nav = ServiceLocator.GetOrDefault<IUINavigator>();
if (_nav != null) _nav.StackChanged += HandleStackChanged;
}
private void OnDisable() => _subs.Clear();
private void OnDisable()
{
_subs.Clear();
if (_nav != null) _nav.StackChanged -= HandleStackChanged;
}
private void Start() => StartCoroutine(PlayEntryAnimation());
@@ -92,10 +97,7 @@ namespace BaseGames.UI.MainMenu
/// <summary>据配置重建主菜单按钮列表public 以便编辑器/测试验证)。</summary>
public void BuildMenu()
{
foreach (var (_, view) in _buttons) if (view != null) Destroy(view.gameObject);
_buttons.Clear();
_firstButton = null;
ClearMenu();
if (_config == null || _container == null || _buttonPrefab == null) return;
foreach (var item in _config.Items)
@@ -111,6 +113,20 @@ namespace BaseGames.UI.MainMenu
RefreshConditional();
}
/// <summary>清空菜单按钮(容器全部子节点)。编辑器预览与运行时重建共用。</summary>
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);
}
}
/// <summary>根据存档存在性刷新 requiresSave 按钮的可用性(如"继续")。</summary>
public void RefreshConditional()
{
@@ -126,27 +142,22 @@ namespace BaseGames.UI.MainMenu
switch (item.action)
{
case MainMenuAction.NewGame:
_saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.NewGame);
OpenSubPanel(_saveSlotPanel);
if (_saveSlotPanel != null) { _saveSlotPanel.SetMode(SaveSlotPanelMode.NewGame); Nav?.Push(_saveSlotPanel); }
break;
case MainMenuAction.Continue:
_saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.Continue);
OpenSubPanel(_saveSlotPanel);
if (_saveSlotPanel != null) { _saveSlotPanel.SetMode(SaveSlotPanelMode.Continue); Nav?.Push(_saveSlotPanel); }
break;
case MainMenuAction.OpenSettings:
OpenSubPanel(_settingsPanel);
if (_settingsPanel != null) Nav?.Push(_settingsPanel);
break;
case MainMenuAction.OpenCredits:
OpenSubPanel(_creditsPanel);
if (_btnCloseCredits != null)
EventSystem.current?.SetSelectedGameObject(_btnCloseCredits.gameObject);
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,
ShowLoadingScreen = true,
});
break;
case MainMenuAction.Quit:
@@ -158,32 +169,25 @@ namespace BaseGames.UI.MainMenu
}
}
// ── 子面板编排 ────────────────────────────────────────────────────────
private void OpenSubPanel(GameObject panel)
{
SetMainButtonsInteractable(false);
SetPanel(panel, true);
}
// ── 栈底按钮组屏蔽(有子面板打开时禁用主按钮,避免方向键穿透)──────────
private IUINavigator Nav => _nav ??= ServiceLocator.GetOrDefault<IUINavigator>();
private void CloseSubPanel(GameObject panel)
private void HandleStackChanged()
{
SetPanel(panel, false);
SetMainButtonsInteractable(true);
if (_firstButton != null)
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 SetMainButtonsInteractable(bool on)
{
if (_mainButtonsGroup == null) return;
_mainButtonsGroup.interactable = on;
_mainButtonsGroup.blocksRaycasts = on;
}
// ── 存档槽确认(与 MainMenuController 一致)────────────────────────────
// ── 存档槽确认 ────────────────────────────────────────────────────────
private void HandleSlotConfirmed(int _)
{
SetPanel(_saveSlotPanel, false);
Nav?.PopToRoot(); // 关闭存档槽(及其上任何子对话框),准备进场景
var svc = ServiceLocator.GetOrDefault<ISaveService>();
string checkpointScene = svc?.LastCheckpointScene;
@@ -194,14 +198,13 @@ namespace BaseGames.UI.MainMenu
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)
if (_mainButtonsGroup != null && Nav != null && Nav.Depth == 0)
{
_mainButtonsGroup.interactable = isMainMenu;
_mainButtonsGroup.blocksRaycasts = isMainMenu;
@@ -243,11 +246,6 @@ namespace BaseGames.UI.MainMenu
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;