Files
zeling_v2/Docs/Plan/01_Phase0_Foundation.md
2026-05-08 11:04:00 +08:00

754 lines
32 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 0 · 项目基础设施
> **周期**1 周
> **前置条件**Unity 2022.3 LTS 项目已创建,以下 Package 已导入Cinemachine 3、New Input System、2D Pixel Perfect、Addressables、Newtonsoft Json、Kybernetik Animancer Pro、PathBerserker2d、Behavior Designer、FeelMore Mountains
> **产出物**:编译无错;能运行 Persistent 场景;能读写 JSON 存档asmdef 依赖方向验证通过
---
## 目录
1. [实施顺序总览](#1-实施顺序总览)
2. [Day 1文件夹与 asmdef 骨架](#2-day-1文件夹与-asmdef-骨架)
3. [Day 2SO 事件系统](#3-day-2so-事件系统)
4. [Day 3Core 模块](#4-day-3core-模块)
5. [Day 4Addressables 与对象池骨架](#5-day-4addressables-与对象池骨架)
6. [Day 5SaveData 骨架 + Persistent 场景验证](#6-day-5savedata-骨架--persistent-场景验证)
7. [完成标准检查清单](#7-完成标准检查清单)
---
## 1. 实施顺序总览
```
Day 1: 文件夹结构 → asmdef 文件 → 依赖验证
Day 2: BaseEventChannelSO 基类 → 所有具体频道类型 → Data/Events/ 资产
Day 3: GameState + GameManager 骨架 → SceneLoader → SettingsManager + GlobalSettingsSO
Day 4: AddressKeys → AssetLoader → GlobalObjectPool + PooledObject
Day 5: SaveData C# 结构 → SaveManager 骨架 → Persistent 场景组装 → 全链路验证
```
**关键原则**每步完成后确认编译无错再继续。asmdef 创建顺序遵循依赖图(无依赖的先创建)。
---
## 2. Day 1文件夹与 asmdef 骨架
### 2.1 创建文件夹结构
`01_ProjectStructure §1``Assets/` 下创建以下文件夹Unity 中不存在同名 .meta 则空文件夹不提交,用 `.gitkeep` 占位):
```
Assets/
├── Scripts/
│ ├── Core/
│ │ ├── Events/
│ │ └── Save/
│ ├── Input/
│ ├── Camera/
│ ├── Player/
│ │ └── States/
│ ├── Combat/
│ │ └── StatusEffects/
│ ├── Parry/
│ ├── Enemies/
│ │ ├── AI/
│ │ ├── Boss/
│ │ │ └── Patterns/
│ │ └── Navigation/
│ ├── Feedback/
│ ├── World/
│ │ ├── Map/
│ │ └── Shop/
│ ├── UI/
│ ├── Audio/
│ ├── Progression/
│ ├── Dialogue/
│ ├── Equipment/
│ ├── Cutscene/
│ ├── Animation/
│ ├── Spells/
│ ├── Localization/
│ ├── Tutorial/
│ ├── Platform/
│ └── Editor/
├── Data/
│ ├── Events/
│ │ ├── Core/
│ │ ├── Player/
│ │ ├── Combat/
│ │ ├── World/
│ │ └── UI/
│ ├── Player/
│ ├── Combat/
│ ├── Enemies/
│ ├── Progression/
│ ├── Audio/
│ ├── World/
│ ├── UI/
│ └── Settings/
├── Prefabs/
│ ├── Player/
│ ├── Enemies/
│ ├── World/
│ ├── UI/
│ ├── Combat/
│ ├── Effects/
│ └── Persistent/
└── Scenes/
```
### 2.2 创建 Assembly Definition 文件
**创建顺序(依赖图自底向上)**
| 顺序 | 文件路径 | Assembly 名 | 引用 |
|------|---------|-------------|------|
| 1 | `Scripts/Core/Events/BaseGames.Core.Events.asmdef` | `BaseGames.Core.Events` | 无 |
| 2 | `Scripts/Core/Save/BaseGames.Core.Save.asmdef` | `BaseGames.Core.Save` | Core.Events, Newtonsoft.Json |
| 3 | `Scripts/Core/BaseGames.Core.asmdef` | `BaseGames.Core` | Core.Events, Core.Save |
| 4 | `Scripts/Input/BaseGames.Input.asmdef` | `BaseGames.Input` | Core.Events, Unity.InputSystem |
| 5 | `Scripts/Camera/BaseGames.Camera.asmdef` | `BaseGames.Camera` | Core.Events, Cinemachine |
| 6 | `Scripts/Combat/BaseGames.Combat.asmdef` | `BaseGames.Combat` | Core.Events |
| 7 | `Scripts/Combat/StatusEffects/BaseGames.Combat.StatusEffects.asmdef` | `BaseGames.Combat.StatusEffects` | Combat |
| 8 | `Scripts/Parry/BaseGames.Parry.asmdef` | `BaseGames.Parry` | Combat |
| 9 | `Scripts/Feedback/BaseGames.Feedback.asmdef` | `BaseGames.Feedback` | Core.Events, Combat |
| 10 | `Scripts/Player/BaseGames.Player.asmdef` | `BaseGames.Player` | Core, Input, Combat, Parry, Feedback, Animancer |
| 11 | `Scripts/Player/States/BaseGames.Player.States.asmdef` | `BaseGames.Player.States` | Player |
| 12 | `Scripts/Enemies/BaseGames.Enemies.asmdef` | `BaseGames.Enemies` | Core, Combat, Feedback, Animancer |
| 13 | `Scripts/Enemies/AI/BaseGames.Enemies.AI.asmdef` | `BaseGames.Enemies.AI` | Enemies, BehaviorDesigner.Runtime |
| 14 | `Scripts/Enemies/Navigation/BaseGames.Enemies.Navigation.asmdef` | `BaseGames.Enemies.Navigation` | Enemies, PathBerserker2d |
| 15 | `Scripts/World/BaseGames.World.asmdef` | `BaseGames.World` | Core, Combat, Animancer |
| 16 | `Scripts/UI/BaseGames.UI.asmdef` | `BaseGames.UI` | Core.Events |
| 17 | `Scripts/Audio/BaseGames.Audio.asmdef` | `BaseGames.Audio` | Core.Events |
| 18 | `Scripts/Progression/BaseGames.Progression.asmdef` | `BaseGames.Progression` | Core, Player |
| 19 | `Scripts/Equipment/BaseGames.Equipment.asmdef` | `BaseGames.Equipment` | Core.Events, Player |
| 20 | `Scripts/Spells/BaseGames.Spells.asmdef` | `BaseGames.Spells` | Core.Events, Player, Combat |
| 21 | `Scripts/World/Map/BaseGames.World.Map.asmdef` | `BaseGames.World.Map` | World, Core.Save |
| 22 | `Scripts/World/Shop/BaseGames.World.Shop.asmdef` | `BaseGames.World.Shop` | World, Core.Events |
| 23 | `Scripts/Dialogue/BaseGames.Dialogue.asmdef` | `BaseGames.Dialogue` | Core.Events |
| 24 | `Scripts/Cutscene/BaseGames.Cutscene.asmdef` | `BaseGames.Cutscene` | Core.Events, Dialogue |
| 25 | `Scripts/Animation/BaseGames.Animation.asmdef` | `BaseGames.Animation` | Core.Events, Animancer |
| 26 | `Scripts/Enemies/Boss/Patterns/BaseGames.Enemies.Boss.Patterns.asmdef` | `BaseGames.Enemies.Boss.Patterns` | Enemies, Combat |
| 27 | `Scripts/Localization/BaseGames.Localization.asmdef` | `BaseGames.Localization` | Core.Events |
| 28 | `Scripts/Platform/BaseGames.Platform.asmdef` | `BaseGames.Platform` | Core.Events |
| 29 | `Scripts/Tutorial/BaseGames.Tutorial.asmdef` | `BaseGames.Tutorial` | Core.Events, World |
| 30 | `Scripts/Editor/BaseGames.Editor.asmdef` | `BaseGames.Editor` | 全部Editor Only`includePlatforms: ["Editor"]` |
### 2.3 放置占位脚本
每个 asmdef 目录放一个 `_Placeholder.cs`(仅 namespace 声明),确保 Unity 不报"asmdef 无脚本"警告:
```csharp
// _Placeholder.cs
namespace BaseGames.Core { } // 各目录对应 namespace
```
### 2.4 验证依赖方向
在 Unity Editor 打开 **Edit → Project Settings → Player → Other Settings** 确认无编译错误。
确认底层 asmdef 的引用列表里**没有**高层 asmdef 名称。
---
## 3. Day 2SO 事件系统
**参考文档**`02_EventSystem.md`
### 3.1 实现基类(`Assets/Scripts/Core/Events/`
按顺序创建以下文件:
```
BaseEventChannelSO.cs ← 泛型基类 + VoidBaseEventChannelSO
VoidEventChannelSO.cs ← [CreateAssetMenu]
BoolEventChannelSO.cs
IntEventChannelSO.cs
FloatEventChannelSO.cs
StringEventChannelSO.cs
Vector2EventChannelSO.cs
TransformEventChannelSO.cs
GameState.cs ← 枚举(顺便在此处定义)
GameStateEventChannelSO.cs
SceneLoadRequest.cs ← structsceneName + entryId + loadingScreen
SceneLoadRequestEventChannelSO.cs
DifficultyLevel.cs ← 枚举Easy/Normal/Hard/SteelSoul
DifficultyChangedEventChannel.cs ← Phase 2 难度系统用(按架构命名,非 DifficultyEventChannelSO
DamageInfoEventChannelSO.cs ← Combat 伤害事件EVT_DamageDealt
HitInfo.cs ← structDamageInfo + HitPoint Vector3
HitConfirmedEventChannelSO.cs ← VFX 命中事件EVT_HitConfirmed洛载类型 HitInfo
ShopPurchaseEvent.cs ← struct 架构 15_MapShopModule §2.3ShopPurchaseEvent { Item, Price };非旧版 ShopTransactionEvent
ShopPurchaseEventChannelSO.cs ← 商店购买事件EVT_ItemPurchased 架构 15 §2.3;非旧版 EVT_ShopTransactionCompleted
DialogueEventChannelSO.cs ← 对话请求事件EVT_DialogueStartRequest payload 为 DialogueDataSO SO 引用,非 struct无 DialogueRequest 类,架构 02 §3
LiquidEventChannelSO.cs ← 液体进出事件EVT_LiquidEntered / EVT_LiquidExited
AbilityType.cs ← `[Flags] uint` 枚举WallCling/WallJump/Dash/... ⚠️ 位于 Scripts/Player/(程序集 BaseGames.Player非 Scripts/Progression/(架构 09_ProgressionModule §1
AbilityTypeEventChannelSO.cs ← 能力解锁事件(仅内部保留;⚠️ EVT_AbilityUnlocked 实际使用 StringEventChannelSO架构 02 §4
// ── Quest 事件频道22_QuestChallengeModule §5──────────────────────────────
QuestState.cs ← 枚举Unavailable/Available/Active/Completed/Failed
QuestStateChangedEvent.cs ← struct { string QuestId; QuestState State; }
QuestStateChangedEventChannel.cs ← 任务状态变更(⚠️ 按架构命名,无 SO 后缀)
QuestObjectiveEventChannelSO.cs ← 任务目标进度payload: QuestObjectiveEvent{QuestId, ObjectiveId, Progress, Required},架构 02 §3
StatusEffectEventChannelSO.cs ← 状态效果施加/过期payload: StatusEffectType 枚举,架构 02 §3
// ── Boss 技能事件频道23_BossSkillModule §11─────────────────────────────
BossSkillEventChannelSO.cs ← Boss 技能开始/结束payload: (string bossId, string skillId)
BossPhaseEventChannelSO.cs ← Boss 阶段切换payload: (string bossId, int phase)
// ── 可访问性事件频道16_SupportingModules §AccessibilityManager──────────
ColorblindMode.cs ← 枚举None/Deuteranopia/Protanopia/Tritanopia
ColorblindModeEventChannelSO.cs ← 色觉模式切换事件EVT_ColorblindModeChanged
// ── 导航标记事件频道21_LiquidPuzzleModule §14─────────────────────────────
WorldMarkerEventChannelSO.cs ← 导航标记激活/失活EVT_WorldMarkerActivated / EVT_WorldMarkerDeactivated
```
### 3.2 在 `Assets/Data/Events/` 下预建全局 SO 资产
命名规范:`EVT_{EventName}.asset`(不含模块前缀中的下划线,与架构 `02_EventSystem` 一致)
**Core 事件**`Data/Events/Core/`
| 资产名 | 类型 |
|--------|------|
| `EVT_PlayerDied` | `VoidEventChannelSO` |
| `EVT_PlayerRespawned` | `VoidEventChannelSO` |
| `EVT_PauseRequested` | `VoidEventChannelSO` |
| `EVT_DeathScreenConfirmed` | `VoidEventChannelSO` |
| `EVT_GameStateChanged` | `GameStateEventChannelSO` |
| `EVT_SceneLoadRequest` | `SceneLoadRequestEventChannelSO` |
| `EVT_SceneLoaded` | `StringEventChannelSO` |
| `EVT_SavePointActivated` | `StringEventChannelSO` |
| `EVT_DifficultyChanged` | `DifficultyChangedEventChannel` |
**UI 事件**`Data/Events/UI/`
| 资产名 | 类型 |
|--------|------|
| `EVT_FadeIn` | `VoidEventChannelSO` |
| `EVT_FadeOut` | `VoidEventChannelSO` |
| `EVT_FastTravelOpen` | `VoidEventChannelSO` |
| `EVT_ShopOpened` | `StringEventChannelSO` |
| `EVT_ShopClosed` | `VoidEventChannelSO` |
| `EVT_MapOpen` | `VoidEventChannelSO` |
| `EVT_ShowPanel` | `StringEventChannelSO`panelId |
| `EVT_HidePanel` | `StringEventChannelSO`panelId |
**Player 事件**`Data/Events/Player/`
| 资产名 | 类型 |
|--------|------|
| `EVT_HPChanged` | `IntEventChannelSO` |
| `EVT_MaxHPChanged` | `IntEventChannelSO` |
| `EVT_SoulPowerChanged` | `IntEventChannelSO` |
| `EVT_SpiritPowerChanged` | `IntEventChannelSO` |
| `EVT_SpringChargesChanged` | `IntEventChannelSO` |
| `EVT_GeoChanged` | `IntEventChannelSO` |
| `EVT_PlayerFormChanged` | `IntEventChannelSO` |
| `EVT_AbilityUnlocked` | `StringEventChannelSO`abilityId 非 AbilityTypeEventChannelSO架构 02 §4 |
| `EVT_SkillSetChanged` | `VoidEventChannelSO`(发布: FormController订阅: SkillHUD |
| `EVT_ShieldHPChanged` | `IntEventChannelSO`(发布: ShieldComponent订阅: HUDController |
| `EVT_ShieldBroken` | `VoidEventChannelSO` |
| `EVT_ShieldRestored` | `VoidEventChannelSO` |
**Combat 事件**`Data/Events/Combat/`
| 资产名 | 类型 |
|--------|------|
| `EVT_EnemyDied` | `TransformEventChannelSO` |
| `EVT_DamageDealt` | `DamageInfoEventChannelSO` |
| `EVT_HitConfirmed` | `HitConfirmedEventChannelSO` |
| `EVT_ParrySuccess` | `VoidEventChannelSO` |
| `EVT_BossFightStarted` | `StringEventChannelSO`bossId 属战斗事件,架构 02 §4非 Core 事件) |
| `EVT_BossFightEnded` | `BoolEventChannelSO`(⚠️ 属战斗事件,架构 02 §4非 Core 事件) |
| `EVT_BossFightToggled` | `BoolEventChannelSO`true=开始false=结束) |
| `EVT_BossHPChanged` | `IntEventChannelSO` |
| `EVT_BossNameSet` | `StringEventChannelSO`bossName |
| `EVT_BossHPMaxSet` | `IntEventChannelSO` |
| `EVT_NailClash` | `VoidEventChannelSO` |
| `EVT_StatusEffectApplied` | `StatusEffectEventChannelSO` |
| `EVT_StatusEffectExpired` | `StatusEffectEventChannelSO` |
| `EVT_BossSkillStarted` | `BossSkillEventChannelSO` |
| `EVT_BossSkillEnded` | `BossSkillEventChannelSO` |
| `EVT_BossVulnerabilityWindowOpened` | `StringEventChannelSO` |
| `EVT_BossPhaseChanged` | `BossPhaseEventChannelSO` |
**World 事件**`Data/Events/World/`
| 资产名 | 类型 |
|--------|------|
| `EVT_CollectiblePickup` | `StringEventChannelSO` |
| `EVT_RoomEntered` | `StringEventChannelSO` |
| `EVT_RoomTransitionRequest` | `SceneLoadRequestEventChannelSO` |
| `EVT_MapUpdated` | `StringEventChannelSO` |
| `EVT_LiquidEntered` | `LiquidEventChannelSO` |
| `EVT_LiquidExited` | `LiquidEventChannelSO` |
| `EVT_DrownProgress` | `FloatEventChannelSO`01 进度) |
| `EVT_PlayerDrowned` | `VoidEventChannelSO` |
| `EVT_GeoRecovered` | `StringEventChannelSO` |
| `EVT_ShowInteractPrompt` | `StringEventChannelSO` |
| `EVT_HideInteractPrompt` | `VoidEventChannelSO` |
| `EVT_WorldMarkerActivated` | `WorldMarkerEventChannelSO` |
| `EVT_WorldMarkerDeactivated` | `WorldMarkerEventChannelSO` |
| `EVT_ItemPurchased` | `ShopPurchaseEventChannelSO` |
**Audio 事件**`Data/Events/Audio/`
| 资产名 | 类型 |
|--------|------|
| `EVT_PlayBGM` | `StringEventChannelSO` |
| `EVT_StopBGM` | `VoidEventChannelSO` |
| `EVT_PlaySFX` | `StringEventChannelSO` |
| `EVT_RegionEntered` | `StringEventChannelSO` |
**Dialogue 事件**`Data/Events/Dialogue/`
| 资产名 | 类型 |
|--------|------|
| `EVT_DialogueStartRequest` | `DialogueEventChannelSO` |
| `EVT_DialogueStarted` | `VoidEventChannelSO` |
| `EVT_DialogueEnded` | `VoidEventChannelSO` |
| `EVT_NpcDialogueCompleted` | `StringEventChannelSO` |
| `EVT_CutsceneStarted` | `VoidEventChannelSO` |
| `EVT_CutsceneEnded` | `VoidEventChannelSO` |
**Quest 事件**`Data/Events/Quest/`
| 资产名 | 类型 |
|--------|------|
| `EVT_QuestStarted` | `StringEventChannelSO`questId 架构命名,对应 QuestManager.StartQuest(),架构 02 §4 |
| `EVT_QuestCompleted` | `StringEventChannelSO`questId |
| `EVT_QuestFailed` | `StringEventChannelSO`questId |
| `EVT_ObjectiveUpdated` | `QuestObjectiveEventChannelSO`QuestObjectiveEvent |
| `EVT_QuestStateChanged` | `QuestStateChangedEventChannel` |
| `EVT_ChallengeCompleted` | `StringEventChannelSO` |
| `EVT_ChallengeFailed` | `StringEventChannelSO` |
**事件链 / EventChain 事件**`Data/Events/EventChain/`
| 资产名 | 类型 |
|--------|------|
| `EVT_ChainCompleted` | `StringEventChannelSO` |
| `EVT_DoorOpened` | `StringEventChannelSO` |
| `EVT_FlagChanged` | `StringEventChannelSO` |
**Save 事件**`Data/Events/Save/`
| 资产名 | 类型 |
|--------|------|
| `EVT_SaveIndicatorVisible` | `BoolEventChannelSO` |
**Accessibility 事件**`Data/Events/Accessibility/`
| 资产名 | 类型 |
|--------|------|
| `EVT_AchievementUnlocked` | `StringEventChannelSO` |
| `EVT_SoftlockDetected` | `VoidEventChannelSO` |
| `EVT_ColorblindModeChanged` | `ColorblindModeEventChannelSO` |
| `EVT_SubtitlesToggled` | `BoolEventChannelSO` |
| `EVT_HighContrastToggled` | `BoolEventChannelSO` |
> **说明**DamageInfoEventChannelSO、HitConfirmedEventChannelSO 等具体 channel 类型的脚本在 Day 2 §3.1 中一并创建;对应 SO 资产在创建完类型后立即在 Inspector 中创建。所有事件频道资产后续在各模块实现时如需补充,按此规范追加。
### 3.3 验证
在 Editor 中创建一个临时 `TestEventChannel.cs`,订阅 `EVT_PlayerDied.OnEventRaised` 并打印日志,在 Inspector 点击"Raise"按钮(需在 `BaseEventChannelSO` Editor 脚本里添加测试按钮)确认事件触发。
---
## 4. Day 3Core 模块
**参考文档**`03_CoreModule.md`
### 4.1 实现文件列表(`Assets/Scripts/Core/`
```
GameStateId.cs ← struct替代旧 enum GameState可扩展的状态 ID
IGameState.cs ← 状态接口OnEnter / OnExit / Tick
GameStateMachine.cs ← 驱动所有 IGameState 切换的状态机
GameStates.cs ← 静态工厂:提供 8 个内置状态实例MainMenu/Gameplay/Paused/BossFight/Cutscene/Loading/Dead/GameOver
IGameStateFactory.cs ← DLC/扩展工厂接口(注册自定义状态)
GameManager.cs ← 字段 + 接口签名Awake/Start 骨架;内嵌 GameStateMachine
GameStateEventChannelSO.cs ← payload 改为 GameStateId 非旧枚举)
SceneLoader.cs ← LoadAsync 骨架(监听 EVT_SceneLoadRequest
SettingsManager.cs ← Load/Save 设置
GlobalSettingsSO.cs ← SO 数据类
// ── 服务层骨架03_CoreModule §11-13───────────────────────────────────────
ServiceLocator.cs ← 静态类Register/Get/GetOrDefault/OverrideForTest/Reset
GameServiceRegistrar.cs ← MonoBehaviourExecutionOrder -2000DontDestroyOnLoad
IAudioService.cs ← 音频服务接口
ISaveService.cs ← 存档服务接口
ISceneService.cs ← 场景加载服务接口UniTask-based
IDeathRespawnService.cs ← 死亡/重生服务接口UniTask-based
IEventChannelRegistry.cs ← SO 事件频道查找接口
NullAudioService.cs ← IAudioService 的空实现(测试兜底)
DeathRespawnService.cs ← IDeathRespawnService 骨架Phase 1 实现完整逻辑)
SceneService.cs ← ISceneService 骨架(封装 SceneLoaderPhase 1 完整实现)
```
> **⚠️ 架构升级**`GameState` 枚举已全面替换为 `GameStateId` struct + `IGameState` 接口体系(架构 03_CoreModule §2
> 旧写法 `TransitionTo(GameState.Gameplay)` 改为 `TransitionTo(GameStates.Gameplay)`
> 订阅方将收到 `GameStateId` 而非枚举值。
### 4.2 GameManager 实现优先级
Day 3 只实现**骨架部分**,不实现完整死亡/复活流程Phase 1 完成):
```csharp
// Day 3 实现范围
// GameStateId 是 struct不是 enum通过 GameStates 静态类获取内置实例
// IGameState 接口void OnEnter(); void OnExit(); void Tick(float dt);
// GameStateMachine 包含 TransitionTo(IGameState next)GameManager 内嵌实例
void Awake()
{
// 1. 单例检查DontDestroyOnLoad 由 Persistent 场景保证)
// 2. 订阅事件频道6个监听
// 3. _stateMachine.TransitionTo(GameStates.MainMenu) — 占位
Debug.Log($"[GameManager] Awake v{Application.version}");
}
// 占位实现(不报错即可)
public void TransitionTo(IGameState newState) { _stateMachine.TransitionTo(newState); }
public void Pause() { TransitionTo(GameStates.Paused); }
public void Resume() { TransitionTo(GameStates.Gameplay); }
```
### 4.3 SceneLoader 实现
```csharp
// Day 3 实现范围
// 订阅 EVT_SceneLoadRequest执行 Addressables.LoadSceneAsync
// 加载完成后发布 EVT_SceneLoaded
// _currentRoomScene 记录当前场景名,用于 Unload
```
### 4.4 GlobalSettingsSO
```csharp
// ⚠️ 字段名、menuName、结构均必须与架构 03_CoreModule §7 一致
[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;
}
```
`SettingsManager.Awake()``PlayerPrefs` 恢复设置到 `GlobalSettingsData``Apply()` 方法将数据应用到 Unity `QualitySettings`/`Screen`/`PlayerPrefs`
---
## 5. Day 4Addressables 与对象池骨架
**参考文档**`13_AssetPoolModule.md`
### 5.1 AddressKeys
```csharp
// Assets/Scripts/Core/Assets/AddressKeys.cs
// ⚠️ 命名规则camelCase无下划线分隔字符串值保持原样架构 13_AssetPoolModule §1 patch
public static class AddressKeys
{
// Scenes
public const string ScenePersistent = "Scene_Persistent"; // ⚠️ 值为 "Scene_Persistent"(非 "Persistent"
public const string SceneMainMenu = "Scene_MainMenu";
public const string SceneTestRoom = "Scene_TestRoom"; // Phase 1 测试房间
// Player
public const string PrefabPlayer = "PLY_Player"; // ⚠️ camelCase非 Prefab_Player
// 其余常量在各 Phase 按需追加(统一 camelCase 命名规则)
}
```
### 5.2 AssetLoader
```csharp
// Assets/Scripts/Core/Assets/AssetLoader.cs
// ⚠️ 架构使用标准 Task非 UniTask见 13_AssetPoolModule §5
public static class AssetLoader
{
// 异步加载单个资产(带缓存)
public static async Task<T> LoadAsync<T>(string addressKey) where T : UnityEngine.Object;
// 释放(减引用计数)
public static void Release(string addressKey);
// 释放全部缓存
public static void ReleaseAll();
}
```
### 5.3 GlobalObjectPool
```csharp
// Assets/Scripts/Core/Pool/GlobalObjectPool.cs
// ⚠️ 最终 API 见 13_AssetPoolModule §3Phase 0 仅建骨架,不实现全量
[DefaultExecutionOrder(-800)]
public class GlobalObjectPool : MonoBehaviour
{
// 预热:按 _warmupConfigs 配置批量实例化所有条目(无参数,以 Task 返回)
public async Task WarmupAsync();
// 获取对象(正式操作: Spawn
public T Spawn<T>(string addressKey, Vector3 position, Quaternion rotation) where T : Component;
// 归还对象(正式操作: Despawn
public void Despawn(string addressKey, GameObject instance);
}
```
### 5.3.1 WarmupManifestSOPhase 1 优化,架构 13 §12
> Phase 0 仅建骨架Phase 1 Vertical Slice 阶段完善 `SceneService.LoadSceneAsync()` 时一并实现。
```csharp
// Assets/Scripts/Core/Pool/WarmupManifestSO.cs
[CreateAssetMenu(menuName = "Core/Pool/Warmup Manifest")]
public class WarmupManifestSO : ScriptableObject
{
[Serializable]
public struct WarmupEntry
{
public string AddressKey; // AddressKeys 常量
public int InitialCount; // 预热实例数
public WarmupCategory Category;
}
public enum WarmupCategory { Enemy = 0, Projectile = 1, VFX = 2, UI = 3, Other = 99 }
public WarmupEntry[] Entries;
[Range(1, 20)] public int InstancesPerFrame = 5; // 每帧最多实例化数量(防卡顿)
}
// GlobalObjectPool 新增方法Phase 1 补充):
// public async UniTask WarmupFromManifestAsync(WarmupManifestSO manifest, CancellationToken ct)
// → 分帧预热:每帧最多实例化 manifest.InstancesPerFrame 个await UniTask.Yield() 让出帧
// SceneService.LoadSceneAsync() 调用:
// await GlobalObjectPool.Instance.WarmupFromManifestAsync(_warmupManifest, ct);
// 资产路径Assets/Data/Pool/Warmup/Global_Warmup.asset、{SceneName}_Warmup.asset 等
```
### 5.4 PooledObject
```csharp
// Assets/Scripts/Core/Pool/PooledObject.cs
// ⚠️ 完整实现(对齐架构 13_AssetPoolModule §4Phase 0 先创建此完整版本,后续直接使用
public class PooledObject : MonoBehaviour
{
public string AddressKey { get; private set; } // ⚠️ 属性而非公共字段(架构 13 §4
private GlobalObjectPool _pool;
// 由 GlobalObjectPool.SpawnInternal 调用,注入 key 和 pool 引用
public void Setup(string key, GlobalObjectPool pool)
{
AddressKey = key;
_pool = pool;
}
// 子类可覆盖(从池中取出时调用)
public virtual void OnSpawn() { }
// 子类可覆盖(归还到池时调用)
public virtual void OnDespawn(){ }
// 便利方法:自归还
public void ReturnToPool() => _pool?.Despawn(AddressKey, gameObject);
// 延迟归还(定时销毁型 VFX / 弹射物常用)
public void ReturnToPoolDelayed(float delay) => StartCoroutine(DelayedReturn(delay));
private IEnumerator DelayedReturn(float delay)
{
yield return new WaitForSeconds(delay);
ReturnToPool();
}
}
```
### 5.5 AssetReleaseTracker
```csharp
// Assets/Scripts/Core/Assets/AssetReleaseTracker.cs
// ⚠️ 完整实现对齐架构 13_AssetPoolModule §8事件驱动订阅 SceneLoadRequestEventChannelSO
// ⚠️ 不使用 RegisterForScene/ReleaseScene 显式注册 APISceneLoader 不主动调用本类)
public class AssetReleaseTracker : MonoBehaviour
{
[Header("Event Channels")]
[SerializeField] private SceneLoadRequestEventChannelSO _onSceneLoadRequest;
private string _lastLoadedScene;
private void OnEnable()
=> _onSceneLoadRequest.OnEventRaised += OnSceneLoadRequested;
private void OnDisable()
=> _onSceneLoadRequest.OnEventRaised -= OnSceneLoadRequested;
private void OnSceneLoadRequested(SceneLoadRequest req)
{
if (!string.IsNullOrEmpty(_lastLoadedScene))
{
// 清除旧场景的对象池(⚠️ GlobalObjectPool.ClearPool 方法存在,架构 13_AssetPoolModule §3
GlobalObjectPool.Instance.ClearPool(AddressKeys.PrefabEnemyGrunt);
// ... 其他场景对象(按需追加)
}
_lastLoadedScene = req.SceneName;
}
}
```
### 5.6 Editor 验证工具
`Assets/Scripts/Editor/AddressKeysValidator.cs` 创建菜单项 `Tools/Validate AddressKeys`
- 反射读取 `AddressKeys` 所有 `const string`
- 对照 `Addressables.ResourceLocators` 验证每个 key 是否存在
- 不匹配的 key 输出 Warning
---
## 6. Day 5SaveData 骨架 + Persistent 场景验证
**参考文档**`12_SaveModule.md §1-4`
### 6.1 SaveData C# 数据结构
创建 `Assets/Scripts/Core/Save/` 下所有数据类(完整结构):
```
SaveData.cs ← 顶层 + JsonExtensionData
SaveMeta.cs ← 版本号、难度、游戏时间
PlayerSaveData.cs ← HP、位置、灵力、形态等
EquipmentSaveData.cs ← 护符相关(⚠️ 工具槽数据在独立的 ToolsSaveData见架构 12_SaveModule §2
WorldSaveData.cs ← 房间状态、已破坏地形、已触发机关
MapSaveData.cs ← 已探索房间列表
QuestSaveData.cs ← 任务进度
AchievementSaveData.cs
ToolsSaveData.cs
StatsSaveData.cs
DeathShadeSaveData.cs
ChallengeRoomsSaveData.cs ← 挑战房间进度(架构 12_SaveModule §1
EventChainsSaveData.cs ← 事件链状态(架构 12_SaveModule §1
ShopsSaveData.cs ← 商店已购记录(架构 12_SaveModule §1
NGPlusSaveData.cs ← New Game+ 数据(架构 12_SaveModule §1
```
⚠️ `SaveData` 类**不含** `Tutorial` 字段(架构 12 §1 无 `TutorialSaveData`),教程进度应通过 `PlayerPrefs` 或独立 JSON 文件持久化,不经过存档系统。
### 6.2 ISaveStorage + LocalFileStorage
```csharp
// Assets/Scripts/Core/Save/ISaveStorage.cs
// ⚠️ 接口方法操作原始 JSON 字符串(架构 12_SaveModule §2序列化/校验由 SaveManager 负责
// ⚠️ 返回类型为标准 Task非 UniTask
public interface ISaveStorage
{
Task WriteAsync(int slotIndex, string json);
Task<string> ReadAsync(int slotIndex);
Task DeleteAsync(int slotIndex);
bool Exists(int slotIndex);
IEnumerable<int> GetExistingSlots();
}
// Assets/Scripts/Core/Save/LocalFileStorage.cs
// ⚠️ 类名无 Save 前缀(架构 12_SaveModule §2
// 存档路径: Application.persistentDataPath/saves/save_{slot}.json
public class LocalFileStorage : ISaveStorage { }
```
### 6.3 SaveManager 骨架
```csharp
// Assets/Scripts/Core/Save/SaveManager.cs
// ⚠️ 返回类型为标准 Task非 UniTask见架构 12_SaveModule §4
[DefaultExecutionOrder(-900)]
public class SaveManager : MonoBehaviour
{
// 存档当前状态(由 EVT_SavePointActivated 触发)
public async Task SaveAsync(int slot = -1); // 遍历 _saveables → 序列化 → WriteAsync
// 读取并恢复存档(返回 false 表示槽位不存在或校验失败)
public async Task<bool> LoadAsync(int slot); // ReadAsync → 反序列化 → 遍历 _saveables
public bool SlotExists(int slot);
public IEnumerable<int> GetExistingSlots();
public void Register(ISaveable saveable);
public void Unregister(ISaveable saveable);
}
```
### 6.4 组装 Persistent 场景
`Persistent.unity` 创建以下 GameObject 层级(组件留空或骨架绑定):
```
[Managers]
├── GameManager ← GameManager.csInspector 绑定所有 EVT_ SO 资产
├── SceneLoader ← SceneLoader.cs
├── GlobalObjectPool ← GlobalObjectPool.cs
├── SaveManager ← SaveManager.cs
└── SettingsManager ← SettingsManager.cs + GlobalSettingsSO 资产引用
```
### 6.5 全链路验证
创建 `Scenes/TestRoom_Phase0.unity`(空房间,一个平台的 Tilemap并完成以下验证
| 验证项 | 方法 |
|--------|------|
| Persistent 场景加载 + GameManager Awake 打印版本号 | Play `Persistent.unity`Console 看 Log |
| `SaveAsync(0)` 写入 JSON 存档文件Phase 0 新游戏可手动构造初始 SaveData 后调用) | 菜单调用,检查 `persistentDataPath/saves/save_0.json` |
| `LoadAsync(0)` 读取并填充 `CurrentSave` | Debug.Log 打印 `CurrentSave.Meta.PlayTime` |
| `EVT_PlayerDied` Raise → `GameManager` 收到并打印 | Inspector "Raise" 按钮 |
| `AddressKeys.SceneTestRoom` 能被 `SceneLoader` 加载 | 调用 `GameManager.LoadRoom` |
---
## 7. 完成标准检查清单
```
✅ Unity 编译无错Console 无 Error脚本层全部创建完毕2026-05-07
✅ 所有 asmdef 依赖方向正确(低层不引用高层)
✅ SO 事件系统Raise 能触发订阅者OnDisable 能正确取消订阅
□ GlobalSettingsSO 序列化/反序列化无 JSON 报错(待 Unity Editor 运行验证)
□ SaveManager.SaveAsync(0) → 磁盘生成 save_0.json待 Unity Editor 运行验证)
□ SaveManager.LoadAsync → CurrentSave 非 null数据匹配待 Unity Editor 运行验证)
□ SceneLoader 能 Additive 加载/卸载 TestRoom 场景(待 TestRoom 场景创建后验证)
□ GlobalObjectPool.Spawn<PooledObject> 能返回实例Despawn 能归还(待 Unity Editor 运行验证)
□ AddressKeysValidator 无 Warning当前定义的 key 均已在 Addressables 分组中)
□ Persistent 场景 GameManager.Awake 打印版本号(待 Persistent 场景组装后验证)
```
> **Phase 0 代码层完成于 2026-05-07。** 运行时验证项待 Phase 1 场景组装完成后一并执行。
**Phase 0 完成后进入 Phase 1。**