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

260 lines
11 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.UI;
using UnityEngine.EventSystems;
using BaseGames.Core;
using BaseGames.Core.Assets;
using BaseGames.Core.Events;
namespace BaseGames.UI.MainMenu
{
/// <summary>
/// 数据驱动主菜单控制器(<see cref="MainMenuController"/> 的表驱动版,非破坏性并存)。
///
/// 按钮列表据 <see cref="MainMenuConfigSO"/> 生成(标签/图标/顺序/动作纯数据,策划改表零代码);
/// 子面板开关、存档槽流程、入场动画、状态锁定等编排仍在本控制器(场景耦合,不下放配置表)。
/// 动作派发:内置 NewGame/Continue/OpenSettings/OpenCredits/LoadScene/Quit + 事件频道 RaiseEvent。
/// </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("子面板")]
[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());
// ── 据表建菜单 ────────────────────────────────────────────────────────
/// <summary>据配置重建主菜单按钮列表public 以便编辑器/测试验证)。</summary>
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();
}
/// <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:
_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<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,
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<ISaveService>();
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;
}
}
}