UI系统
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user