
一、项目整体架构总体分为两个部分持久化场景与动态加载场景持久化场景初始化场景不卸载├── SceneLoaderManager ── 协管场景加载、持有 DataManager├── UIManager ── 监听场景事件按需显隐 UI├── DataManager ── 存档系统中枢单例└── EventSystem ── 全局事件动态加载场景Additive├── 主菜单├── Level_01 / Level_02 / ...└── Boss 房间架构设计思想1. 事件驱动ScriptableObject 作为事件通道解耦模块2. 单例 服务定位DataManager、SceneLoader 等全局唯一访问3. 接口隔离ISavable 接口统一存档行为4. 状态模式敌人 AIBaseState → 具体状态类二、场景加载系统其中几个关键代码逻辑---SceneLoader (挂载在持久化场景中)1.持有多个 SceneLoadEventSOScriptableObject 事件实例在 Awake 时给各个 SO 订阅回调函数2.保存各个加载状态数据如isLoading / currentScene--- SceneLoadEventSO用作场景间通信的 ScriptableObject1.LoadRequestEvent 携带 (场景SO, 出生坐标, 是否渐入) 参数2.SceneUnloadEvent 卸载前广播UIManager 监听3.AfterSceneLoaded 加载完成后广播摄像机等模块监听4. NewGameEvent 新游戏专用携带首个场景出生坐标以玩家进入传送门为例展示完整流程链1.玩家进入传送门并点击传送键传送门脚本调用: loadRequestEventSO.RaiseEvent(sceneSO, pos, fadeOut)2.SceneLoader 收到事件订阅函数 OnLoadRequestEvent├── 检查 isLoading → true 则 return防止重复加载├── 保存 sceneToLoad / posToGo / fadeScreen 到成员变量├── isLoading true├── 锁定玩家操作└── 启动协程 SwitchScene()3.SwitchScene() 协程链├── ① 渐出黑幕FadeOut├── ② 广播 SceneUnloadEvent → UIManager 监听决定隐藏哪些 UI├── ③ 关闭玩家图片├── ④ 卸载当前场景 currentScene.SceneReference.UnloadSceneAsync()└── ⑤ 调用 LoadNewScene()4. LoadNewScene()├── loadingOption sceneToLoad.SceneReference.LoadSceneAsync(Additive)├── loadingOption.Completed OnSceneLoadCompleted└── 完成后广播 SceneLoadedEvent5. OnSceneLoadCompleted()├── currentScene sceneToLoad├── 设置 player.position posToGo├── 启动 ShowScene() 协程└── 广播 AfterSceneLoadedEvent → 摄像机获取范围6.ShowScene() 协程├── 非主菜单 → 打开玩家图像 渐入 解锁玩家└── isLoading false场景加载系统的优点1.ScriptableObject 做事件通道 模块不需要互相引用只依赖同一个 SO 资产脱离场景生命周期持久化存在2.Additive 场景加载主场景持久化场景始终存在不销毁。 关卡场景 Additive 加载卸载时只卸关卡。全局管理器DataManager / UIManager不丢失三、蹬墙跳逻辑实现if (isGround) → 普通跳跃只加向上的力if (onWall) → 蹬墙跳向水平反方向 向上加力wallJump trueOnWall判定Physics2D.OverlapCircleLayerMask检测半径碰到地面图层玩家当前有水平输入按住方向键玩家正在下落rigidbody.velocity.y 0wallJump 标记作用- 防止蹬墙跳后立刻往回走又贴墙引发连续无限跳- 可能配合短暂的方向锁四、敌人状态机基类 BaseState抽象类:OnEnter() 抽象方法进入状态时调用一次LogicUpdate() 抽象方法在 Update 中每帧调用PhysicsUpdate() 抽象方法在 FixedUpdate 中每帧调用OnExit() 抽象方法离开状态时调用一次在 Enemy 主类中LogicUpdate 在 Update() 中调用 currentState.LogicUpdate()PhysicsUpdate 在 FixedUpdate() 中调用 currentState.PhysicsUpdate()切换状态先 currentState.OnExit() → currentState newState → newState.OnEnter()优点状态模式比起if-else 堆砌来说状态独立成类职责单一易扩展加新状态不需改旧代码符合开闭原则 OCP五、存档系统---DataManager (单例)┌─────────────────────┐│ ListISavable │ ← 注册的所有可存档物体│ Data savedData │ ← 唯一存档数据实例│ ││ RegisterData() │ ← 物体 Awake/Start 时调用│ UnregisterData() │ ← 物体 OnDisable 时调用│ Save() │ ← 遍历 List每个物体写数据│ Load() │ ← 遍历 List每个物体读数据└──┬──────────┬───────┘│ │┌─────────▼──┐ ┌───▼──────────┐│ Player │ │ Enemy_01 │ ... 都实现 ISavable│ (坐标/血量) │ │ (位置/状态) │└────────────┘ └──────────────┘---每一个需保存的物品实现接口ISavable- RegisterSaveData() 注册到 DataManager 的 List 中- UnregisterSaveData() 从 DataManager 的 List 中移除- GetSaveData(Data data) 将自己的状态写入传入的 Data 对象- LoadData(Data data) 从 Data 对象读取状态恢复自身---Data 类存档数据的容器内部结构多种字典键统一为 GUID 字符串Dictionarystring, Vector3 characterPosDict // 位置Dictionarystring, float healthDict // 血量Dictionarystring, float energyDict // 能量/法力string currentScene // 当前所在场景名键的生成挂载在每个可存档物体上的 DataDefination 脚本C# 运行时生成全局唯一的 GUID 字符串 每个物体终生唯一 ID--- 存档流程[保存] 按 ESC 键触发1. 检测是否已在菜单界面 → 是则 return2. 调用 DataManager.Instance.Save()- 遍历 List 中所有 ISavable 实例- 每个实例执行 GetSaveData(savedData)→ 将自己的坐标、血量等写入 savedData 对应字典3. 返回主菜单场景4. 注返回主菜单时旧场景场景卸载 → 所有 ISavable 物体 OnDisable()→ UnregisterSaveData → List 清空[加载] 点击继续游戏1. SceneLoader 中触发 ContinueGame()2. 如果 savedData.currentScene 为 null 或是主菜单 → 开新游戏3. 否则加载存储的场景 → 走协程链 (渐出 → 卸载 → 加载 → 渐入)4. isContinuedLoad true标识本次是加载存档5. 等场景加载完毕 AfterSceneLoaded 事件触发后此时新场景中的 ISavable 物体已完成 RegisterSaveDataList 已重新填充6. 检测 isContinuedLoad → 调用 DataManager.Instance.Load()- 遍历 List 中所有 ISavable 实例- 每个实例执行 LoadData(savedData)→ 从字典中读取自己 GUID 对应的坐标、血量覆盖当前值7. isContinuedLoad false六、核心问题1用 ScriptableObject 做事件系统好处SO 脱离场景可在 Inspector 可视化连接订阅关系单例需要 FindObjectOfType 或全局引用耦合较重SO 天然支持多份实例不同场景用不同事件配置。2: 状态机如果用 Animator 状态机Mecanim做和代码做有什么区别Mecanim 适合动画过渡代码状态机适合 AI 决策速度/视野/目标。本项目把两者结合代码状态机控制行为逻辑Animator 控制表现层动画。3: 蹬墙跳怎么防止玩家对着墙无限跳设置 wallJump 标记跳跃后短暂禁止水平输入或锁定方向并且 OnWall 需要 玩家在下落 才判定上升到最高点后不再判定为贴墙。4: Additive 加载多个场景内存怎么管理同一时间只加载一个关卡场景。加载新场景前先 UnloadSceneAsync 卸载旧场景避免 Accumulated(累积内存)。全局的管理器在持久化场景不受影响。5: 怎么处理加载存档和新游戏的入口差异通过 isContinuedLoad 标记区分新游戏直接加载第一个关卡不执行 DataManager.Load()加载存档加载存储的场景完成后执行 Load() 覆盖位置血量