chore: initial commit
This commit is contained in:
967
Docs/Architecture/03_CoreModule.md
Normal file
967
Docs/Architecture/03_CoreModule.md
Normal file
@@ -0,0 +1,967 @@
|
||||
# 03 · Core 核心模块
|
||||
|
||||
> **命名空间** `BaseGames.Core`
|
||||
> **程序集** `BaseGames.Core`
|
||||
> **路径** `Assets/Scripts/Core/`
|
||||
> **依赖** `BaseGames.Core.Events`、`Newtonsoft.Json`
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [GameManager](#1-gamemanager)
|
||||
2. [GameState 枚举与合法转换表](#2-gamestate-枚举与合法转换表)
|
||||
3. [SceneLoader](#3-sceneloader)
|
||||
4. [SceneLoadRequest 数据结构](#4-sceneloadrequest-数据结构)
|
||||
5. [GlobalObjectPool(引用 13_AssetPoolModule)](#5-globalobjectpool)
|
||||
6. [SettingsManager](#6-settingsmanager)
|
||||
7. [GlobalSettingsSO](#7-globalsettingsso)
|
||||
8. [死亡复活流程(时序)](#8-死亡复活流程时序)
|
||||
9. [Boss 战切换流程(时序)](#9-boss-战切换流程时序)
|
||||
10. [初始化序列(ExecutionOrder)](#10-初始化序列)
|
||||
11. [ServiceLocator — 轻量依赖注入](#11-servicelocator--轻量依赖注入)
|
||||
12. [DeathRespawnService — 死亡/复活服务拆分](#12-deathrespawnservice--死亡复活服务拆分)
|
||||
13. [SceneService — 场景管理服务拆分](#13-sceneservice--场景管理服务拆分)
|
||||
|
||||
---
|
||||
|
||||
## 1. GameManager
|
||||
|
||||
```
|
||||
路径: Assets/Scripts/Core/GameManager.cs
|
||||
程序集: BaseGames.Core
|
||||
[DefaultExecutionOrder(-1000)]
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
```csharp
|
||||
[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;
|
||||
```
|
||||
|
||||
### 公开接口
|
||||
|
||||
```csharp
|
||||
// 状态管理
|
||||
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();
|
||||
```
|
||||
|
||||
### 私有/内部方法
|
||||
|
||||
```csharp
|
||||
// 生命周期
|
||||
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 — 运行时可注册状态标识
|
||||
|
||||
```csharp
|
||||
// 路径: 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 — 状态行为接口
|
||||
|
||||
```csharp
|
||||
// 路径: 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 — 轻量状态机核心
|
||||
|
||||
```csharp
|
||||
// 路径: 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 集成
|
||||
|
||||
```csharp
|
||||
// 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 内置状态示例
|
||||
|
||||
```csharp
|
||||
// 路径: 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`)的代码,将事件类型从 `GameState`(enum)替换为 `GameStateId`(struct)即可,API 形态不变。`GameStateId == GameStateId` 值比较性能与 enum 相同(int 比较)。
|
||||
|
||||
|
||||
|
||||
```
|
||||
路径: Assets/Scripts/Core/SceneLoader.cs
|
||||
程序集: BaseGames.Core
|
||||
```
|
||||
|
||||
### 职责
|
||||
|
||||
- 监听 `EVT_SceneLoadRequest` 频道,执行异步加载
|
||||
- 管理 Persistent 场景常驻(加法加载/卸载)
|
||||
- 加载完成后发布 `EVT_SceneLoaded`
|
||||
|
||||
### 字段
|
||||
|
||||
```csharp
|
||||
[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; // 当前已加载的房间场景名
|
||||
```
|
||||
|
||||
### 接口
|
||||
|
||||
```csharp
|
||||
// 由 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 数据结构
|
||||
|
||||
```csharp
|
||||
// 路径: Assets/Scripts/Core/SceneLoadRequest.cs
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
[System.Serializable]
|
||||
public struct SceneLoadRequest
|
||||
{
|
||||
public string SceneName; // Addressable 场景键(AddressKeys 常量)
|
||||
public string EntryTransitionId; // 玩家出生点 ID(RoomTransition 与之匹配)
|
||||
public bool ShowLoadingScreen; // 跨大区域时 true
|
||||
public bool IsRespawn; // 死亡复活时 true,不执行过渡动画
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. GlobalObjectPool
|
||||
|
||||
> ⚠️ **权威实现见 [`13_AssetPoolModule.md §3`](../Architecture/13_AssetPoolModule.md)**
|
||||
> 类名为 `GlobalObjectPool`(命名空间 `BaseGames.Core`)。
|
||||
> 本模块不重复定义,以下仅列出与 CoreModule 相关的调用入口摘要。
|
||||
|
||||
```csharp
|
||||
// 使用方式(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 调用)
|
||||
|
||||
### 字段
|
||||
|
||||
```csharp
|
||||
[SerializeField] private GlobalSettingsSO _defaultSettings;
|
||||
[SerializeField] private FloatEventChannelSO _onMasterVolumeChanged; // 同步 HUD
|
||||
|
||||
private GlobalSettingsData _current; // 运行时值(从文件读取)
|
||||
```
|
||||
|
||||
### 接口
|
||||
|
||||
```csharp
|
||||
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
|
||||
```
|
||||
|
||||
```csharp
|
||||
[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 BGM(AudioMixer 快照)
|
||||
← [UIManager] 显示 Boss 血量条
|
||||
← [CinemachineCamera] 切换为 BossCamera 虚拟摄像机
|
||||
|
||||
[BossOrchestrator] Boss 死亡
|
||||
→ Raise EVT_BossFightEnded(victory: true)
|
||||
|
||||
[GameManager] HandleBossFightEnded(true)
|
||||
→ TransitionTo(GameState.Gameplay)
|
||||
← [AudioManager] 恢复常规 BGM
|
||||
← [UIManager] 隐藏 Boss 血量条
|
||||
→ 触发剧情 / 过场(如有)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 初始化序列
|
||||
|
||||
```csharp
|
||||
// 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.Instance`、`EventChannelRegistry.Instance`、`SaveManager` 等均为静态单例,
|
||||
> 在多场景 Play Mode 快速进入(Direct Play from Room scene)时若 Persistent 场景未加载,
|
||||
> `Instance` 为 null 导致崩溃;测试时也无法替换为 Mock 实现。
|
||||
> 引入 `ServiceLocator` 替代所有硬编码 `Instance`,保留 Inspector 序列化注入路径(无需引入 Zenject/VContainer 等外部 DI 框架)。
|
||||
|
||||
### 11.1 ServiceLocator 核心
|
||||
|
||||
```csharp
|
||||
// 路径: Assets/Scripts/Core/ServiceLocator.cs
|
||||
namespace BaseGames.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 轻量服务定位器。
|
||||
/// - 通过类型键注册/查找服务,支持接口类型注册(依赖倒置)
|
||||
/// - 在 Persistent 场景 Awake(ExecutionOrder -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>
|
||||
/// 查找服务。未注册时返回 default(T为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 服务注册器
|
||||
|
||||
```csharp
|
||||
// 路径: 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)
|
||||
|
||||
```csharp
|
||||
// ❌ 旧写法(直接单例,测试不友好)
|
||||
AudioManager.Instance.PlaySFX(clip);
|
||||
|
||||
// ✅ 新写法(ServiceLocator,可 Mock)
|
||||
ServiceLocator.Get<IAudioService>().PlaySFX(clip);
|
||||
|
||||
// ✅ 可选服务(如 PlatformManager 仅 Steam 平台存在)
|
||||
ServiceLocator.GetOrDefault<IPlatformService>(NullPlatformService.Instance)
|
||||
.UnlockAchievement("ACH_FirstKill");
|
||||
```
|
||||
|
||||
### 11.5 IAudioService 接口
|
||||
|
||||
```csharp
|
||||
// 路径: 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。
|
||||
> 将死亡/复活流程提取为独立的 `DeathRespawnService`,GameManager 只负责
|
||||
> 监听事件 → 委托给对应服务 → 进行状态转换。
|
||||
|
||||
### 12.1 IDeathRespawnService 接口
|
||||
|
||||
```csharp
|
||||
// 路径: 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 实现
|
||||
|
||||
```csharp
|
||||
// 路径: 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 变化
|
||||
|
||||
```csharp
|
||||
// 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 预热触发等逻辑。提取为独立的 `SceneService`,GameManager 调用
|
||||
> `ServiceLocator.Get<ISceneService>().LoadSceneAsync()` 即可。
|
||||
|
||||
### 13.1 ISceneService 接口
|
||||
|
||||
```csharp
|
||||
// 路径: 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 实现
|
||||
|
||||
```csharp
|
||||
// 路径: 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);
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user