264 lines
12 KiB
C#
264 lines
12 KiB
C#
using System.Collections;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.EventSystems;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Assets;
|
||
using BaseGames.Core.Events;
|
||
|
||
namespace BaseGames.UI.MainMenu
|
||
{
|
||
/// <summary>
|
||
/// 主菜单 UI 控制器(挂载在 Scene_MainMenu 的根 Canvas 上)。
|
||
///
|
||
/// 面板结构(按 Inspector 绑定):
|
||
/// ├── MainButtonsPanel — 主按钮组(新游戏 / 继续 / 设置 / 制作团队 / 退出)
|
||
/// ├── SaveSlotPanel — 存档槽选择(新游戏 & 继续共用)
|
||
/// ├── SettingsPanel — 设置面板
|
||
/// └── CreditsPanel — 制作团队面板
|
||
///
|
||
/// 入场动画:主按钮组从下方滑入(代码驱动,无需 Animator)。
|
||
///
|
||
/// 流程:
|
||
/// 玩家选择存档槽(SaveSlotController 发布 _onSlotConfirmed)
|
||
/// → 关闭存档槽面板 → 发布 SceneLoadRequest(目标游戏场景)
|
||
/// → GameManager 响应,进入 LoadingScene 状态,显示加载画面,最终切换到 Gameplay。
|
||
/// </summary>
|
||
public class MainMenuController : MonoBehaviour
|
||
{
|
||
// ── 面板引用 ──────────────────────────────────────────────────────────
|
||
|
||
[Header("面板")]
|
||
[SerializeField] private CanvasGroup _mainButtonsGroup;
|
||
[SerializeField] private RectTransform _mainButtonsRect; // 用于滑入动画
|
||
[SerializeField] private GameObject _saveSlotPanel;
|
||
[Tooltip("存档槽面板控制器。打开前调用 SetMode 区分新游戏 / 继续语境。")]
|
||
[SerializeField] private BaseGames.UI.Menus.SaveSlotController _saveSlotController;
|
||
[SerializeField] private GameObject _settingsPanel;
|
||
[SerializeField] private GameObject _creditsPanel;
|
||
|
||
// ── 按钮引用 ──────────────────────────────────────────────────────────
|
||
|
||
[Header("主菜单按钮")]
|
||
[SerializeField] private Button _btnNewGame;
|
||
[SerializeField] private Button _btnContinue;
|
||
[SerializeField] private Button _btnSettings;
|
||
[SerializeField] private Button _btnCredits;
|
||
[SerializeField] private Button _btnQuit;
|
||
|
||
// ── 按钮(子面板关闭)────────────────────────────────────────────────
|
||
|
||
[Header("子面板关闭按钮(可选)")]
|
||
[SerializeField] private Button _btnCloseSaveSlot;
|
||
[SerializeField] private Button _btnCloseSettings;
|
||
[SerializeField] private Button _btnCloseCredits;
|
||
|
||
// ── 入场动画参数 ──────────────────────────────────────────────────────
|
||
|
||
[Header("入场动画")]
|
||
[Tooltip("按钮组初始偏移(像素,向下)")]
|
||
[SerializeField] private float _entrySlideOffset = 80f;
|
||
[Tooltip("入场动画持续时间(秒)")]
|
||
[SerializeField] private float _entryDuration = 0.55f;
|
||
|
||
// ── 游戏场景 ──────────────────────────────────────────────────────────
|
||
|
||
[Header("场景")]
|
||
[Tooltip("新游戏 / 继续后进入的第一个游戏场景(Addressable Key)")]
|
||
[SerializeField] private string _firstGameSceneKey = AddressKeys.SceneGameChapter1;
|
||
|
||
// ── Event Channels ────────────────────────────────────────────────────
|
||
|
||
[Header("Event Channels - Listen")]
|
||
[SerializeField] private GameStateEventChannelSO _onGameStateChanged;
|
||
[Tooltip("SaveSlotController 完成选槽后发布(携带槽索引)")]
|
||
[SerializeField] private IntEventChannelSO _onSlotConfirmed;
|
||
|
||
[Header("Event Channels - Raise")]
|
||
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
|
||
|
||
// ── 内部状态 ──────────────────────────────────────────────────────────
|
||
|
||
private readonly CompositeDisposable _subs = new();
|
||
private Vector2 _buttonsPanelOriginalPos;
|
||
|
||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||
|
||
private void Awake()
|
||
{
|
||
// 按钮绑定
|
||
_btnNewGame? .onClick.AddListener(OnNewGameClicked);
|
||
_btnContinue?.onClick.AddListener(OnContinueClicked);
|
||
_btnSettings?.onClick.AddListener(OnSettingsClicked);
|
||
_btnCredits? .onClick.AddListener(OnCreditsClicked);
|
||
_btnQuit? .onClick.AddListener(Application.Quit);
|
||
|
||
_btnCloseSaveSlot?.onClick.AddListener(() => CloseSubPanel(_saveSlotPanel, _btnNewGame));
|
||
_btnCloseSettings?.onClick.AddListener(() => CloseSubPanel(_settingsPanel, _btnSettings));
|
||
_btnCloseCredits? .onClick.AddListener(() => CloseSubPanel(_creditsPanel, _btnCredits));
|
||
|
||
// 记录按钮组原始位置(供动画使用)
|
||
if (_mainButtonsRect != null)
|
||
_buttonsPanelOriginalPos = _mainButtonsRect.anchoredPosition;
|
||
|
||
// 初始状态:隐藏子面板,主按钮组不可见(等待入场动画)
|
||
SetPanel(_saveSlotPanel, false);
|
||
SetPanel(_settingsPanel, false);
|
||
SetPanel(_creditsPanel, false);
|
||
SetButtonsGroupVisible(false);
|
||
|
||
// 刷新"继续"按钮可用性(需要至少一个有效存档)
|
||
RefreshContinueButton();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
_onGameStateChanged?.Subscribe(HandleGameStateChanged).AddTo(_subs);
|
||
_onSlotConfirmed? .Subscribe(HandleSlotConfirmed).AddTo(_subs);
|
||
}
|
||
|
||
private void OnDisable() => _subs.Clear();
|
||
|
||
private void Start()
|
||
{
|
||
// 场景初始化完成后播放入场动画
|
||
StartCoroutine(PlayEntryAnimation());
|
||
}
|
||
|
||
// ── 入场动画 ─────────────────────────────────────────────────────────
|
||
|
||
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;
|
||
|
||
// 手柄导航:入场动画完成后将焦点置于第一个按钮
|
||
EventSystem.current?.SetSelectedGameObject(_btnNewGame?.gameObject);
|
||
}
|
||
|
||
// ── 按钮回调 ─────────────────────────────────────────────────────────
|
||
|
||
private void OnNewGameClicked()
|
||
{
|
||
_saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.NewGame);
|
||
OpenSubPanel(_saveSlotPanel);
|
||
}
|
||
private void OnContinueClicked()
|
||
{
|
||
_saveSlotController?.SetMode(BaseGames.UI.Menus.SaveSlotPanelMode.Continue);
|
||
OpenSubPanel(_saveSlotPanel);
|
||
}
|
||
private void OnSettingsClicked() => OpenSubPanel(_settingsPanel); // SettingsPanelController 自行设焦点
|
||
private void OnCreditsClicked()
|
||
{
|
||
OpenSubPanel(_creditsPanel);
|
||
// Credits 面板无独立控制器,打开时把焦点交给返回按钮(键盘 / 手柄可直接退出)
|
||
if (_btnCloseCredits != null)
|
||
EventSystem.current?.SetSelectedGameObject(_btnCloseCredits.gameObject);
|
||
}
|
||
|
||
/// <summary>打开子面板:禁用主按钮组交互,避免键盘/手柄导航"穿透"到背后的主菜单按钮。</summary>
|
||
private void OpenSubPanel(GameObject panel)
|
||
{
|
||
SetMainButtonsInteractable(false);
|
||
SetPanel(panel, true);
|
||
}
|
||
|
||
/// <summary>关闭子面板:恢复主按钮组交互,并把焦点恢复到对应主菜单按钮(导航连续性)。</summary>
|
||
private void CloseSubPanel(GameObject panel, Button focusAfter)
|
||
{
|
||
SetPanel(panel, false);
|
||
SetMainButtonsInteractable(true);
|
||
if (focusAfter != null)
|
||
EventSystem.current?.SetSelectedGameObject(focusAfter.gameObject);
|
||
}
|
||
|
||
private void SetMainButtonsInteractable(bool on)
|
||
{
|
||
if (_mainButtonsGroup == null) return;
|
||
_mainButtonsGroup.interactable = on;
|
||
_mainButtonsGroup.blocksRaycasts = on;
|
||
}
|
||
|
||
// ── 存档槽确认 ───────────────────────────────────────────────────────
|
||
|
||
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;
|
||
// 离开 MainMenu(加载游戏中)时锁定所有交互,防止重复点击
|
||
if (_mainButtonsGroup != null)
|
||
{
|
||
_mainButtonsGroup.interactable = isMainMenu;
|
||
_mainButtonsGroup.blocksRaycasts = isMainMenu;
|
||
}
|
||
}
|
||
|
||
// ── 工具方法 ─────────────────────────────────────────────────────────
|
||
|
||
private void RefreshContinueButton()
|
||
{
|
||
if (_btnContinue == null) return;
|
||
|
||
var saveService = ServiceLocator.GetOrDefault<ISaveService>();
|
||
bool hasAny = saveService != null
|
||
&& (saveService.HasSave(0) || saveService.HasSave(1) || saveService.HasSave(2));
|
||
_btnContinue.interactable = hasAny;
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|