# 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、Feel(More Mountains) > **产出物**:编译无错;能运行 Persistent 场景;能读写 JSON 存档;asmdef 依赖方向验证通过 --- ## 目录 1. [实施顺序总览](#1-实施顺序总览) 2. [Day 1:文件夹与 asmdef 骨架](#2-day-1文件夹与-asmdef-骨架) 3. [Day 2:SO 事件系统](#3-day-2so-事件系统) 4. [Day 3:Core 模块](#4-day-3core-模块) 5. [Day 4:Addressables 与对象池骨架](#5-day-4addressables-与对象池骨架) 6. [Day 5:SaveData 骨架 + 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 2:SO 事件系统 **参考文档**:`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 ← struct(sceneName + entryId + loadingScreen) SceneLoadRequestEventChannelSO.cs DifficultyLevel.cs ← 枚举(Easy/Normal/Hard/SteelSoul) DifficultyChangedEventChannel.cs ← Phase 2 难度系统用(按架构命名,非 DifficultyEventChannelSO) DamageInfoEventChannelSO.cs ← Combat 伤害事件(EVT_DamageDealt) HitInfo.cs ← struct(DamageInfo + HitPoint Vector3) HitConfirmedEventChannelSO.cs ← VFX 命中事件(EVT_HitConfirmed,洛载类型 HitInfo) ShopPurchaseEvent.cs ← struct(⚠️ 架构 15_MapShopModule §2.3:ShopPurchaseEvent { 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`(0–1 进度) | | `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 3:Core 模块 **参考文档**:`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 ← MonoBehaviour,ExecutionOrder -2000,DontDestroyOnLoad 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 骨架(封装 SceneLoader,Phase 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 4:Addressables 与对象池骨架 **参考文档**:`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 LoadAsync(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 §3;Phase 0 仅建骨架,不实现全量 [DefaultExecutionOrder(-800)] public class GlobalObjectPool : MonoBehaviour { // 预热:按 _warmupConfigs 配置批量实例化所有条目(无参数,以 Task 返回) public async Task WarmupAsync(); // 获取对象(正式操作: Spawn) public T Spawn(string addressKey, Vector3 position, Quaternion rotation) where T : Component; // 归还对象(正式操作: Despawn) public void Despawn(string addressKey, GameObject instance); } ``` ### 5.3.1 WarmupManifestSO(Phase 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 §4);Phase 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 显式注册 API(SceneLoader 不主动调用本类) 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 5:SaveData 骨架 + 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 ReadAsync(int slotIndex); Task DeleteAsync(int slotIndex); bool Exists(int slotIndex); IEnumerable 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 LoadAsync(int slot); // ReadAsync → 反序列化 → 遍历 _saveables public bool SlotExists(int slot); public IEnumerable GetExistingSlots(); public void Register(ISaveable saveable); public void Unregister(ISaveable saveable); } ``` ### 6.4 组装 Persistent 场景 在 `Persistent.unity` 创建以下 GameObject 层级(组件留空或骨架绑定): ``` [Managers] ├── GameManager ← GameManager.cs,Inspector 绑定所有 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 能返回实例,Despawn 能归还(待 Unity Editor 运行验证) □ AddressKeysValidator 无 Warning(当前定义的 key 均已在 Addressables 分组中) □ Persistent 场景 GameManager.Awake 打印版本号(待 Persistent 场景组装后验证) ``` > **Phase 0 代码层完成于 2026-05-07。** 运行时验证项待 Phase 1 场景组装完成后一并执行。 **Phase 0 完成后进入 Phase 1。**