561 lines
25 KiB
Markdown
561 lines
25 KiB
Markdown
# zeling_v2 代码综合评审(Post-Fix 终态)
|
||
|
||
> **评审日期**:2025-05-11
|
||
> **评审范围**:`Assets/Scripts/` 全部模块(约 270 个 .cs 文件,25 个 Assembly Definition)
|
||
> **评审标准**:以《空洞骑士》《Celeste》《Neon Abyss》等成熟商业 2D 动作游戏的代码架构与质量水准为参照
|
||
> **状态说明**:本文档反映经历两轮全面优化(MasterCodeReview.md × 3 P0/P1/P2 + FullCodeReview.md × 18 项)后的**当前终态**代码;已修复问题不再列入缺陷表,仅记录剩余未解决项及综合评估。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [评分总览](#1-评分总览)
|
||
2. [架构设计](#2-架构设计)
|
||
3. [性能](#3-性能)
|
||
4. [可扩展性](#4-可扩展性)
|
||
5. [编辑器友好性](#5-编辑器友好性)
|
||
6. [使用便利性 DX](#6-使用便利性-dx)
|
||
7. [剩余问题优先级表](#7-剩余问题优先级表)
|
||
8. [与商业基准的对标分析](#8-与商业基准的对标分析)
|
||
9. [下一阶段建议](#9-下一阶段建议)
|
||
|
||
---
|
||
|
||
## 1. 评分总览
|
||
|
||
| 维度 | 本次得分(/10) | 上轮得分 | 变化 | 主要驱动因素 |
|
||
|------|---------------|----------|------|------------|
|
||
| **架构设计** | **8.0** | 7.5 | ▲ 0.5 | SerializeField 减少、SaveManager.Data 移除、EnemyDied 类型修正 |
|
||
| **性能** | **7.8** | 7.0 | ▲ 0.8 | Pool 统一 async、批量 LOS 节流、StatusEffect 双结构 |
|
||
| **可扩展性** | **8.2** | 7.5 | ▲ 0.7 | WorldStateRegistry 泛化、AddressKeyRegistry DLC 扩展、SaveMigrator 版本链 |
|
||
| **编辑器友好性** | **7.8** | 6.5 | ▲ 1.3 | SOValidationRunner 构建钩子、多个自定义 EditorWindow |
|
||
| **使用便利性** | **8.1** | 7.0 | ▲ 1.1 | EventSubscription RAII、PlayerStateBase 便捷属性、具名存档访问器 |
|
||
| **综合** | **8.0** | 7.1 | ▲ 0.9 | 全面提升,接近《Neon Abyss》量级商业质量 |
|
||
|
||
> **评分说明**:满分 10 = 顶级 AA 商业代码(《空洞骑士》级别);8.0 ≈ 功能完善的商业独立游戏中上水准(如 _Neon Abyss_、_Haiku the Robot_)。
|
||
|
||
---
|
||
|
||
## 2. 架构设计
|
||
|
||
### 2.1 SO Event Channel 系统 ✅ 强
|
||
|
||
`BaseEventChannelSO<T>` 泛型事件基类实现完整,超过大多数独立参考实现:
|
||
|
||
```csharp
|
||
// BaseEventChannelSO.cs — 核心设计
|
||
private event Action<T> _onEventRaisedBacking; // 防止外部 = 赋值
|
||
public EventSubscription Subscribe(Action<T> callback)
|
||
{
|
||
OnEventRaised += callback;
|
||
return new EventSubscription(() => OnEventRaised -= callback);
|
||
}
|
||
public void Raise(T value)
|
||
{
|
||
#if UNITY_EDITOR
|
||
EventBusMonitor.Record(name, value?.ToString() ?? "null", _subscriberCount, Time.frameCount);
|
||
#endif
|
||
_onEventRaisedBacking?.Invoke(value);
|
||
}
|
||
```
|
||
|
||
**亮点:**
|
||
- 私有 backing field + public event 属性,防止意外的 `=` 覆盖
|
||
- `EventSubscription` RAII 句柄 + `CompositeDisposable`,与 UniRx 风格一致,订阅生命周期零泄漏风险
|
||
- Editor-only `_subscriberCount` + `EventBusMonitor` 256 条环形日志,帧号/载荷/监听器数量齐全
|
||
- 35+ 具体频道覆盖全模块(`BossPhaseEventChannelSO`、`ShopPurchaseEventChannelSO`、`ParryInfoEventChannelSO` 等),系统间完全解耦
|
||
|
||
**商业对比**:达到 GDC 2017 Unite Austin 推荐的 SO 架构完整落地水准;`CompositeDisposable.AddTo()` 扩展方法在开源 Unity 项目中属于高完整度实现。
|
||
|
||
---
|
||
|
||
### 2.2 Assembly Definition 拓扑 ✅ 强
|
||
|
||
25 个 asmdef 形成明确的单向依赖链:
|
||
|
||
```
|
||
BaseGames.Core.Events
|
||
↓
|
||
BaseGames.Core
|
||
↓ ↓ ↓
|
||
BaseGames.Combat BaseGames.Parry BaseGames.Player
|
||
↓ ↓
|
||
BaseGames.Enemies BaseGames.Player.States
|
||
↓
|
||
BaseGames.Quest / Progression / World
|
||
```
|
||
|
||
- 循环依赖为零,编译期隔离各模块
|
||
- `IPathAgent` 接口位于 `BaseGames.Enemies`,`EnemyNavAgent` 实现位于 `BaseGames.Enemies.Navigation`,运行时依赖倒置
|
||
- 测试/替换某模块不影响其他层,支持后续按需热重载或 DLC 接入
|
||
|
||
---
|
||
|
||
### 2.3 ServiceLocator ✅ 强
|
||
|
||
```csharp
|
||
// ServiceLocator.cs — 接口类型键,支持测试覆盖
|
||
public static void Register<TInterface>(TInterface impl)
|
||
=> _services[typeof(TInterface)] = impl;
|
||
public static void RegisterIfAbsent<TInterface>(TInterface impl) { ... }
|
||
public static TInterface GetOrDefault<TInterface>(TInterface fallback = default) { ... }
|
||
#if UNITY_EDITOR
|
||
public static void OverrideForTest<TInterface>(TInterface mock) { ... }
|
||
public static void Reset() => _services.Clear();
|
||
#endif
|
||
```
|
||
|
||
`RegisterIfAbsent` 幂等注册有效解决多场景重复注册问题;`OverrideForTest` Editor-only 沙箱支持单元测试替换,不污染 Release 构建。
|
||
|
||
---
|
||
|
||
### 2.4 玩家 FSM ✅ 强
|
||
|
||
```csharp
|
||
// PlayerController.cs — POCO 状态 + Type-keyed Dictionary
|
||
private readonly Dictionary<Type, PlayerStateBase> _states = new();
|
||
public T GetState<T>() where T : PlayerStateBase
|
||
=> _states.TryGetValue(typeof(T), out var s) ? s as T : null;
|
||
```
|
||
|
||
16 个具体状态类(Idle/Run/Jump/Fall/Dash/AerialDash/Parry/Hurt/Dead/Attack×4 等):
|
||
- 全部 POCO(非 MonoBehaviour),零堆内存分配的状态切换
|
||
- `Dictionary<Type, PlayerStateBase>` O(1) 查找,无 switch/if-else 链
|
||
- `PlayerStateBase.ValidTransitions`(Editor-only)声明合法出口状态,开发期转换验证
|
||
- Animancer 双层:Layer 0 全身状态,Layer 1 Overlay 叠加层(Spring/SoulSkill 不打断移动动画)
|
||
|
||
---
|
||
|
||
### 2.5 Boss 体系 ✅ 强
|
||
|
||
`EnemyBase → BossBase` 清晰继承,`BossBase.EnterPhase()` 虚方法支持多阶段;`BossSkillSO` + `SkillSequenceSO` 数据驱动,`BossSkillExecutor` 运行时执行。`BD_EnterPhase` / `BD_IsHPBelow` 等 Behavior Designer 任务节点封装了 Boss 特有逻辑,与通用 AI 节点复用统一的 `EnemyBase` 接口。
|
||
|
||
---
|
||
|
||
### 2.6 仍存在的架构问题
|
||
|
||
#### ⚠️ P2:单例/ServiceLocator 混用
|
||
|
||
| 类 | 当前访问方式 | 建议 |
|
||
|----|------------|------|
|
||
| `SaveManager` | `SaveManager.Instance` | 注册为 `ISaveService` |
|
||
| `GlobalObjectPool` | `GlobalObjectPool.Instance` | 注册为 `IObjectPoolService` |
|
||
| `MapManager` | `MapManager.Instance` | 注册为 `IMapService` |
|
||
|
||
三者均为 `DontDestroyOnLoad` 单例,与 `ServiceLocator` 模式并存,增加全局状态来源的认知负担。`QuestManager` 已迁移至 ServiceLocator,是正确方向。
|
||
|
||
#### ⚠️ P2:Enemy 状态为枚举,不可无侵入扩展
|
||
|
||
`EnemyStateType`(`Controlled/Hurt/Stagger/Dead`)为简单枚举,新增状态类型需修改枚举定义,与玩家 POCO FSM 扩展性不对等。对于 Boss 专属状态(如 `Enraged`、`Stunned`)扩展不够灵活。
|
||
|
||
#### ⚠️ P3:`AntiSoftlockSystem.Start()` 使用 FindFirstObjectByType
|
||
|
||
```csharp
|
||
_player = FindFirstObjectByType<PlayerController>();
|
||
```
|
||
|
||
每次场景加载执行一次,开销有限但仍属"全场景扫描"。可通过注入 `_onPlayerSpawned` 事件频道替代。
|
||
|
||
---
|
||
|
||
## 3. 性能
|
||
|
||
### 3.1 热路径优化 ✅ 强
|
||
|
||
| 优化项 | 实现 | 说明 |
|
||
|--------|------|------|
|
||
| 距离判断 | `sqrMagnitude` | 避免 `Vector2.Distance` 开根 |
|
||
| GetComponent | 全量 Awake 缓存 | 无 Update 中动态调用 |
|
||
| C# event vs UnityEvent | 全用 C# `Action<T>` | 无 UnityEvent 反射开销 |
|
||
| 方向翻转 | `SpriteRenderer.flipX` | 替代旧版 `localScale.x = -1`(避免碰撞体跟随缩放) |
|
||
| 状态切换 | POCO Dictionary 查找 | 零 GC,O(1) |
|
||
| 输入 | `InputReaderSO` C# event | 无 `Input.GetKey` 每帧轮询 |
|
||
|
||
### 3.2 状态效果双结构 ✅ 优秀
|
||
|
||
```csharp
|
||
// StatusEffectManager.cs — List + Dictionary 并行
|
||
private readonly List<StatusEffect> _activeList = new();
|
||
private readonly Dictionary<StatusEffectType, StatusEffect> _activeIndex = new();
|
||
|
||
// Update:逆序遍历 List(无 GC,避免移除时索引偏移)
|
||
for (int i = _activeList.Count - 1; i >= 0; i--)
|
||
{
|
||
effect.Update(delta);
|
||
if (effect.IsExpired) RemoveAt(i, effect);
|
||
}
|
||
```
|
||
|
||
List 用于每帧 Tick 遍历,Dictionary 用于 O(1) 叠层查找(`existing.OnStack()`),是引擎级 StatusEffect 系统的标准实现方式。`MaterialPropertyBlock` 修改 Shader 参数不污染共享材质,零 Draw Call 增加。
|
||
|
||
### 3.3 批量 LOS 视线检测节流 ✅ 优秀
|
||
|
||
```csharp
|
||
// BatchLOSSystem.cs — 每帧仅处理部分请求者
|
||
[SerializeField, Min(1)] private int _maxRequestersPerFrame = 8;
|
||
private int _currentOffset = 0;
|
||
|
||
// FixedUpdate 中均匀轮询,避免单帧全量 Raycast
|
||
int count = Mathf.Min(_maxRequestersPerFrame, _requesters.Count);
|
||
for (int i = 0; i < count; i++)
|
||
int idx = (_currentOffset + i) % _requesters.Count;
|
||
```
|
||
|
||
20 个敌人仅每帧最多 8 条射线,均匀分配保证每个敌人 ~2.5 帧更新一次,感知延迟可配置,Raycast 数量与敌人规模解耦。
|
||
|
||
### 3.4 对象池 WarmupAsync 统一 ✅ 强
|
||
|
||
```csharp
|
||
// GlobalObjectPool.cs — 单一入口,Coroutine 重载已移除
|
||
public async Task WarmupAsync()
|
||
{
|
||
foreach (var cfg in _warmupConfigs)
|
||
{
|
||
_maxCounts[cfg.AddressKey] = cfg.MaxCount;
|
||
await WarmupSingleAsync(cfg.AddressKey, cfg.InitialCount);
|
||
}
|
||
}
|
||
```
|
||
|
||
LRU `LinkedList<PooledObject>` 活跃链表 + `Queue<PooledObject>` 空闲队列;Addressables 预加载,`MaxCount > 0` 时强制上限并回收最旧对象;`BackgroundRefillCoroutine` 低优先级后台补充(异步与协程混用有充分理由:补充逻辑跨帧分散)。
|
||
|
||
### 3.5 存档系统 async/SemaphoreSlim ✅ 强
|
||
|
||
- `SemaphoreSlim(1,1)` 保证同一时刻只有一个写操作,防止并发存档数据竞争
|
||
- `Formatting.None` 序列化减少 JSON 字符串体积
|
||
- HMAC-SHA256 完整性校验防止存档篡改
|
||
- `SaveMigrator` 线性版本链(1.0 → 1.1 → 2.0 → 2.1),goto fall-through 模式简洁正确
|
||
|
||
### 3.6 轻微性能风险项
|
||
|
||
| 位置 | 问题 | 影响 |
|
||
|------|------|------|
|
||
| `DashState` | `Cfg != null ? Cfg.DashDuration : 0.18f` 魔法数值兜底 | 无运行时开销,但 Cfg 一旦为 null 行为静默 |
|
||
| `SpeedrunTimer` | 使用 `Stats.DistanceTraveled` 字段存储计时(语义复用) | 无性能问题,仅语义混乱 |
|
||
| `GameServiceRegistrar.EnsureSingleAudioListener()` | 首次调用 `FindObjectsOfType<AudioListener>` | 仅执行一次(Awake),后续场景加载改为局部扫描,可接受 |
|
||
|
||
---
|
||
|
||
## 4. 可扩展性
|
||
|
||
### 4.1 WorldStateRegistry 泛化 ✅ 强
|
||
|
||
```csharp
|
||
// WorldStateRegistry.cs — Dictionary<WorldObjectCategory, HashSet<string>>
|
||
public bool IsMarked(WorldObjectCategory category, string id) { ... }
|
||
public void Mark(WorldObjectCategory category, string id) { ... }
|
||
// 向后兼容具名快捷 API
|
||
public bool IsCollected(string id) => IsMarked(WorldObjectCategory.Collectible, id);
|
||
public void MarkDoorOpened(string id) => Mark(WorldObjectCategory.Door, id);
|
||
```
|
||
|
||
新增世界对象类型只需在 `WorldObjectCategory` 枚举中添加值,无需修改 Registry 本体;`OnStateChanged` 事件使 UI/测试代码可响应式刷新;`OnEnable` 在域重载时清空状态,消除 Editor 跨 Play Session 污染。
|
||
|
||
### 4.2 存档版本迁移管道 ✅ 强
|
||
|
||
```csharp
|
||
// SaveMigrator.cs — fall-through switch 版本链
|
||
switch (data.Meta.Version)
|
||
{
|
||
case "1.0": data = MigrateFrom1_0(data); goto case "1.1";
|
||
case "1.1": data = MigrateFrom1_1(data); goto case "2.0";
|
||
case "2.0": data = MigrateFrom2_0(data); goto case "2.1";
|
||
case "2.1": break;
|
||
default: Debug.LogWarning(...); break;
|
||
}
|
||
```
|
||
|
||
新版本只需添加一个 `MigrateFromX_X` 方法和一个 `case`,跨多个版本升级的玩家自动经历完整迁移路径,是商业游戏存档系统的标准实践。
|
||
|
||
### 4.3 Addressable Key 运行时注册 ✅ 强
|
||
|
||
```csharp
|
||
// AddressKeyRegistry.cs — DLC/扩展包运行时注册额外 key
|
||
AddressKeyRegistry.TryRegister("DLC_WeaponScythe", "DLC/WPN_Scythe");
|
||
// GlobalObjectPool 内部调用 Resolve 解析,兼容静态常量调用方
|
||
```
|
||
|
||
`ForceRegister` 供热更/测试覆盖,`Unregister` 支持 DLC 卸载,`Resolve` 对未注册 key 回退原值(向后兼容)。这是 Addressables 驱动的内容管理系统中少见的完善实现。
|
||
|
||
### 4.4 装备/护符体系 ✅ 强
|
||
|
||
`ICharmEffect` 接口 + 6 种具体效果(`StatModifierEffect`、`AttackSpeedEffect`、`OnHitEffect`、`WeaponOverrideEffect`、`SkillSlotOverrideEffect`、`SoulSpellEffect`)覆盖属性、攻击速度、命中触发、武器替换等场景;`SkillModifierRegistry` 为技能的数值修改提供独立注册点;`EquipmentConfigSO` 初始 Notch 容量可配。新增护符类型只需实现 `ICharmEffect`,无需修改 `EquipmentManager`。
|
||
|
||
### 4.5 成就条件体系 ✅ 强
|
||
|
||
12 种具体条件实现(`DefeatedBossCondition`、`ParryCountCondition`、`NoHealRunCondition`、`TimedBossKillCondition`、`MapExplorationCondition` 等),通过抽象 `AchievementCondition` 统一接口,`AchievementSO` 数据驱动,设计器可自由组合条件而不改代码。
|
||
|
||
### 4.6 可扩展性薄弱项
|
||
|
||
| 项目 | 当前状态 | 问题 |
|
||
|------|---------|------|
|
||
| `EnemyStateType` 枚举 | `Controlled/Hurt/Stagger/Dead` | 新增状态(如 Boss 专属 `Enraged`)需改枚举 |
|
||
| `DashState` 魔法数值 | `0.18f / 20f / 3f / 0.4f` 硬编码 | Config 为 null 时行为不可配置 |
|
||
|
||
---
|
||
|
||
## 5. 编辑器友好性
|
||
|
||
### 5.1 SO 数据验证构建钩子 ✅ 强
|
||
|
||
```csharp
|
||
// SOValidationRunner.cs — IPreprocessBuildWithReport
|
||
public void OnPreprocessBuild(BuildReport report)
|
||
{
|
||
var (errors, warnings) = RunAll();
|
||
if (errors.Count > 0)
|
||
throw new BuildFailedException(...); // 数据错误直接中止构建
|
||
}
|
||
[MenuItem("Tools/Validate All ScriptableObjects")]
|
||
public static void ValidateMenu() { ... }
|
||
```
|
||
|
||
构建前自动扫描所有实现 `IValidatable` 的 SO,发现错误立即中止,防止数据配置错误进入发布版本;菜单项支持手动一键验证。`AddressKeyValidator` 以 `callbackOrder = 0` 在其前一步运行(先验证地址合法性,再验证 SO 数据),顺序正确。
|
||
|
||
### 5.2 EventBusMonitorWindow ✅ 强
|
||
|
||
- 256 条环形记录(帧号 + 时间戳 + 频道名 + 载荷 + 监听器数量)
|
||
- 运行时实时观察事件流,无需断点即可诊断"事件发出了但没人听"
|
||
- `Clear()` API 供测试前手动清空
|
||
|
||
### 5.3 多个自定义 EditorWindow ✅ 强
|
||
|
||
| 工具 | 作用 |
|
||
|------|------|
|
||
| `EventBusMonitorWindow` | 运行时事件流监控 |
|
||
| `BossSkillSequenceWindow` | Boss 技能序列可视化编辑 |
|
||
| `EventChainEditorWindow` | EventChain(事件链)节点图编辑 |
|
||
| `AddressReferenceGraphWindow` | Addressable 依赖关系可视化 |
|
||
| `SceneScaffoldTools` | 场景结构一键生成 |
|
||
| `NavSurfaceBakeShortcut` | 导航网格烘培快捷键 |
|
||
| `MapRoomDataEditor` | 地图房间数据编辑器 |
|
||
|
||
7 个自定义工具窗口/菜单项覆盖了动画、AI、关卡、导航的日常工作流,质量接近商业 AA 内部工具链水准。
|
||
|
||
### 5.4 Gizmos 覆盖
|
||
|
||
- `WorldMarker`:Gizmos 可视化世界标记位置
|
||
- `HitBox` / `HurtBox`:判定盒/受击盒在 Scene 视图中可见(红/绿色)
|
||
- `CameraTriggerZone`:相机触发区域边界可视化
|
||
- `RoomVisibleArea`:房间可见区域 Gizmos
|
||
|
||
### 5.5 编辑器友好性薄弱项
|
||
|
||
| 项目 | 问题 |
|
||
|------|------|
|
||
| `HurtBox` 注入方法 | `SetShieldable/SetParrySystem/SetPoiseSource` 代码注入,Inspector 中无法观察到注入状态,调试时需查看代码 |
|
||
| `PlayerController` SerializeField | 约 15 个(已从 18 减至 15),对于新成员仍有认知负担;建议将战斗组件拆分到 `PlayerCombatInitializer` 或 `PlayerComponentHub` |
|
||
|
||
---
|
||
|
||
## 6. 使用便利性 DX
|
||
|
||
### 6.1 PlayerStateBase 便捷属性 ✅ 强
|
||
|
||
```csharp
|
||
// PlayerStateBase.cs — 状态类内部无需直接引用 PlayerController
|
||
protected InputReaderSO Input => _owner.Input;
|
||
protected InputBuffer Buffer => _owner.Buffer;
|
||
protected PlayerMovement Move => _owner.Movement;
|
||
protected PlayerStats Stats => _owner.Stats;
|
||
protected AnimancerComponent Anim => _owner.Animancer;
|
||
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
|
||
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
|
||
```
|
||
|
||
16 个状态类共享同一套命名约定,新增状态只需实现 `OnStateEnter/Update/Exit`,直接使用 `Anim.Play(AnimCfg.Dash)` 等高级语义,无需知道底层 Animancer API 细节。
|
||
|
||
### 6.2 EventSubscription RAII 模式 ✅ 优秀
|
||
|
||
```csharp
|
||
// 典型用法——统一 OnEnable/OnDisable 管理
|
||
private readonly CompositeDisposable _subs = new();
|
||
private void OnEnable()
|
||
{
|
||
_onEnemyDied.Subscribe(HandleEnemyDefeated).AddTo(_subs);
|
||
_onCollectiblePickup.Subscribe(HandleCollectiblePickup).AddTo(_subs);
|
||
}
|
||
private void OnDisable() => _subs.Clear();
|
||
```
|
||
|
||
`.AddTo(CompositeDisposable)` 链式调用风格与 UniRx/R3 一致,降低学习成本;开发者无需记住每个订阅的取消操作,杜绝订阅泄漏。
|
||
|
||
### 6.3 存档访问 API 具名化 ✅ 强
|
||
|
||
```csharp
|
||
// 旧(已删除):var data = SaveManager.Instance.Data; data.World.OpenedDoors.Contains(id)
|
||
// 新(当前):
|
||
sm.IsBossDefeated(bossId);
|
||
sm.IsWorldCollected(itemId);
|
||
sm.IsDoorOpened(doorId);
|
||
sm.GetPlayerMaxHP();
|
||
```
|
||
|
||
`SaveManager.Data` 属性已完全移除,强制调用方使用具名方法,避免调用者对存档数据结构产生直接依赖,后续修改存档结构只需改具名方法内部,不影响调用方。
|
||
|
||
### 6.4 EnemyDied 事件直接携带 EnemyId ✅ 强
|
||
|
||
```csharp
|
||
// EnemyBase.cs
|
||
[SerializeField] private string _enemyId;
|
||
// Die() 中:
|
||
_onEnemyDied?.Raise(_enemyId);
|
||
|
||
// QuestManager.cs — 直接用字符串匹配,无需解析 Transform
|
||
private void HandleEnemyDefeated(string enemyId)
|
||
{
|
||
foreach (var objective in _activeObjectives)
|
||
if (objective.TargetEnemyId == enemyId) ...
|
||
}
|
||
```
|
||
|
||
消除了之前 `QuestManager` 需要通过 `Transform` 反查敌人 ID 的间接依赖,`StringEventChannelSO` 是更语义化的类型选择。
|
||
|
||
### 6.5 FormController 三段广播 ✅ 强
|
||
|
||
```csharp
|
||
// FormController.SwitchForm()
|
||
_onFormChanged?.Raise(index); // 1. SO 频道:UI 刷新 / Save 持久化
|
||
OnFormChanged?.Invoke(); // 2. C# 事件:WeaponManager 同帧响应
|
||
_onSkillSetChanged?.Raise(); // 3. 另一 SO 频道:SkillHUD 刷新
|
||
```
|
||
|
||
明确区分了"跨系统广播"(SO 频道)与"同模块紧密协作"(C# 事件)的场景,三个通知面向三类不同消费者,无多余耦合。
|
||
|
||
### 6.6 DX 薄弱项
|
||
|
||
| 问题 | 细节 | 建议 |
|
||
|------|------|------|
|
||
| `SpeedrunTimer` 字段语义复用 | 使用 `Stats.DistanceTraveled` 存储速通时间,字段名与内容不符 | `SaveData.Stats` 添加 `SpeedrunTime` 字段,或 `ExtensionData["SpeedrunTime"]` |
|
||
| `DashState` 魔法数值兜底 | `Cfg != null ? Cfg.DashDuration : 0.18f`,Config 为 null 时静默使用硬编码值,无警告 | 添加 `Debug.LogWarning` 提示,或 `DashState` 构造时断言 Cfg 不为 null |
|
||
| `AntiSoftlockSystem.Start()` FindFirstObjectByType | 可接受,但不如事件驱动 | 改订阅 `_onPlayerSpawned` 事件频道 |
|
||
|
||
---
|
||
|
||
## 7. 剩余问题优先级表
|
||
|
||
> 仅列出经历两轮修复后**仍未完全解决**的项目。所有 P0 已清零。
|
||
|
||
| # | 优先级 | 问题 | 状态 | 影响范围 |
|
||
|---|--------|------|------|---------|
|
||
| A1 | P2 | 单例/ServiceLocator 混用(SaveManager / GlobalObjectPool / MapManager) | 部分 | 架构一致性 |
|
||
| A2 | P2 | EnemyStateType 枚举不可无侵入扩展 | 存在 | Enemy AI 扩展性 |
|
||
| A3 | P3 | HurtBox 依赖注入不可 Inspector 可见 | 存在 | 调试体验 |
|
||
| A4 | P3 | DashState 魔法数值兜底无警告 | 存在 | 配置错误静默 |
|
||
| A5 | P3 | SpeedrunTimer 字段语义复用 | 存在 | 存档可读性 |
|
||
| A6 | P3 | AntiSoftlockSystem 使用 FindFirstObjectByType | 存在 | Start 时一次性开销 |
|
||
| A7 | P3 | GameServiceRegistrar 首次加载 FindObjectsOfType | 存在 | Awake 时一次性开销 |
|
||
|
||
---
|
||
|
||
## 8. 与商业基准的对标分析
|
||
|
||
### 8.1 《空洞骑士》(Team Cherry,2017)
|
||
|
||
| 领域 | 空洞骑士 | zeling_v2 | 差距 |
|
||
|------|---------|-----------|------|
|
||
| 敌人 AI | 手工 FSM,每个敌人独立 C# 状态机 | Behavior Designer 行为树 + EnemyStateType 枚举 | BT 工具链更规范,但枚举不如 POCO FSM 灵活 |
|
||
| 存档系统 | 自定义二进制格式,单 slot | HMAC + JSON + 多 slot + 版本迁移 | **zeling_v2 更现代化** |
|
||
| 事件系统 | UnityEvent + 直接引用 | SO Event Channel + RAII 订阅 | **zeling_v2 解耦更彻底** |
|
||
| 装备系统 | 护符系统,硬编码效果 | ICharmEffect 接口,6 种多态效果 | **zeling_v2 可扩展性更强** |
|
||
| 输入 | Input.GetKey 轮询 | New Input System + InputReaderSO | **zeling_v2 输入架构更现代** |
|
||
|
||
### 8.2 《Celeste》(Maddy Makes Games,2018)
|
||
|
||
| 领域 | Celeste | zeling_v2 | 差距 |
|
||
|------|---------|-----------|------|
|
||
| 玩家 FSM | PICO-8 → C# 重写,纯 POCO 状态 | POCO + Dictionary<Type, State> | 架构相似,zeling_v2 查找更高效 |
|
||
| 辅助功能 | 完整辅助模式(无敌、慢速等) | `AccessibilityManager` + `AccessibilitySettingsSO` | 结构对等,具体功能待实现 |
|
||
| 速通支持 | 内置计时器 | `SpeedrunTimer`(ISaveable,unscaledDeltaTime) | 功能完整,字段复用是小缺陷 |
|
||
| 防软锁 | 无 | `AntiSoftlockSystem`(超时检测 + 逃脱选项) | **zeling_v2 更完善** |
|
||
|
||
### 8.3 《Neon Abyss》(Veewo Games,2021)
|
||
|
||
| 领域 | Neon Abyss | zeling_v2 | 差距 |
|
||
|------|-----------|-----------|------|
|
||
| 对象池 | 简单 Prefab 池 | Addressables + LRU + MaxCount + DLC 注册 | **zeling_v2 更完善** |
|
||
| 状态效果 | 叠加型 Buff 字典 | 双结构 List+Dict + MaterialPropertyBlock | **zeling_v2 架构更清晰** |
|
||
| 商店系统 | 数据驱动,状态持久化 | `ShopController` + `UIManager._currentShopId` + SO | 对等 |
|
||
| 编辑器工具 | 标准 Unity 工具 | 7 个自定义 EditorWindow + 构建钩子 | **zeling_v2 工具链更丰富** |
|
||
|
||
### 8.4 总评
|
||
|
||
zeling_v2 在**架构现代化**(SO 事件、Assembly 拓扑、New Input System)、**存档健壮性**(HMAC 完整性、版本迁移、SemaphoreSlim 并发安全)、**编辑器工具链**(7 个 EditorWindow、构建验证钩子)三个维度上已超过上述三款参照游戏的已知水准;在**敌人 AI 扩展性**(枚举 FSM vs POCO FSM)和**全局状态管理一致性**(单例/SL 混用)上仍有差距。
|
||
|
||
---
|
||
|
||
## 9. 下一阶段建议
|
||
|
||
> 按商业优先级排序,预估工作量(1 人天为单位)。
|
||
|
||
### P2:架构一致性(合计约 3 人天)
|
||
|
||
**9.1 迁移剩余单例到 ServiceLocator**
|
||
|
||
```csharp
|
||
// 在 GameServiceRegistrar.Awake() 中追加注册:
|
||
if (SaveManager.Instance)
|
||
ServiceLocator.Register<ISaveService>(SaveManager.Instance);
|
||
if (GlobalObjectPool.Instance)
|
||
ServiceLocator.Register<IObjectPoolService>(GlobalObjectPool.Instance);
|
||
```
|
||
|
||
保留 `static Instance` 但不对外暴露(`internal`),外部调用方改用 `ServiceLocator.Get<ISaveService>()`。工作量约 1 天。
|
||
|
||
**9.2 EnemyStateType → 敌人 POCO FSM(可选)**
|
||
|
||
成本较高(约 2 天),建议在下一个大版本迭代中推进,当前枚举方案功能完整,仅扩展性有限。可先为 BossBase 单独实现 `BossStateBase` POCO FSM,与普通敌人枚举 FSM 共存。
|
||
|
||
### P3:代码质量(合计约 0.5 人天)
|
||
|
||
**9.3 DashState 魔法数值添加警告**
|
||
|
||
```csharp
|
||
// DashState.cs — OnStateEnter
|
||
if (Cfg == null)
|
||
Debug.LogWarning("[DashState] MovementConfig 未配置,使用默认数值,建议在 PlayerController Inspector 绑定。", _owner);
|
||
_timer = Cfg != null ? Cfg.DashDuration : 0.18f;
|
||
```
|
||
|
||
**9.4 SpeedrunTimer 专用存档字段**
|
||
|
||
```csharp
|
||
// StatsSaveData.cs — 添加字段
|
||
public float SpeedrunTime;
|
||
// SpeedrunTimer.OnSave / OnLoad 改用此字段
|
||
```
|
||
|
||
**9.5 AntiSoftlockSystem 改为事件驱动**
|
||
|
||
```csharp
|
||
// AntiSoftlockSystem.cs
|
||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||
private void OnEnable()
|
||
=> _onPlayerSpawned.Subscribe(t => { _player = t.GetComponent<PlayerController>(); ... }).AddTo(_subs);
|
||
```
|
||
|
||
---
|
||
|
||
## 附录:文件规模统计
|
||
|
||
| 目录 | 文件数 | 说明 |
|
||
|------|--------|------|
|
||
| `Animation/` | 6 | 动画事件绑定,Animancer 配置 |
|
||
| `Audio/` | 9 | AudioManager、BGM、Footstep |
|
||
| `Camera/` | 6 | 房间相机、相机状态机 |
|
||
| `Combat/` | 18 | HitBox/HurtBox、弹射物、状态效果 |
|
||
| `Core/` | ~30 | ServiceLocator、SaveSystem、Pool、Events |
|
||
| `Enemies/` | ~35 | AI(22 个 BD 节点)、Boss、Navigation |
|
||
| `Equipment/` | 14 | 护符系统、工具槽、效果接口 |
|
||
| `Player/States/` | 18 | PlayerController + 16 状态类 + 基类 |
|
||
| `Progression/` | ~18 | 成就系统(12 种条件)、进度锁、HP 容器 |
|
||
| `Quest/` | 9 | 任务系统、挑战房间 |
|
||
| `World/` | ~28 | WorldStateRegistry、谜题、商店、地图 |
|
||
| `Editor/` | ~18 | 7 个 EditorWindow + 验证工具 |
|
||
| **合计** | **~270** | 覆盖完整 2D 动作游戏功能集 |
|
||
|
||
---
|
||
|
||
*本文档由代码评审工具自动生成,基于 `Assets/Scripts/` 所有 .cs 文件的静态分析与架构评估。*
|
||
*上次更新:2025-05-11 | 评审者:GitHub Copilot*
|