32 KiB
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 依赖方向验证通过
目录
- 实施顺序总览
- Day 1:文件夹与 asmdef 骨架
- Day 2:SO 事件系统
- Day 3:Core 模块
- Day 4:Addressables 与对象池骨架
- Day 5:SaveData 骨架 + Persistent 场景验证
- 完成标准检查清单
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 无脚本"警告:
// _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枚举已全面替换为GameStateIdstruct +IGameState接口体系(架构 03_CoreModule §2)。 旧写法TransitionTo(GameState.Gameplay)改为TransitionTo(GameStates.Gameplay); 订阅方将收到GameStateId而非枚举值。
4.2 GameManager 实现优先级
Day 3 只实现骨架部分,不实现完整死亡/复活流程(Phase 1 完成):
// 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 实现
// Day 3 实现范围
// 订阅 EVT_SceneLoadRequest,执行 Addressables.LoadSceneAsync
// 加载完成后发布 EVT_SceneLoaded
// _currentRoomScene 记录当前场景名,用于 Unload
4.4 GlobalSettingsSO
// ⚠️ 字段名、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
// 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
// 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
// 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<T>(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()时一并实现。
// 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
// 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
// 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
// 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 骨架
// 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.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<PooledObject> 能返回实例,Despawn 能归还(待 Unity Editor 运行验证)
□ AddressKeysValidator 无 Warning(当前定义的 key 均已在 Addressables 分组中)
□ Persistent 场景 GameManager.Awake 打印版本号(待 Persistent 场景组装后验证)
Phase 0 代码层完成于 2026-05-07。 运行时验证项待 Phase 1 场景组装完成后一并执行。
Phase 0 完成后进入 Phase 1。