Files
zeling_v2/Docs/Architecture/03_CoreModule.md
2026-05-08 11:04:00 +08:00

36 KiB
Raw Permalink Blame History

03 · Core 核心模块

命名空间 BaseGames.Core
程序集 BaseGames.Core
路径 Assets/Scripts/Core/
依赖 BaseGames.Core.EventsNewtonsoft.Json


目录

  1. GameManager
  2. GameState 枚举与合法转换表
  3. SceneLoader
  4. SceneLoadRequest 数据结构
  5. GlobalObjectPool引用 13_AssetPoolModule
  6. SettingsManager
  7. GlobalSettingsSO
  8. 死亡复活流程(时序)
  9. Boss 战切换流程(时序)
  10. 初始化序列ExecutionOrder
  11. ServiceLocator — 轻量依赖注入
  12. DeathRespawnService — 死亡/复活服务拆分
  13. SceneService — 场景管理服务拆分

1. GameManager

路径: Assets/Scripts/Core/GameManager.cs
程序集: BaseGames.Core
[DefaultExecutionOrder(-1000)]

字段

[Header("Event Channels - Listen")]
[SerializeField] private VoidEventChannelSO    _onPlayerDied;
[SerializeField] private VoidEventChannelSO    _onSavePointActivated;
[SerializeField] private StringEventChannelSO  _onBossFightStarted;
[SerializeField] private BoolEventChannelSO    _onBossFightEnded;
[SerializeField] private VoidEventChannelSO    _onPauseRequested;
[SerializeField] private StringEventChannelSO  _onSceneLoaded;
[SerializeField] private VoidEventChannelSO    _onDeathScreenConfirmed;  // DeathScreenController 按钮点击

[Header("Event Channels - Raise")]
[SerializeField] private GameStateEventChannelSO     _onGameStateChanged;
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
[SerializeField] private VoidEventChannelSO          _onPlayerRespawned;

[Header("Sub Managers同 Persistent 场景内引用)")]
[SerializeField] private SceneLoader     _sceneLoader;
[SerializeField] private GlobalObjectPool _objectPool;   // ⚠️ 权威类名为 GlobalObjectPool见 13_AssetPoolModule §3
[SerializeField] private SettingsManager _settingsManager;
// SaveManager 是独立单例,不通过 GameManager 暴露

private GameState _currentState = GameState.Initializing;
private string    _lastSavePointId;
private string    _currentSceneName;

公开接口

// 状态管理
public GameState CurrentState => _currentState;
public void TransitionTo(GameState newState);   // 内含合法性检查,发布 _onGameStateChanged

// 场景控制(通过 SceneLoader 编排)
public void LoadRoom(string roomSceneName, string entryTransitionId = null);
public void ReturnToMainMenu();

// 暂停
public void Pause();
public void Resume();

// 复活(由死亡流程 Coroutine 最终调用)
internal void CompleteRespawn();

私有/内部方法

// 生命周期
private void Awake();   // 初始化序列(见 §10
private void Start();   // 首帧后跳转 MainMenu

// 事件处理
private void HandlePlayerDied();               // → TransitionTo(Dead) → 启动 DeathCoroutine
private void HandleSavePointActivated(string saveId);  // → SaveManager.SaveAsync()
private void HandleBossFightStarted(string bossId);    // → TransitionTo(BossFight)
private void HandleBossFightEnded(bool victory);       // → 根据结果决策
private void HandlePauseRequested();           // → Pause() / Resume() 切换

// Coroutines
private IEnumerator DeathSequenceCoroutine();     // 死亡动画 → 显示 DeathScreen → 等待输入
private IEnumerator RespawnCoroutine();           // 淡出 → LoadLastSaveScene → 淡入 → 初始化
private IEnumerator LoadSceneCoroutine(SceneLoadRequest request);

2. GameState 插件化状态机

设计动机:原 enum GameState 在编译期固定DLC 若需引入新游戏模式必须修改核心枚举,违反开闭原则。本节将状态机改为 IGameState 接口 + 运行时注册表,内置 8 个状态保持不变,外部模块可通过 RuntimeInitializeOnLoad 注入新状态,无需重新编译核心程序集。

2.1 GameStateId — 运行时可注册状态标识

// 路径: Assets/Scripts/Core/GameStateId.cs
namespace BaseGames.Core
{
    /// <summary>
    /// 轻量状态标识符(值类型,无堆分配)。
    /// 内置状态通过 <see cref="GameStates"/> 静态字段访问,
    /// 扩展状态在 [RuntimeInitializeOnLoad] 中调用 Register() 注入。
    /// </summary>
    public readonly struct GameStateId : System.IEquatable<GameStateId>
    {
        public readonly int Value;
        private GameStateId(int v) => Value = v;

        private static int _nextId;
        private static readonly System.Collections.Generic.Dictionary<string, GameStateId>
            _registry = new();

        /// <summary>注册状态;已注册则返回现有 ID幂等。</summary>
        public static GameStateId Register(string name)
        {
            if (_registry.TryGetValue(name, out var existing)) return existing;
            var id = new GameStateId(_nextId++);
            _registry[name] = id;
            return id;
        }

        public static bool TryResolve(string name, out GameStateId id)
            => _registry.TryGetValue(name, out id);

        public static IEnumerable<GameStateId> All => _registry.Values;

        public bool Equals(GameStateId other) => Value == other.Value;
        public override bool Equals(object obj) => obj is GameStateId g && Equals(g);
        public override int GetHashCode() => Value;
        public static bool operator ==(GameStateId a, GameStateId b) => a.Value == b.Value;
        public static bool operator !=(GameStateId a, GameStateId b) => a.Value != b.Value;
        public override string ToString()
        {
            foreach (var kv in _registry)
                if (kv.Value == this) return kv.Key;
            return $"GameState({Value})";
        }
    }

    /// <summary>内置状态常量(静态初始化顺序确保在任何 Awake 前完成)。</summary>
    public static class GameStates
    {
        public static readonly GameStateId Initializing = GameStateId.Register("Initializing");
        public static readonly GameStateId MainMenu     = GameStateId.Register("MainMenu");
        public static readonly GameStateId LoadingScene = GameStateId.Register("LoadingScene");
        public static readonly GameStateId Gameplay     = GameStateId.Register("Gameplay");
        public static readonly GameStateId BossFight    = GameStateId.Register("BossFight");
        public static readonly GameStateId Paused       = GameStateId.Register("Paused");
        public static readonly GameStateId Dead         = GameStateId.Register("Dead");
        public static readonly GameStateId Cutscene     = GameStateId.Register("Cutscene");
    }
}

2.2 IGameState — 状态行为接口

// 路径: Assets/Scripts/Core/IGameState.cs
namespace BaseGames.Core
{
    /// <summary>
    /// 可插件化的游戏状态行为接口。
    /// 每个状态封装自身的进入/退出/Tick 逻辑,不再依赖 GameManager 中的 switch 分支。
    /// </summary>
    public interface IGameState
    {
        GameStateId Id { get; }

        /// <summary>合法的后继状态集合。GameManager.TransitionTo() 据此校验。</summary>
        IReadOnlyCollection<GameStateId> ValidNextStates { get; }

        void OnEnter(GameStateId previousState);
        void OnExit(GameStateId nextState);

        /// <summary>每帧由 GameManager.Update() 调用(可为空实现)。</summary>
        void Tick(float deltaTime) { }
    }
}

2.3 GameStateMachine — 轻量状态机核心

// 路径: Assets/Scripts/Core/GameStateMachine.cs
namespace BaseGames.Core
{
    /// <summary>
    /// 状态机核心,持有状态注册表与当前状态。
    /// GameManager 持有一个实例;状态对象在 Awake 注入(内置 + 可选 DLC 追加)。
    /// </summary>
    public class GameStateMachine
    {
        private readonly Dictionary<GameStateId, IGameState> _states = new();
        private IGameState _current;

        public GameStateId CurrentStateId => _current?.Id ?? default;

        /// <summary>注册状态实现(同 Id 注册多次以最后一次为准)。</summary>
        public void Register(IGameState state) => _states[state.Id] = state;

        public bool TransitionTo(GameStateId nextId, out string error)
        {
            if (!_states.TryGetValue(nextId, out var next))
            {
                error = $"[GameStateMachine] 未知状态 '{nextId}'";
                return false;
            }
            if (_current != null && !_current.ValidNextStates.Contains(nextId))
            {
                error = $"[GameStateMachine] 非法转换 {_current.Id} → {nextId}";
                return false;
            }
            var prev = _current?.Id ?? default;
            _current?.OnExit(nextId);
            _current = next;
            _current.OnEnter(prev);
            error = null;
            return true;
        }

        public void Tick(float deltaTime) => _current?.Tick(deltaTime);
    }
}

2.4 GameManager 集成

// GameManager.cs — 关键修改(其余字段不变)
public class GameManager : MonoBehaviour
{
    // 状态机实例(不再使用 enum currentState
    private readonly GameStateMachine _fsm = new();

    private void Awake()
    {
        // 注册内置状态
        _fsm.Register(new InitializingState());
        _fsm.Register(new MainMenuState(_onMainMenuEntered));
        _fsm.Register(new LoadingSceneState(_onLoadingStarted));
        _fsm.Register(new GameplayState(_inputReader, _onGameplayStarted));
        _fsm.Register(new BossFightState(_onBossFightEntered));
        _fsm.Register(new PausedState(_onGamePaused));
        _fsm.Register(new DeadState(_onPlayerDied));
        _fsm.Register(new CutsceneState(_onCutsceneStarted));

        // DLC / 模块注入(可选;通过 [RuntimeInitializeOnLoad] 在 Awake 前注入)
        foreach (var ext in _externalStateFactories)
            _fsm.Register(ext.Create());

        _fsm.TransitionTo(GameStates.Initializing, out _);
    }

    // 对外仍使用 GameStateId或兼容属性
    public GameStateId CurrentState => _fsm.CurrentStateId;

    public void TransitionTo(GameStateId nextState)
    {
        if (!_fsm.TransitionTo(nextState, out var err))
            Debug.LogWarning(err);
        else
            _onGameStateChanged.Raise(nextState);
    }

    private void Update() => _fsm.Tick(Time.deltaTime);

    // ── DLC 扩展点 ────────────────────────────────────────────────────────
    // 外部模块在 [RuntimeInitializeOnLoad(RuntimeInitializeLoadType.BeforeSceneLoad)]
    // 调用 GameManager.RegisterStateFactory(factory) 注入新状态
    private static readonly List<IGameStateFactory> _externalStateFactories = new();
    public static void RegisterStateFactory(IGameStateFactory factory)
        => _externalStateFactories.Add(factory);
}

/// <summary>DLC 模块实现此接口以注入新游戏状态。</summary>
public interface IGameStateFactory
{
    IGameState Create();
}

2.5 内置状态示例

// 路径: Assets/Scripts/Core/States/GameplayState.cs
public class GameplayState : IGameState
{
    private readonly VoidEventChannelSO _onEnterChannel;
    private readonly InputReaderSO      _inputReader;

    public GameplayState(InputReaderSO input, VoidEventChannelSO onEnter)
    {
        _inputReader   = input;
        _onEnterChannel = onEnter;
    }

    public GameStateId Id => GameStates.Gameplay;

    public IReadOnlyCollection<GameStateId> ValidNextStates { get; } =
        new HashSet<GameStateId>
        {
            GameStates.LoadingScene,
            GameStates.BossFight,
            GameStates.Paused,
            GameStates.Dead,
            GameStates.Cutscene,
        };

    public void OnEnter(GameStateId prev)
    {
        _inputReader.EnableGameplayMap();
        _onEnterChannel.Raise();
    }

    public void OnExit(GameStateId next)
    {
        if (next == GameStates.Paused) return;  // 暂停不禁用 Gameplay map
        _inputReader.DisableGameplayMap();
    }
}

合法转换一览(同前)

Initializing  → MainMenu
MainMenu      → LoadingScene
LoadingScene  → MainMenu | Gameplay
Gameplay      → LoadingScene | BossFight | Paused | Dead | Cutscene
BossFight     → LoadingScene | Gameplay | Paused | Dead
Paused        → Gameplay | BossFight | MainMenu
Dead          → LoadingScene
Cutscene      → Gameplay

向后兼容:现有订阅 GameStateEventChannelSO(泛型参数改为 GameStateId)的代码,将事件类型从 GameStateenum替换为 GameStateIdstruct即可API 形态不变。GameStateId == GameStateId 值比较性能与 enum 相同int 比较)。

路径: Assets/Scripts/Core/SceneLoader.cs
程序集: BaseGames.Core

职责

  • 监听 EVT_SceneLoadRequest 频道,执行异步加载
  • 管理 Persistent 场景常驻(加法加载/卸载)
  • 加载完成后发布 EVT_SceneLoaded

字段

[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
[SerializeField] private StringEventChannelSO           _onSceneLoaded;
[SerializeField] private VoidEventChannelSO             _onFadeInRequest;   // 触发 UI 淡入
[SerializeField] private VoidEventChannelSO             _onFadeOutRequest;  // 触发 UI 淡出
[SerializeField] private float _fadeDuration = 0.3f;

private string _currentRoomScene;  // 当前已加载的房间场景名

接口

// 由 GameManager 或 RoomTransition 触发
public void RequestLoad(SceneLoadRequest request);

// 内部流程
private IEnumerator LoadSceneCoroutine(SceneLoadRequest request);
// 1. Raise FadeOut
// 2. yield LoadSceneAsync房间场景
// 3. 卸载旧房间场景(保留 Persistent
// 4. 激活新场景
// 5. Raise SceneLoaded供 GameManager 切状态、MapManager 更新)
// 6. Raise FadeIn

4. SceneLoadRequest 数据结构

// 路径: Assets/Scripts/Core/SceneLoadRequest.cs
namespace BaseGames.Core
{
    [System.Serializable]
    public struct SceneLoadRequest
    {
        public string  SceneName;           // Addressable 场景键AddressKeys 常量)
        public string  EntryTransitionId;   // 玩家出生点 IDRoomTransition 与之匹配)
        public bool    ShowLoadingScreen;   // 跨大区域时 true
        public bool    IsRespawn;           // 死亡复活时 true不执行过渡动画
    }
}

5. GlobalObjectPool

⚠️ 权威实现见 13_AssetPoolModule.md §3
类名为 GlobalObjectPool(命名空间 BaseGames.Core)。
本模块不重复定义,以下仅列出与 CoreModule 相关的调用入口摘要。

// 使用方式CoreModule 内部调用)
GlobalObjectPool.Instance.WarmupAsync();                   // SceneLoader 步骤 4 调用
GlobalObjectPool.Instance.Spawn(addressKey, pos, rot);     // 取出实例
GlobalObjectPool.Instance.Despawn(addressKey, instance);   // 归还实例
GlobalObjectPool.Instance.ClearPool(addressKey);           // 场景卸载时清除指定池

6. SettingsManager

路径: Assets/Scripts/Core/SettingsManager.cs
程序集: BaseGames.Core
[DefaultExecutionOrder(-800)]

职责

  • GlobalSettingsSO 读取默认值,从 PlayerPrefs / 独立设置文件加载用户覆盖
  • 应用设置到 AudioMixer、Screen、InputSystem
  • 提供运行时修改接口(供 SettingsPanel 调用)

字段

[SerializeField] private GlobalSettingsSO _defaultSettings;
[SerializeField] private FloatEventChannelSO _onMasterVolumeChanged;  // 同步 HUD

private GlobalSettingsData _current;   // 运行时值(从文件读取)

接口

public void Initialize();   // GameManager.Awake 调用

// 音量0-1 线性;内部调用 AudioManager.Instance.SetVolume(AudioMixerKeys.*, value) 写入 AudioMixer
public void SetMasterVolume(float value)  => AudioManager.Instance.SetVolume(AudioMixerKeys.Master,  value);
public void SetBGMVolume(float value)     => AudioManager.Instance.SetVolume(AudioMixerKeys.BGM,     value);
public void SetSFXVolume(float value)     => AudioManager.Instance.SetVolume(AudioMixerKeys.SFX,     value);
public void SetAmbientVolume(float value) => AudioManager.Instance.SetVolume(AudioMixerKeys.Ambient, value);

// 画面
public void SetResolution(int width, int height, FullScreenMode mode);
public void SetVSync(bool enabled);
public void SetTargetFrameRate(int fps);   // 仅非 VSync 时生效

// 语言
public void SetLanguage(string localeCode);  // 调用 LocalizationSettings.SelectedLocale

// 保存(写入独立设置文件)
public void Save();

// 当前值访问
public GlobalSettingsData Current => _current;

7. GlobalSettingsSO

路径: Assets/Scripts/Core/GlobalSettingsSO.cs
资产: Assets/Data/Settings/SET_GlobalSettings.asset
[CreateAssetMenu(menuName = "Settings/GlobalSettings")]
public class GlobalSettingsSO : ScriptableObject
{
    [Header("Audio")]
    public float DefaultMasterVolume  = 1f;
    public float DefaultBGMVolume     = 0.8f;
    public float DefaultSFXVolume     = 1f;
    public float DefaultAmbientVolume = 0.8f;   // ⚠️ 与 AudioMixerKeys.Ambient 对应

    [Header("Display")]
    public int   DefaultTargetFPS    = 60;
    public bool  DefaultVSync        = false;

    [Header("Language")]
    public string DefaultLocaleCode  = "zh-CN";

    [Header("Accessibility")]
    public bool DefaultHighContrast  = false;
    public bool DefaultScreenShake   = true;
}

[System.Serializable]
public class GlobalSettingsData
{
    public float MasterVolume;
    public float BGMVolume;
    public float SFXVolume;
    public float AmbientVolume;   // ⚠️ 与 AudioMixerKeys.Ambient 对应
    public int   TargetFPS;
    public bool  VSync;
    public string LocaleCode;
    public bool  HighContrast;
    public bool  ScreenShake;
}

8. 死亡复活流程(时序)

[PlayerStats] HP ≤ 0
    → Raise EVT_PlayerDied

[GameManager] HandlePlayerDied()
    → TransitionTo(GameState.Dead)
        → Raise EVT_GameStateChanged(Dead)
            ← [UIManager] 禁用 HUD 输入
            ← [AudioManager] 播放死亡音乐/SFX
    → StartCoroutine(DeathSequenceCoroutine)
        1. 等待死亡动画完成(约 1.5s
        2. DeathScreenController 订阅 EVT_PlayerDied延迟 1.5s 后自动显示死亡画面;无需 GameManager 直接调用 UI
        3. 等待玩家按下"重试"(监听 EVT_DeathScreenConfirmed

[GameManager] RespawnCoroutine()
    → TransitionTo(GameState.LoadingScene)
    → SceneLoader.RequestLoad(lastSaveScene, isRespawn: true)
        → 淡出
        → 卸载当前房间场景
        → 加载存档点对应场景
        → SaveManager 恢复玩家状态HP = MaxHP×0.67,灵泉=1
        → 淡入
    → TransitionTo(GameState.Gameplay)
    → Raise EVT_PlayerRespawned

9. Boss 战切换流程(时序)

[BossOrchestrator] 玩家进入 Boss 房间触发器
    → Raise EVT_BossFightStarted("boss_forest")

[GameManager] HandleBossFightStarted(bossId)
    → TransitionTo(GameState.BossFight)
        ← [AudioManager] 切换 Boss BGMAudioMixer 快照)
        ← [UIManager] 显示 Boss 血量条
        ← [CinemachineCamera] 切换为 BossCamera 虚拟摄像机

[BossOrchestrator] Boss 死亡
    → Raise EVT_BossFightEnded(victory: true)

[GameManager] HandleBossFightEnded(true)
    → TransitionTo(GameState.Gameplay)
        ← [AudioManager] 恢复常规 BGM
        ← [UIManager] 隐藏 Boss 血量条
    → 触发剧情 / 过场(如有)

10. 初始化序列

// GameManager.Awake()
[DefaultExecutionOrder(-1000)]
private void Awake()
{
    DontDestroyOnLoad(gameObject);              // Persistent 场景随 GameManager 常驻

    Application.targetFrameRate = 60;
    Physics2D.simulationMode = SimulationMode2D.Script;  // 手动物理步进

    // 子系统初始化(按依赖顺序)
    _settingsManager.Initialize();  // 1. 读取设置文件,应用音量/分辨率
    // SaveManager 自初始化SaveManager.Awake 先于 GameManager.Start
    // GlobalObjectPool 无 Initialize() 方法;预热由 SceneLoader.LoadSceneCoroutine 步骤 4
    // 调用 GlobalObjectPool.Instance.WarmupAsync() 触发(见 13_AssetPoolModule §6
    // DifficultyManager 自初始化DefaultExecutionOrder: -900早于本 Awake
    //   → 读取 SaveData.Meta.CurrentDifficulty
    //   → Apply(level) → 广播 EVT_DifficultyChanged
    //   → EnemyStats / PlayerStats / ShopController 等系统订阅此事件后注入难度系数
    //   → 详见 19_DifficultyModule.md

    TransitionTo(GameState.Initializing);
}

private IEnumerator Start()
{
    yield return null;    // 等待首帧渲染
    TransitionTo(GameState.MainMenu);
    // → UIManager 收到 EVT_GameStateChanged(MainMenu) → 显示 MainMenuPanel
    // → AudioManager 收到 → 播放主菜单 BGM
}

11. ServiceLocator — 轻量依赖注入

P0 优化:原项目中 AudioManager.InstanceEventChannelRegistry.InstanceSaveManager 等均为静态单例,
在多场景 Play Mode 快速进入Direct Play from Room scene时若 Persistent 场景未加载,
Instance 为 null 导致崩溃;测试时也无法替换为 Mock 实现。
引入 ServiceLocator 替代所有硬编码 Instance,保留 Inspector 序列化注入路径(无需引入 Zenject/VContainer 等外部 DI 框架)。

11.1 ServiceLocator 核心

// 路径: Assets/Scripts/Core/ServiceLocator.cs
namespace BaseGames.Core
{
    /// <summary>
    /// 轻量服务定位器。
    /// - 通过类型键注册/查找服务,支持接口类型注册(依赖倒置)
    /// - 在 Persistent 场景 AwakeExecutionOrder -2000批量注册全局服务
    /// - 支持 Fallback找不到服务时返回 NullObject 而非 null防崩溃
    /// - Editor 下 Play Mode "Enter Play Mode Without Domain Reload" 友好(静态字典不清空)
    /// </summary>
    public static class ServiceLocator
    {
        private static readonly Dictionary<Type, object> _services = new();

        // ── 注册 ────────────────────────────────────────────────────────
        /// <summary>以接口类型 TInterface 注册实现 impl。</summary>
        public static void Register<TInterface>(TInterface impl)
            => _services[typeof(TInterface)] = impl;

        /// <summary>仅当尚未注册时才注册(防多场景重复注册同一服务)。</summary>
        public static void RegisterIfAbsent<TInterface>(TInterface impl)
        {
            if (!_services.ContainsKey(typeof(TInterface)))
                _services[typeof(TInterface)] = impl;
        }

        // ── 查找 ────────────────────────────────────────────────────────
        /// <summary>
        /// 查找服务。未注册时返回 defaultT为class则为null并输出LogError。
        /// </summary>
        public static TInterface Get<TInterface>()
        {
            if (_services.TryGetValue(typeof(TInterface), out var svc) && svc is TInterface typed)
                return typed;
            // 未注册时抛出明确异常(带上下文),而非返回 null 造成延迟 NullReferenceException
            throw new InvalidOperationException(
                $"[ServiceLocator] Service '{typeof(TInterface).Name}' is not registered. "
                + "Ensure GameServiceRegistrar.Awake() has run before this call "
                + "(ExecutionOrder -2000). For optional services use GetOrDefault<T>().");
        }

        /// <summary>安全版 Get未注册时返回 fallback不报错适用于可选服务。</summary>
        public static TInterface GetOrDefault<TInterface>(TInterface fallback = default)
            => _services.TryGetValue(typeof(TInterface), out var svc) && svc is TInterface typed
               ? typed : fallback;

        // ── 测试支持 ─────────────────────────────────────────────────────
        /// <summary>单元测试中替换服务实现(仅在 UNITY_EDITOR 或 TEST 编译条件下可用)。</summary>
        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void OverrideForTest<TInterface>(TInterface mock)
            => _services[typeof(TInterface)] = mock;

        [System.Diagnostics.Conditional("UNITY_EDITOR")]
        public static void Reset() => _services.Clear();
    }
}

11.2 全局服务接口清单

接口 实现类 NullObject 实现
IAudioService AudioManager NullAudioService
ISaveService SaveManager
ISceneService SceneService(见 §13
IDeathRespawnService DeathRespawnService(见 §12
IEventChannelRegistry EventChannelRegistry

11.3 服务注册器

// 路径: Assets/Scripts/Core/GameServiceRegistrar.cs
// Persistent 场景内挂载最早执行ExecutionOrder -2000
[DefaultExecutionOrder(-2000)]
public class GameServiceRegistrar : MonoBehaviour
{
    [Header("服务实现引用Inspector 拖拽)")]
    [SerializeField] private AudioManager        _audioManager;
    [SerializeField] private SaveManager         _saveManager;
    [SerializeField] private SceneService        _sceneService;
    [SerializeField] private DeathRespawnService _deathRespawnService;
    [SerializeField] private EventChannelRegistry _eventChannelRegistry;

    private void Awake()
    {
        ServiceLocator.Register<IAudioService>(_audioManager);
        ServiceLocator.Register<ISaveService>(_saveManager);
        ServiceLocator.Register<ISceneService>(_sceneService);
        ServiceLocator.Register<IDeathRespawnService>(_deathRespawnService);
        ServiceLocator.Register<IEventChannelRegistry>(_eventChannelRegistry);
    }
}

11.4 消费示例(替代旧 Singleton

// ❌ 旧写法(直接单例,测试不友好)
AudioManager.Instance.PlaySFX(clip);

// ✅ 新写法ServiceLocator可 Mock
ServiceLocator.Get<IAudioService>().PlaySFX(clip);

// ✅ 可选服务(如 PlatformManager 仅 Steam 平台存在)
ServiceLocator.GetOrDefault<IPlatformService>(NullPlatformService.Instance)
              .UnlockAchievement("ACH_FirstKill");

11.5 IAudioService 接口

// 路径: Assets/Scripts/Audio/IAudioService.cs
namespace BaseGames.Audio
{
    public interface IAudioService
    {
        void PlayBGM(AudioCueSO cue, float fadeInTime = 1.0f);
        void StopBGM(float fadeOutTime = 1.0f);
        void PlaySFX(AudioCueSO cue);
        void PlaySFXAtPosition(AudioCueSO cue, Vector2 position);
        void SetMixerVolume(string paramName, float linearVolume);   // 0..1 → dB
        void TransitionToSnapshot(string snapshotName, float transitionTime);
        AudioCueSO GetCurrentBGM();
    }

    /// <summary>静默实现,用于测试和 BGM 静默场景。</summary>
    public class NullAudioService : IAudioService
    {
        public static readonly NullAudioService Instance = new();
        public void PlayBGM(AudioCueSO c, float t = 1f) { }
        public void StopBGM(float t = 1f) { }
        public void PlaySFX(AudioCueSO c) { }
        public void PlaySFXAtPosition(AudioCueSO c, Vector2 p) { }
        public void SetMixerVolume(string n, float v) { }
        public void TransitionToSnapshot(string n, float t) { }
        public AudioCueSO GetCurrentBGM() => null;
    }
}

12. DeathRespawnService — 死亡/复活服务拆分

P1 优化:原 GameManager 同时管理状态机 + 死亡序列协程 + 场景切换,违反 SRP。
将死亡/复活流程提取为独立的 DeathRespawnServiceGameManager 只负责
监听事件 → 委托给对应服务 → 进行状态转换。

12.1 IDeathRespawnService 接口

// 路径: Assets/Scripts/Core/IDeathRespawnService.cs
namespace BaseGames.Core
{
    public interface IDeathRespawnService
    {
        /// <summary>玩家死亡时由 GameManager 调用,启动死亡演出流程。</summary>
        UniTask StartDeathSequenceAsync(CancellationToken ct = default);

        /// <summary>DeathScreen 确认按钮点击后调用,执行复活流程。</summary>
        UniTask StartRespawnAsync(CancellationToken ct = default);

        /// <summary>SteelSoul 模式HP 归零后直接清档并返回主菜单。</summary>
        UniTask StartGameOverAsync(CancellationToken ct = default);
    }
}

12.2 DeathRespawnService 实现

// 路径: Assets/Scripts/Core/DeathRespawnService.cs
namespace BaseGames.Core
{
    /// <summary>
    /// 死亡 / 复活流程的独立服务。
    /// 依赖ISceneService场景切换、ISaveService加载存档
    ///       GameStateMachine状态转换通知、LoadingOverlay淡入淡出
    /// 所有依赖由 ServiceLocator 在 Awake 时解析,不持有 GameManager 引用。
    /// </summary>
    public class DeathRespawnService : MonoBehaviour, IDeathRespawnService
    {
        [Header("Config")]
        [SerializeField] private float _deathAnimDuration   = 1.2f;   // 死亡动画时长
        [SerializeField] private float _deathScreenDelay    = 0.5f;   // 动画结束 → 死亡画面
        [SerializeField] private float _respawnFadeDuration = 0.4f;   // 淡出/淡入时长

        [Header("Event Channels - Raise")]
        [SerializeField] private VoidEventChannelSO _onRespawnStarted;
        [SerializeField] private VoidEventChannelSO _onRespawnCompleted;

        [Header("Event Channels - Listen")]
        [SerializeField] private VoidEventChannelSO _onDeathScreenConfirmed;

        // 内部等待 DeathScreen 用户确认
        private UniTaskCompletionSource _deathConfirmTcs;

        private void OnEnable()
            => _onDeathScreenConfirmed.OnEventRaised += HandleDeathScreenConfirmed;
        private void OnDisable()
            => _onDeathScreenConfirmed.OnEventRaised -= HandleDeathScreenConfirmed;
        private void HandleDeathScreenConfirmed()
            => _deathConfirmTcs?.TrySetResult();

        // ── IDeathRespawnService ──────────────────────────────────────
        public async UniTask StartDeathSequenceAsync(CancellationToken ct = default)
        {
            // 1. 等待死亡动画
            await UniTask.Delay(TimeSpan.FromSeconds(_deathAnimDuration), cancellationToken: ct);
            // 2. 等待 DeathScreen 延迟
            await UniTask.Delay(TimeSpan.FromSeconds(_deathScreenDelay), cancellationToken: ct);
            // 3. 等待玩家在死亡画面确认UI → EVT_DeathScreenConfirmed
            _deathConfirmTcs = new UniTaskCompletionSource();
            await _deathConfirmTcs.Task.AttachExternalCancellation(ct);
        }

        public async UniTask StartRespawnAsync(CancellationToken ct = default)
        {
            var sceneService = ServiceLocator.Get<ISceneService>();
            var saveService  = ServiceLocator.Get<ISaveService>();

            _onRespawnStarted.Raise();
            // 1. 淡出
            await ServiceLocator.Get<LoadingOverlay>().FadeOutAsync(_respawnFadeDuration, ct);
            // 2. 加载最后存档场景
            var saveData = await saveService.LoadCurrentSlotAsync(ct);
            await sceneService.LoadSceneAsync(new SceneLoadRequest
            {
                SceneName         = saveData.Player.Scene,
                EntryTransitionId = saveData.Meta.SavePointId,
                IsRespawn         = true,
            }, ct);
            // 3. 淡入
            await ServiceLocator.Get<LoadingOverlay>().FadeInAsync(_respawnFadeDuration, ct);
            _onRespawnCompleted.Raise();
        }

        public async UniTask StartGameOverAsync(CancellationToken ct = default)
        {
            // SteelSoul清档 → 返回主菜单
            await ServiceLocator.Get<ISaveService>().DeleteCurrentSlotAsync(ct);
            await ServiceLocator.Get<ISceneService>().LoadMainMenuAsync(ct);
        }
    }
}

12.3 重构后 GameManager 变化

// GameManager 精简为:事件监听 → 委托服务 → 状态转换
private async UniTaskVoid HandlePlayerDied()
{
    _stateMachine.TransitionTo(GameStates.Dead);
    await ServiceLocator.Get<IDeathRespawnService>().StartDeathSequenceAsync(_cts.Token);
    // DeathScreen 确认后触发复活
    _stateMachine.TransitionTo(GameStates.Loading);
    await ServiceLocator.Get<IDeathRespawnService>().StartRespawnAsync(_cts.Token);
    _stateMachine.TransitionTo(GameStates.Playing);
}

协程 → UniTask 迁移DeathRespawnService 全程使用 UniTask消除 IEnumerator 混用问题。
GameManager.Start() 改为 private async UniTaskVoid Start()
_cts = new CancellationTokenSource()OnDestroy().Cancel() + .Dispose()


13. SceneService — 场景管理服务拆分

P1 优化:原 GameManager.LoadSceneCoroutine 内嵌了场景加载、LoadingOverlay 控制、
Addressables 预热触发等逻辑。提取为独立的 SceneServiceGameManager 调用
ServiceLocator.Get<ISceneService>().LoadSceneAsync() 即可。

13.1 ISceneService 接口

// 路径: Assets/Scripts/Core/ISceneService.cs
namespace BaseGames.Core
{
    public interface ISceneService
    {
        /// <summary>通用场景加载(带 LoadingOverlay + Addressables 预热)。</summary>
        UniTask LoadSceneAsync(SceneLoadRequest request, CancellationToken ct = default);

        /// <summary>返回主菜单(清理所有 Additive 场景后加载 MainMenu。</summary>
        UniTask LoadMainMenuAsync(CancellationToken ct = default);

        /// <summary>当前已加载场景名称Persistent 除外)。</summary>
        string CurrentSceneName { get; }
    }
}

13.2 SceneService 实现

// 路径: Assets/Scripts/Core/SceneService.cs
[DefaultExecutionOrder(-900)]
public class SceneService : MonoBehaviour, ISceneService
{
    [Header("Config")]
    [SerializeField] private WarmupManifestSO _warmupManifest;  // 见 13_AssetPoolModule §12
    [SerializeField] private LoadingOverlay   _loadingOverlay;

    public string CurrentSceneName { get; private set; }

    public async UniTask LoadSceneAsync(SceneLoadRequest request, CancellationToken ct = default)
    {
        // 1. 淡出
        await _loadingOverlay.FadeOutAsync(0.3f, ct);

        // 2. 卸载当前场景(保留 Persistent
        if (!string.IsNullOrEmpty(CurrentSceneName))
            await Addressables.UnloadSceneAsync(CurrentSceneName).ToUniTask(ct);

        // 3. 加载新场景
        var handle = Addressables.LoadSceneAsync(request.SceneName,
            LoadSceneMode.Additive, activateOnLoad: false);
        await handle.ToUniTask(ct);

        // 4. Addressables 预热(见 13_AssetPoolModule §9
        await ServiceLocator.Get<GlobalObjectPool>()
            .WarmupFromManifestAsync(_warmupManifest, ct);

        // 5. 激活场景
        await handle.Result.ActivateAsync().ToUniTask(ct);
        CurrentSceneName = request.SceneName;

        // 6. 淡入
        await _loadingOverlay.FadeInAsync(0.3f, ct);
    }

    public async UniTask LoadMainMenuAsync(CancellationToken ct = default)
        => await LoadSceneAsync(new SceneLoadRequest
            { SceneName = AddressKeys.SceneMainMenu }, ct);
}