Files
zeling_v2/Assets/_Game/Scripts/UI/MainMenu/DataDrivenMainMenuController.cs
2026-06-07 11:49:55 +08:00

258 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
{
/// <summary>
/// 数据驱动主菜单控制器(表驱动按钮列表 + 子面板编排)。
///
/// 按钮列表据 <see cref="MainMenuConfigSO"/> 生成(标签/图标/顺序/动作纯数据,策划改表零代码)。
/// 子面板(存档槽 / 设置 / 制作人员)经统一 <see cref="IUINavigator"/> 压栈栈式回退、ESC 逐层关闭、
/// 焦点恢复均由导航器负责,本控制器不再自管取消键与面板显隐。主按钮组作为"栈底上下文"
/// (不入栈),据 <see cref="IUINavigator.Depth"/> 在有子面板打开时屏蔽自身交互。
/// </summary>
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<IUINavigator>();
if (_nav != null) _nav.StackChanged += HandleStackChanged;
}
private void OnDisable()
{
_subs.Clear();
if (_nav != null) _nav.StackChanged -= HandleStackChanged;
}
private void Start() => StartCoroutine(PlayEntryAnimation());
// ── 据表建菜单 ────────────────────────────────────────────────────────
/// <summary>据配置重建主菜单按钮列表public 以便编辑器/测试验证)。</summary>
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();
}
/// <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()
{
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<IUINavigator>();
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<ISaveService>();
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<ISaveService>();
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;
}
}
}