完整启动流程

This commit is contained in:
2026-05-19 23:20:44 +08:00
parent d0a1112737
commit 5fd981f5b9
22 changed files with 1938 additions and 14 deletions

View File

@@ -0,0 +1,212 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Core;
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;
[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 = "Scene_Game_Chapter1";
// ── 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(() => SetPanel(_saveSlotPanel, false));
_btnCloseSettings?.onClick.AddListener(() => SetPanel(_settingsPanel, false));
_btnCloseCredits? .onClick.AddListener(() => SetPanel(_creditsPanel, false));
// 记录按钮组原始位置(供动画使用)
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;
}
// ── 按钮回调 ─────────────────────────────────────────────────────────
private void OnNewGameClicked() => SetPanel(_saveSlotPanel, true);
private void OnContinueClicked() => SetPanel(_saveSlotPanel, true);
private void OnSettingsClicked() => SetPanel(_settingsPanel, true);
private void OnCreditsClicked() => SetPanel(_creditsPanel, true);
// ── 存档槽确认 ───────────────────────────────────────────────────────
private void HandleSlotConfirmed(int _)
{
SetPanel(_saveSlotPanel, false);
_onSceneLoadRequest?.Raise(new SceneLoadRequest
{
SceneName = _firstGameSceneKey,
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;
}
}
}

View File

@@ -1,5 +1,7 @@
using UnityEngine;
using UnityEngine.UI;
using BaseGames.Core;
using BaseGames.Core.Assets;
using BaseGames.Core.Events;
namespace BaseGames.UI
@@ -51,9 +53,9 @@ namespace BaseGames.UI
_uiManager.CloseTopPanel();
_onSceneLoadRequest?.Raise(new SceneLoadRequest
{
SceneName = "MainMenu",
SceneName = AddressKeys.SceneMainMenu,
TransitionType = TransitionType.Scene,
ShowLoadingScreen = true
ShowLoadingScreen = false,
});
}
}

View File

@@ -0,0 +1,138 @@
using System.Collections;
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.UI.Splash
{
/// <summary>
/// 游戏启动 Logo 演出控制器。挂载在 Persistent 场景的 Canvas_Splash 根节点。
///
/// 演出顺序:工作室 Logo 淡入 → 停留 → 淡出 → 游戏标题淡入 → 停留 → 黑幕整体淡出。
/// 任意按键 / 手柄键可跳过整段演出。
///
/// 与 Core 程序集完全解耦:
/// 监听 EVT_SplashStartRequestVoidEventChannelSO→ 开始演出。
/// 完成后发布 EVT_SplashCompleteVoidEventChannelSO→ 通知 BootSequencer 继续。
/// </summary>
public class SplashScreenController : MonoBehaviour
{
[Header("Canvas Groups")]
[Tooltip("整个 Splash 画布的根节点 CanvasGroup用于整体淡出黑幕")]
[SerializeField] private CanvasGroup _splashRoot;
[Tooltip("工作室 Logo CanvasGroup")]
[SerializeField] private CanvasGroup _studioLogoGroup;
[Tooltip("游戏标题 Logo CanvasGroup")]
[SerializeField] private CanvasGroup _gameTitleGroup;
[Header("时序(秒)")]
[SerializeField] private float _fadeInDuration = 0.8f;
[SerializeField] private float _holdDuration = 1.5f;
[SerializeField] private float _fadeOutDuration = 0.6f;
[SerializeField] private float _stageGapDuration = 0.3f;
[Header("Event Channels")]
[SerializeField] private VoidEventChannelSO _onSplashStartRequest; // Listen
[SerializeField] private VoidEventChannelSO _onSplashComplete; // Raise
private bool _skipRequested;
private readonly CompositeDisposable _subs = new();
// ── 生命周期 ─────────────────────────────────────────────────────────
private void Awake()
{
// 初始态:根节点完全不透明(充当开场黑幕),各 Logo 完全透明
SetAlpha(_splashRoot, 1f, blocksRaycasts: true);
SetAlpha(_studioLogoGroup, 0f, blocksRaycasts: false);
SetAlpha(_gameTitleGroup, 0f, blocksRaycasts: false);
}
private void OnEnable() => _onSplashStartRequest?.Subscribe(OnStartRequested).AddTo(_subs);
private void OnDisable() => _subs.Clear();
// ── 入口 ─────────────────────────────────────────────────────────────
private void OnStartRequested() => StartCoroutine(PlayAndNotify());
private IEnumerator PlayAndNotify()
{
yield return StartCoroutine(PlayAsync());
_onSplashComplete?.Raise();
}
// ── 演出主逻辑 ───────────────────────────────────────────────────────
private IEnumerator PlayAsync()
{
_skipRequested = false;
// Stage 1工作室 Logo
if (_studioLogoGroup != null)
{
yield return StartCoroutine(FadeGroup(_studioLogoGroup, 0f, 1f, _fadeInDuration));
yield return StartCoroutine(WaitOrSkip(_holdDuration));
yield return StartCoroutine(FadeGroup(_studioLogoGroup, 1f, 0f, _fadeOutDuration));
}
if (!_skipRequested)
yield return new WaitForSecondsRealtime(_stageGapDuration);
// Stage 2游戏标题
if (_gameTitleGroup != null)
{
yield return StartCoroutine(FadeGroup(_gameTitleGroup, 0f, 1f, _fadeInDuration));
yield return StartCoroutine(WaitOrSkip(_holdDuration));
yield return StartCoroutine(FadeGroup(_gameTitleGroup, 1f, 0f, _fadeOutDuration));
}
// 黑幕整体淡出(显露主菜单场景)
yield return StartCoroutine(FadeGroup(_splashRoot, 1f, 0f, _fadeOutDuration));
if (_splashRoot != null)
_splashRoot.blocksRaycasts = false;
gameObject.SetActive(false);
}
// ── Unity 输入(任意按键跳过)────────────────────────────────────────
private void Update()
{
if (Input.anyKeyDown)
_skipRequested = true;
}
// ── 内部工具 ─────────────────────────────────────────────────────────
private IEnumerator FadeGroup(CanvasGroup group, float from, float to, float duration)
{
if (group == null) yield break;
group.alpha = from;
float elapsed = 0f;
while (elapsed < duration && !_skipRequested)
{
group.alpha = Mathf.Lerp(from, to, elapsed / duration);
elapsed += Time.unscaledDeltaTime;
yield return null;
}
group.alpha = to;
}
private IEnumerator WaitOrSkip(float duration)
{
float elapsed = 0f;
while (elapsed < duration && !_skipRequested)
{
elapsed += Time.unscaledDeltaTime;
yield return null;
}
}
private static void SetAlpha(CanvasGroup group, float alpha, bool blocksRaycasts)
{
if (group == null) return;
group.alpha = alpha;
group.blocksRaycasts = blocksRaycasts;
}
}
}