完整启动流程
This commit is contained in:
@@ -63,6 +63,8 @@ namespace BaseGames.Core.Assets
|
||||
public const string Poolable = "Poolable";
|
||||
public const string BGM = "BGM";
|
||||
public const string Charms = "Charms";
|
||||
/// <summary>游戏启动时预热下载的资产标签(BootSequencer 使用)。</summary>
|
||||
public const string Preload = "Preload";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
104
Assets/_Game/Scripts/Core/BootSequencer.cs
Normal file
104
Assets/_Game/Scripts/Core/BootSequencer.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏启动序列编排器(挂载在 Persistent 场景)。
|
||||
///
|
||||
/// 职责:
|
||||
/// 1. 触发 Splash 演出(通过 EVT_SplashStartRequest 事件,与 BaseGames.UI 程序集解耦)。
|
||||
/// 2. 并行预热 Addressable 依赖(标签 <see cref="Assets.AddressKeys.Labels.Preload"/>)。
|
||||
/// 3. 两者均完成后协程返回,由 <see cref="GameManager"/> 继续加载主菜单场景。
|
||||
///
|
||||
/// 配置说明(Inspector):
|
||||
/// _preloadLabel — 留空则跳过预热,游戏仍可正常启动。
|
||||
/// _onSplashStartRequest / _onSplashComplete — 绑定同名 SO 资产;
|
||||
/// 若留空则视为"无 Splash"直接进入主菜单。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-800)]
|
||||
public class BootSequencer : MonoBehaviour
|
||||
{
|
||||
[Header("Addressable 预热(可选)")]
|
||||
[Tooltip("填写 Addressable 标签名(如 'Preload')以在启动时预热该标签下的所有依赖;留空则跳过。")]
|
||||
[SerializeField] private AssetLabelReference _preloadLabel;
|
||||
|
||||
[Header("Event Channels - Raise")]
|
||||
[Tooltip("发布后 SplashScreenController 开始播放演出。留空则不显示 Splash。")]
|
||||
[SerializeField] private VoidEventChannelSO _onSplashStartRequest;
|
||||
[Tooltip("预热进度 [0,1],供进度条显示。")]
|
||||
[SerializeField] private FloatEventChannelSO _onPreloadProgress;
|
||||
|
||||
[Header("Event Channels - Listen")]
|
||||
[Tooltip("SplashScreenController 演出结束时发布此事件。留空则不等待 Splash 完成。")]
|
||||
[SerializeField] private VoidEventChannelSO _onSplashComplete;
|
||||
|
||||
private bool _splashDone;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 生命周期 ─────────────────────────────────────────────────────────
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onSplashComplete?.Subscribe(() => _splashDone = true).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
// ── 公开 API(供 GameManager 通过 yield 驱动)────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 运行完整启动序列(Splash 演出 + Addressable 预热并行执行)。
|
||||
/// 两者均完成后协程返回;GameManager 随后加载主菜单场景并切换 FSM 状态。
|
||||
/// </summary>
|
||||
public IEnumerator RunBootSequenceCoroutine()
|
||||
{
|
||||
// 若未绑定 SplashComplete 频道,则无需等待 Splash,直接视为已完成
|
||||
_splashDone = (_onSplashComplete == null);
|
||||
|
||||
// 触发 Splash 演出(SplashScreenController 订阅此事件)
|
||||
_onSplashStartRequest?.Raise();
|
||||
|
||||
// 并行启动 Addressable 预热
|
||||
bool preloadDone = false;
|
||||
StartCoroutine(PreloadCoroutine(() => preloadDone = true));
|
||||
|
||||
// 等待两者均完成
|
||||
yield return new WaitUntil(() => _splashDone && preloadDone);
|
||||
}
|
||||
|
||||
// ── 内部:Addressable 预热 ────────────────────────────────────────────
|
||||
|
||||
private IEnumerator PreloadCoroutine(System.Action onDone)
|
||||
{
|
||||
if (_preloadLabel == null || string.IsNullOrEmpty(_preloadLabel.labelString))
|
||||
{
|
||||
// 无预热配置,立即完成
|
||||
_onPreloadProgress?.Raise(1f);
|
||||
onDone?.Invoke();
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 仅下载依赖(不实例化),最小化内存占用
|
||||
var handle = Addressables.DownloadDependenciesAsync(_preloadLabel, false);
|
||||
|
||||
while (!handle.IsDone)
|
||||
{
|
||||
// 保留 10% 余量,避免 PercentComplete 到 0.9 后卡顿
|
||||
_onPreloadProgress?.Raise(handle.PercentComplete * 0.9f);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (handle.Status != AsyncOperationStatus.Succeeded)
|
||||
Debug.LogWarning("[BootSequencer] Addressable 预热部分失败,游戏将继续启动。" +
|
||||
" 请检查 Addressable 标签配置与网络连接。");
|
||||
|
||||
_onPreloadProgress?.Raise(1f);
|
||||
Addressables.Release(handle);
|
||||
onDone?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Assets;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.States;
|
||||
|
||||
@@ -16,6 +17,7 @@ namespace BaseGames.Core
|
||||
// ── Inspector 引用 ────────────────────────────────────────────────
|
||||
[Header("Managers")]
|
||||
[SerializeField] private SettingsManager _settingsManager;
|
||||
[SerializeField] private BootSequencer _bootSequencer;
|
||||
|
||||
[Header("Event Channels - Listen")]
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
@@ -24,6 +26,10 @@ namespace BaseGames.Core
|
||||
[SerializeField] private StringEventChannelSO _onBossFightStarted;
|
||||
[SerializeField] private BoolEventChannelSO _onBossFightEnded;
|
||||
[SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed;
|
||||
[Tooltip("所有场景切换请求(与 SceneService 共享同一 SO)")]
|
||||
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
|
||||
[Tooltip("SceneLoader 完成加载后发布(携带场景名称)")]
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||||
|
||||
[Header("Event Channels - Raise")]
|
||||
[SerializeField] private BaseEventChannelSO<GameStateId> _onGameStateChanged;
|
||||
@@ -51,8 +57,30 @@ namespace BaseGames.Core
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 在 Start 广播初始状态,确保其他组件已在 OnEnable 中完成订阅。
|
||||
// 广播初始状态,确保其他组件已在 OnEnable 中完成订阅。
|
||||
_onGameStateChanged?.Raise(new Events.GameStateId(GameStates.Initializing.Id));
|
||||
|
||||
// 启动引导序列:Splash → 预热 → 加载主菜单场景 → 切换到 MainMenu 状态
|
||||
StartCoroutine(BootCoroutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 游戏启动引导协程:
|
||||
/// 1. 运行 BootSequencer(Splash + Addressable 预热)
|
||||
/// 2. 通过 ISceneService 加载主菜单场景
|
||||
/// 3. SceneLoader 完成后发布 _onSceneLoaded → HandleSceneLoaded 切换 FSM 到 MainMenu
|
||||
/// </summary>
|
||||
private IEnumerator BootCoroutine()
|
||||
{
|
||||
if (_bootSequencer != null)
|
||||
yield return StartCoroutine(_bootSequencer.RunBootSequenceCoroutine());
|
||||
|
||||
var sceneService = ServiceLocator.GetOrDefault<ISceneService>();
|
||||
if (sceneService != null)
|
||||
yield return StartCoroutine(sceneService.LoadMainMenuCoroutine());
|
||||
else
|
||||
Debug.LogError("[GameManager] ISceneService 未注册,无法加载主菜单。" +
|
||||
" 请检查 Persistent 场景的 GameServiceRegistrar 配置。");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -63,6 +91,8 @@ namespace BaseGames.Core
|
||||
_onBossFightStarted? .Subscribe(HandleBossFightStarted).AddTo(_subs);
|
||||
_onBossFightEnded? .Subscribe(HandleBossFightEnded).AddTo(_subs);
|
||||
_onDeathScreenConfirmed?.Subscribe(HandleDeathScreenConfirmed).AddTo(_subs);
|
||||
_onSceneLoadRequest? .Subscribe(HandleSceneLoadRequest).AddTo(_subs);
|
||||
_onSceneLoaded? .Subscribe(HandleSceneLoaded).AddTo(_subs);
|
||||
}
|
||||
|
||||
private void OnDisable() => _subs.Clear();
|
||||
@@ -125,6 +155,45 @@ namespace BaseGames.Core
|
||||
else RequestTransition(GameStates.GameOver);
|
||||
}
|
||||
|
||||
// ── 场景加载状态同步 ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 当 SceneService 开始加载主要场景(非 Room 过渡)时,更新 FSM 到 LoadingScene 状态。
|
||||
/// Room 过渡(TransitionType.Room)不经过此处,状态保持 Gameplay。
|
||||
/// </summary>
|
||||
private void HandleSceneLoadRequest(SceneLoadRequest request)
|
||||
{
|
||||
if (request.TransitionType != TransitionType.Scene) return;
|
||||
|
||||
// 返回主菜单不经过 LoadingScene 中间状态(_onSceneLoaded 会直接处理)
|
||||
if (request.SceneName == AddressKeys.SceneMainMenu) return;
|
||||
|
||||
var cur = _fsm.CurrentStateId;
|
||||
if (cur == GameStates.MainMenu
|
||||
|| cur == GameStates.Gameplay
|
||||
|| cur == GameStates.BossFight)
|
||||
{
|
||||
RequestTransition(GameStates.LoadingScene);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SceneLoader 完成加载后根据目标场景名自动切换 FSM 状态:
|
||||
/// SceneMainMenu → MainMenu(适用于 Initializing / Paused / GameOver / LoadingScene)
|
||||
/// 其他场景 → Gameplay(仅当处于 LoadingScene 时;Room 过渡不受影响)
|
||||
/// </summary>
|
||||
private void HandleSceneLoaded(string sceneName)
|
||||
{
|
||||
if (sceneName == AddressKeys.SceneMainMenu)
|
||||
{
|
||||
RequestTransition(GameStates.MainMenu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fsm.CurrentStateId == GameStates.LoadingScene)
|
||||
RequestTransition(GameStates.Gameplay);
|
||||
}
|
||||
|
||||
private bool _deathScreenConfirmed;
|
||||
private GameStateId _prePauseState = GameStates.Gameplay;
|
||||
private void HandleDeathScreenConfirmed() => _deathScreenConfirmed = true;
|
||||
|
||||
@@ -20,19 +20,39 @@ namespace BaseGames.Core
|
||||
[Header("Event Channels - Raise")]
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded;
|
||||
|
||||
[Header("Event Channels - Raise(加载画面)")]
|
||||
[Tooltip("ShowLoadingScreen=true 时,在加载开始时发布。")]
|
||||
[SerializeField] private VoidEventChannelSO _onLoadingStarted;
|
||||
[Tooltip("ShowLoadingScreen=true 时,在加载完成时发布。")]
|
||||
[SerializeField] private VoidEventChannelSO _onLoadingComplete;
|
||||
[Tooltip("ShowLoadingScreen=true 时,持续发布加载进度 [0,1]。")]
|
||||
[SerializeField] private FloatEventChannelSO _onLoadingProgressUpdated;
|
||||
|
||||
private string _currentRoomScene;
|
||||
private AsyncOperationHandle<SceneInstance> _currentHandle;
|
||||
|
||||
public IEnumerator LoadSceneCoroutine(SceneLoadRequest request)
|
||||
{
|
||||
if (request.ShowLoadingScreen)
|
||||
_onLoadingStarted?.Raise();
|
||||
|
||||
// 先加载新场景(Additive),成功后再卸载旧场景
|
||||
// 顺序保证:若加载失败,旧场景仍保持可用,不会出现无场景的空状态
|
||||
var loadOp = Addressables.LoadSceneAsync(request.SceneName, LoadSceneMode.Additive);
|
||||
yield return loadOp;
|
||||
|
||||
// 逐帧轮询以上报进度(不能直接 yield return loadOp,那样无法回调进度)
|
||||
while (!loadOp.IsDone)
|
||||
{
|
||||
if (request.ShowLoadingScreen)
|
||||
_onLoadingProgressUpdated?.Raise(loadOp.PercentComplete * 0.9f);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (loadOp.Status != AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
Debug.LogError($"[SceneLoader] 加载场景失败:{request.SceneName}(旧场景保持不变)");
|
||||
if (request.ShowLoadingScreen)
|
||||
_onLoadingComplete?.Raise();
|
||||
yield break;
|
||||
}
|
||||
|
||||
@@ -45,6 +65,13 @@ namespace BaseGames.Core
|
||||
|
||||
_currentHandle = loadOp;
|
||||
_currentRoomScene = request.SceneName;
|
||||
|
||||
if (request.ShowLoadingScreen)
|
||||
{
|
||||
_onLoadingProgressUpdated?.Raise(1f);
|
||||
_onLoadingComplete?.Raise();
|
||||
}
|
||||
|
||||
_onSceneLoaded?.Raise(request.SceneName);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user