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;
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Assets/_Game/Scripts/UI/MainMenu/NewGameModeConfigSO.cs
Normal file
43
Assets/_Game/Scripts/UI/MainMenu/NewGameModeConfigSO.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.UI.MainMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// 新游戏难度选择数据表(策划编辑)。按顺序列出可选难度;
|
||||
/// <see cref="NewGameModeController"/> 据此生成按钮,点击返回对应 <see cref="DifficultyLevel"/>。
|
||||
/// 策划可增删 / 重排 / 改标签 / 改说明,无需改代码。样式改 UI_NewGameModePanel / UI_MainMenu_Button 预制件。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/UI/New Game Mode Config", fileName = "UI_NewGameModeConfig")]
|
||||
public class NewGameModeConfigSO : ScriptableObject
|
||||
{
|
||||
[Serializable]
|
||||
public struct Item
|
||||
{
|
||||
[Tooltip("此项对应的难度等级。")]
|
||||
public DifficultyLevel level;
|
||||
|
||||
[Tooltip("难度名标签本地化 Key(UI 表,如 MODE_NORMAL)。")]
|
||||
public string labelKey;
|
||||
|
||||
[Tooltip("难度说明本地化 Key(选中该项时显示,可空,如 MODE_STEELSOUL_DESC)。")]
|
||||
public string descKey;
|
||||
|
||||
[Tooltip("难度图标(可空)。")]
|
||||
public Sprite icon;
|
||||
}
|
||||
|
||||
[Tooltip("面板标题本地化 Key。")]
|
||||
[SerializeField] private string _titleKey = "MODE_SELECT_TITLE";
|
||||
|
||||
[Tooltip("返回按钮本地化 Key。")]
|
||||
[SerializeField] private string _backLabelKey = "BTN_BACK";
|
||||
|
||||
[SerializeField] private Item[] _items;
|
||||
|
||||
public string TitleKey => _titleKey;
|
||||
public string BackLabelKey => _backLabelKey;
|
||||
public Item[] Items => _items;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e1f947f274273f4c9a5a310fc40a625
|
||||
guid: ff0448c32aab82546bd71b33da6b2c9a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
@@ -9,99 +11,100 @@ using BaseGames.Localization;
|
||||
namespace BaseGames.UI.MainMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// 新游戏模式选择面板(普通 / 钢铁之魂):开新档前选择难度模式。
|
||||
///
|
||||
/// 设计:
|
||||
/// · 自包含、场景无关——本地 SetActive 显隐 + 回调,不走 UIManager 面板栈,
|
||||
/// 与 MainMenuController 现有的子面板管理方式一致。
|
||||
/// · 选定后通过 onModeChosen 回调把 DifficultyLevel 交还给调用方(SaveSlotController),
|
||||
/// 由调用方负责 CreateSlot(slot, steelSoul) + IDifficultyService.BeginNewGame(level)。
|
||||
/// · 钢铁之魂为破坏性/高难选项,默认焦点置于普通,并显示一段警示文案。
|
||||
/// 新游戏模式(难度)选择面板,<b>数据驱动</b>:据 <see cref="NewGameModeConfigSO"/> 生成难度按钮,
|
||||
/// 经 <see cref="IUINavigator"/> 模态压栈,返回 <see cref="DifficultyLevel"/>;
|
||||
/// 点返回 / 按 ESC 取消则返回 null(由 <see cref="UIResultPanel{T}"/> 兜底)。
|
||||
/// 选中某项时在共享说明区显示其 descKey(手柄/鼠标导航通用)。
|
||||
/// 策划改 UI_NewGameModeConfig 即可增删/重排难度、改标签/说明;样式改 UI_NewGameModePanel / UI_MainMenu_Button 预制件。
|
||||
/// </summary>
|
||||
public class NewGameModeController : MonoBehaviour
|
||||
public class NewGameModeController : UIResultPanel<DifficultyLevel?>
|
||||
{
|
||||
[Header("根节点(显隐用,留空则用本 GameObject)")]
|
||||
[SerializeField] private GameObject _root;
|
||||
[Header("数据表 / 选项列表")]
|
||||
[SerializeField] private NewGameModeConfigSO _config;
|
||||
[Tooltip("难度按钮的父节点(通常挂 VerticalLayoutGroup)。")]
|
||||
[SerializeField] private Transform _container;
|
||||
[SerializeField] private MainMenuButtonView _buttonPrefab;
|
||||
|
||||
[Header("按钮")]
|
||||
[SerializeField] private Button _btnNormal;
|
||||
[SerializeField] private Button _btnSteelSoul;
|
||||
[SerializeField] private Button _btnBack;
|
||||
[Header("引用")]
|
||||
[SerializeField] private LocalizedText _titleText;
|
||||
[Tooltip("当前选中难度的说明文本(随选中项切换)。")]
|
||||
[SerializeField] private TMP_Text _descText;
|
||||
[SerializeField] private Button _btnBack;
|
||||
|
||||
[Header("钢铁之魂说明")]
|
||||
[Tooltip("选中钢铁之魂时显示的警示文案(一命模式,死亡即清档)。走本地化键 MODE_STEELSOUL_DESC。")]
|
||||
[SerializeField] private TMP_Text _steelSoulDescText;
|
||||
[SerializeField] private string _steelSoulDescKey = "MODE_STEELSOUL_DESC";
|
||||
// 取消 / ESC / 返回默认结果:未选择。
|
||||
protected override DifficultyLevel? CancelResult => null;
|
||||
|
||||
private Action<DifficultyLevel> _onModeChosen;
|
||||
private Action _onBack;
|
||||
private readonly List<(MainMenuButtonView view, string descKey)> _options = new();
|
||||
private MainMenuButtonView _firstButton;
|
||||
private GameObject _lastSelected;
|
||||
|
||||
private void Awake()
|
||||
private void Awake() => _btnBack?.onClick.AddListener(() => Complete(null));
|
||||
|
||||
protected override void OnPanelOpen() => BuildMenu();
|
||||
|
||||
/// <summary>默认焦点:第一项(通常普通难度),避免误选高难项。</summary>
|
||||
protected override GameObject ResolveFirstSelected()
|
||||
=> _firstButton != null ? _firstButton.Button.gameObject
|
||||
: _btnBack != null ? _btnBack.gameObject : null;
|
||||
|
||||
/// <summary>据配置重建难度按钮(public 以便编辑器预览/测试)。</summary>
|
||||
public void BuildMenu()
|
||||
{
|
||||
_btnNormal? .onClick.AddListener(() => Choose(DifficultyLevel.Normal));
|
||||
_btnSteelSoul?.onClick.AddListener(() => Choose(DifficultyLevel.SteelSoul));
|
||||
_btnBack? .onClick.AddListener(HandleBack);
|
||||
SetVisible(false);
|
||||
}
|
||||
ClearMenu();
|
||||
if (_titleText != null && _config != null) _titleText.SetKey(_config.TitleKey);
|
||||
if (_config == null || _container == null || _buttonPrefab == null) return;
|
||||
|
||||
/// <summary>
|
||||
/// 弹出模式选择。
|
||||
/// </summary>
|
||||
/// <param name="onModeChosen">玩家选定模式后回调(面板已自动关闭),携带难度档位。</param>
|
||||
/// <param name="onBack">点击返回 / 取消后回调(可选)。</param>
|
||||
public void Show(Action<DifficultyLevel> onModeChosen, Action onBack = null)
|
||||
{
|
||||
_onModeChosen = onModeChosen;
|
||||
_onBack = onBack;
|
||||
|
||||
if (_steelSoulDescText != null && !string.IsNullOrEmpty(_steelSoulDescKey))
|
||||
foreach (var item in _config.Items)
|
||||
{
|
||||
string s = LocalizationManager.Get(_steelSoulDescKey, LocalizationTable.UI);
|
||||
_steelSoulDescText.text = string.IsNullOrEmpty(s) ? _steelSoulDescKey : s;
|
||||
var view = Instantiate(_buttonPrefab, _container);
|
||||
view.gameObject.SetActive(true);
|
||||
var level = item.level;
|
||||
view.Bind(item.labelKey, item.icon, () => Complete(level));
|
||||
_options.Add((view, item.descKey));
|
||||
if (_firstButton == null) _firstButton = view;
|
||||
}
|
||||
|
||||
SetVisible(true);
|
||||
|
||||
// 默认焦点置于普通模式(避免误选一命模式)
|
||||
EventSystem.current?.SetSelectedGameObject(_btnNormal != null
|
||||
? _btnNormal.gameObject
|
||||
: _btnSteelSoul?.gameObject);
|
||||
if (_descText != null) _descText.text = string.Empty;
|
||||
_lastSelected = null;
|
||||
}
|
||||
|
||||
/// <summary>外部强制关闭,不触发回调。</summary>
|
||||
public void Close()
|
||||
private void ClearMenu()
|
||||
{
|
||||
_onModeChosen = null;
|
||||
_onBack = null;
|
||||
SetVisible(false);
|
||||
_options.Clear();
|
||||
_firstButton = null;
|
||||
if (_container == null) return;
|
||||
for (int i = _container.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
var c = _container.GetChild(i).gameObject;
|
||||
if (Application.isPlaying) Destroy(c); else DestroyImmediate(c);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 回调 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void Choose(DifficultyLevel level)
|
||||
// 显示当前选中难度的说明(轮询 EventSystem 选中项;手柄/鼠标导航通用,按钮无需额外组件)。
|
||||
private void Update()
|
||||
{
|
||||
var cb = _onModeChosen;
|
||||
SetVisible(false);
|
||||
_onModeChosen = null;
|
||||
_onBack = null;
|
||||
cb?.Invoke(level);
|
||||
if (_descText == null || EventSystem.current == null) return;
|
||||
var sel = EventSystem.current.currentSelectedGameObject;
|
||||
if (sel == _lastSelected) return;
|
||||
_lastSelected = sel;
|
||||
|
||||
string descKey = null;
|
||||
foreach (var (view, dk) in _options)
|
||||
if (view != null && view.Button != null && view.Button.gameObject == sel) { descKey = dk; break; }
|
||||
_descText.text = string.IsNullOrEmpty(descKey)
|
||||
? string.Empty
|
||||
: LocalizationManager.Get(descKey, LocalizationTable.UI);
|
||||
}
|
||||
|
||||
private void HandleBack()
|
||||
/// <summary>弹出难度选择并等待结果(DifficultyLevel / null=取消)。由导航器压栈管理。</summary>
|
||||
public Task<DifficultyLevel?> ShowAsync(CancellationToken ct = default)
|
||||
{
|
||||
var cb = _onBack;
|
||||
SetVisible(false);
|
||||
_onModeChosen = null;
|
||||
_onBack = null;
|
||||
cb?.Invoke();
|
||||
}
|
||||
|
||||
// ── 工具 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void SetVisible(bool visible)
|
||||
{
|
||||
var go = _root != null ? _root : gameObject;
|
||||
go.SetActive(visible);
|
||||
var nav = GetService<IUINavigator>();
|
||||
if (nav == null)
|
||||
{
|
||||
Debug.LogError("[NewGameMode] 未找到 IUINavigator 服务,无法弹出模式选择。", this);
|
||||
return Task.FromResult<DifficultyLevel?>(null);
|
||||
}
|
||||
return nav.PushForResultAsync<DifficultyLevel?>(this, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user