258 lines
12 KiB
C#
258 lines
12 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|