remove open spec skills

This commit is contained in:
2026-03-17 08:58:56 +08:00
parent f809af4e15
commit aa081bcc3c
15 changed files with 353 additions and 2175 deletions
@@ -1,200 +1,311 @@
# Simulation Development Skill (VampireLike)
# SimulationWorld Architecture Specification
## 目标
本文件是 SimulationWorld 的正式设计说明和扩展开发规范。
后续在 Simulation 相关模块做功能扩展、性能优化、回归修复时,统一按本规范执行。
## 文档定位
本文件是 `SimulationWorld` 的架构规范与扩展开发约束。
用途分为两部分:
- 作为当前 `SimulationWorld` 实现的架构总览,说明模块职责、依赖边界、运行链路和数据所有权。
- 作为后续扩展、重构、性能优化和回归修复时的约束文档,防止破坏核心不变量。
文档与实现冲突时,当前分支源码优先;提交前必须同步修正文档。
## 适用范围
- Assets/GameMain/Scripts/Simulation/*
- Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs
- Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs
- Assets/GameMain/Scripts/Utility/AIUtility.cs
- Assets/Tests/Simulation/EditMode/*
- Assets/Tests/Simulation/PlayMode/*
- `Assets/GameMain/Scripts/Simulation/*`
- `Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs`
- `Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs`
- `Assets/GameMain/Scripts/Utility/AIUtility.cs`
- `Assets/Tests/Simulation/EditMode/*`
- `Assets/Tests/Simulation/PlayMode/*`
当前状态:P2 Job/Burst 主体已完成,SimulationWorld 已是战斗核心调度层。
## 架构目标
- 将战斗中的敌人、投射物、掉落物运行时状态收口到统一仿真容器。
- 将热路径逻辑与 Unity 表现层解耦,避免在仿真阶段直接读写 `Transform`。
- 为 Job/Burst 提供稳定的数据通道、生命周期管理和主线程结算收口点。
- 保持 `SimulationWorld` 对外是单一战斗调度入口,而不是分散的业务入口集合。
- 保证扩展新仿真对象或新碰撞规则时,能够沿着既有管线接入,而不是旁路修改。
## 模块分层
SimulationWorld 使用 partial 拆分,职责如下:
## 非目标
- 不负责完整战斗规则定义。伤害公式、碰撞业务语义仍由 `AIUtility` 和实体逻辑承担。
- 不负责实体创建策略。实体创建与隐藏仍由外部流程和 Entity 系统负责。
- 不追求全局 ECS 化。本模块仍以 `SimulationWorld + partial + Native 容器` 为中心组织。
- 不在 Job 中直接驱动表现层、事件系统或 Unity 对象生命周期。
- SimulationWorld.cs
- 开关、主容器、绑定、主 Tick 入口。
- SimulationWorld.EntitySync.cs
- 实体 Show/Hide 到仿真容器的生命周期同步。
- SimulationWorld.EnemyJobs.cs
- 敌人移动和互斥分离的 Job/Burst 链路。
- SimulationWorld.ProjectileJobs.cs
- 投射物移动、寿命、碰撞候选、主线程结算。
- SimulationWorld.JobDataChannel.cs
- Native 容器、拷贝转换、容量管理、运行时统计。
- SimulationWorld.TargetSelectionSpatialIndex.cs
- 目标选择空间索引(最近敌人查询)。
- SimulationWorld.Presentation.cs
- 表现层写回(Transform)和命中表现事件消费。
## 外部依赖与系统边界
`SimulationWorld` 处于战斗流程中层,位于 `GameStateBattle` 和具体实体逻辑之间。
## 运行时执行链路
1. GameStateBattle.OnUpdate
- 先执行 EnemyManager.OnUpdate
- 再执行 SimulationWorld.Tick
上游依赖:
- `GameStateBattle.OnUpdate` 驱动每帧 `Tick`。
- `GameEntry.Event` 提供实体显示/隐藏事件,用于同步仿真容器生命周期。
- `GameEntry.Entity` 提供实体查询、隐藏和表现事件消费。
2. SimulationWorld.Tick
- UseSimulationMovement = false:直接返回(完全回退旧链路)
- UseSimulationMovement = true 且 UseJobSimulation = false:走主线程敌人仿真
- UseSimulationMovement = true 且 UseJobSimulation = true:走 Job/Burst 总链路
下游协作:
- `AIUtility` 负责伤害与碰撞业务结算。
- Enemy/Projectile/Drop 实体提供初始化所需运行时数据。
- Presentation 子模块负责把仿真结果写回表现层。
3. SimulationWorld.LateUpdate
- 调用 Presentation.OnLateUpdate
- 统一写回 Enemy/Projectile 表现
边界要求:
- 外部业务代码不得直接增删 `_enemies`、`_projectiles`、`_pickups`。
- 外部业务代码不得绕过 `SimulationWorld` 直接维护仿真索引。
- `SimulationWorld` 不直接拥有实体生成权,只消费实体生命周期事件。
## 核心数据契约
## 模块结构
`SimulationWorld` 使用 `partial` 拆分,职责按以下边界划分:
- `SimulationWorld.cs`
- 核心组件入口、主状态容器、基础依赖、Unity 生命周期入口。
- `SimulationWorld.SimEntityState.cs`
- 敌人、投射物、掉落物的仿真态创建、更新、删除和清空。
- `SimulationWorld.EntitySync.cs`
- 监听实体 show/hide 事件,将实体生命周期映射到仿真容器。
- `DataChannel/SimulationWorld.JobDataChannel.cs`
- Native 容器持有、初始化、清理、容量准备、仿真数据到 Job 数据的转换。
- `Jobs/SimulationWorld.EnemyJobs.cs`
- 每帧仿真主编排、敌人移动与互斥分离 Job 调度。
- `Jobs/SimulationWorld.ProjectileJobs.cs`
- 投射物移动、寿命处理、越界回收。
- `Jobs/SimulationWorld.CollisionPipeline.cs`
- 投射物与区域碰撞查询构建、候选筛选、主线程命中结算。
- `SimulationWorld.TargetSelectionSpatialIndex.cs`
- 敌人目标选择空间索引。
- `Presentation/SimulationWorld.TransformSync.cs`
- `LateUpdate` 表现写回。
- `Presentation/SimulationWorld.HitPresentation.cs`
- 命中事件的表现消费桥。
## 核心数据所有权
### 主容器
- List<EnemySimData> _enemies
- List<ProjectileSimData> _projectiles
- List<PickupSimData> _pickups
- `_enemies`
- `_projectiles`
- `_pickups`
这些容器是真实仿真态所有者。Job 输入输出缓冲只是当前帧的镜像通道,不是持久源数据。
### 绑定关系
- EntityBinding 维护 EntityId <-> SimulationIndex 双向映射。
- 删除必须使用 swap-back:
- 尾元素覆盖删除位
- RemapIndex
- RemoveAt(last)
- `EntityBinding` 维护 `EntityId <-> SimulationIndex` 双向映射。
- 容器删除使用 `swap-back`。
- 发生尾元素覆盖时,必须同步 `RemapIndex`。
- 删除完成后再 `Unbind`,避免索引悬挂。
### Job 通道
- EnemyJobInput/Output
- ProjectileJobInput/Output
- CollisionQuery/CollisionCandidate
- NativeParallelMultiHashMap(互斥桶、碰撞桶、目标桶)
### Native 容器
- Job 通道一律使用 `Allocator.Persistent`。
- 生命周期由 `InitializeJobDataChannels` / `DisposeJobDataChannels` 集中管理。
- 帧间复用时使用 `Clear`,不允许用临时重建替代正常复用。
- Job 数据与主容器数据之间的转换必须集中在 `JobDataChannel` 侧完成。
统一规则:
- Allocator.Persistent 分配
- Initialize/Dispose 集中管理
- Clear 只清容器,不破坏生命周期
## 生命周期模型
### 实体进入仿真
统一由 `EntitySync` 监听实体显示事件后触发:
- Enemy group -> `RegisterEnemyLifecycle`
- Drop group -> `RegisterPickupLifecycle`
- Bullet / Projectile / EnemyProjectile group -> `RegisterProjectileLifecycle`
## 不可破坏的设计约束
### 生命周期单入口
仿真容器增删只能由 EntitySync 驱动。
禁止在 Enemy/Weapon/Projectile 业务代码中直接改仿真容器。
### 实体退出仿真
统一由 `EntitySync` 监听实体隐藏事件后触发:
- Enemy -> `UnregisterEnemyLifecycle`
- Drop -> `UnregisterPickupLifecycle`
- Projectile 相关 group -> `UnregisterProjectileLifecycle`
### 逻辑与表现边界
- Simulation 只产出逻辑数据,不直接写 Transform。
- Transform 写回只能在 Presentation。
- 命中表现通过事件缓冲在主线程提交。
### 清场
`ClearSimulationState` 负责:
- 清空主容器
- 清空投射物回收与结算缓存
- 清空区域碰撞请求与命中缓存
- 清空 Job 通道
- 清空全部 `EntityBinding`
### 开关和回滚
- UseSimulationMovement:总开关,支持一键回滚。
- UseJobSimulation:Job 开关,支持 P1.5/ P2 对照。
- UseBurstJobs:Burst 开关。
## 运行时执行链路
### 帧级入口
1. `GameStateBattle.OnUpdate`
2. `_enemyManager.OnUpdate(...)`
3. `SimulationWorld.Tick(...)`
4. `SimulationWorld.LateUpdate()`
### 生效时机约束(重要)
- SetUseSimulationMovement / SetUseJobSimulation 在 Battle 中会被忽略。
- 这两个开关不支持战斗内热切换,只允许战斗外修改生效。
## 敌人和投射物执行模型
### 敌人(Job)
固定阶段:
- BuildInput
- Move
- Separation
- Commit
### Tick 总流程
`SimulationWorld.Tick` 是战斗仿真的唯一主入口。
约束:
- 热路径禁止 LINQ 和托管分配
- 不读写 Transform
- 阶段必须可独立 Profile
- 当 `UseSimulationMovement == false` 时,直接返回。
- `Tick` 只负责逻辑仿真与结算,不直接写 `Transform`。
### 投射物(Job)
包含:
- 移动更新
- 寿命和越界回收
- Broad Phase 候选构建
- 主线程命中结算和回收
### 每帧仿真管线
当前实现的标准顺序为:
1. Early Return
- `DeltaTime <= 0` 时只清理碰撞临时通道和统计。
2. BuildInput
- 将 `_enemies` / `_projectiles` 同步为 Job 输入。
- 准备敌人输出、投射物输出、碰撞查询缓冲。
3. StateUpdate
- 调度敌人移动 Job。
- 调度投射物移动 Job。
4. Schedule
- 按需调度敌人互斥分离 Job。
- 合并敌人与投射物 Job 依赖。
5. Complete
- 等待本帧仿真 Job 完成。
6. Collision
- 构建碰撞查询。
- 构建敌人碰撞桶。
- 生成候选并统计。
7. WriteBack
- 把输出写回主容器。
- 在主线程结算碰撞与伤害。
- 回收失效投射物。
## 碰撞和伤害结算规范
### Broad Phase
候选由 _collisionQueryInputs + _enemyCollisionBuckets 计算。
MaxTargets 必须覆盖“玩家候选 + 敌人候选”的总量。
### LateUpdate
`LateUpdate` 只做表现写回,不做逻辑判定:
- 敌人位置/朝向写回
- 投射物位置/朝向写回
### Area Query 快照语义
- 入队时记录 SourceWasActiveAtQueryTime。
- 结算按快照判定来源有效性,避免查询后状态变化导致误判。
## 线程模型与边界
### Job/Burst 允许做的事
- 读取 Job 输入缓冲
- 写入 Job 输出缓冲
- 写入 NativeHashMap / NativeList 等碰撞与分桶数据
- 执行纯数据计算
### 主线程结算
- Projectile 命中:按 ImpactData + AIUtility.CalcDamageHP。
- Area 命中:调用 AIUtility.PerformCollision(target, source, true)。
### Job/Burst 禁止做的事
- 读写 `Transform`
- 操作 GameObject / Entity 生命周期
- 调用事件系统
- 直接调用 `AIUtility.PerformCollision`
- 进行托管分配、LINQ、装箱
### 伤害公式约束
AIUtility.CalcDamageHP:
- 闪避使用 dodgeStat.Value(加算语义),不使用 Percent。
- 攻击: (attack + AttackStat.Value) * AttackStat.Percent
- 防御: (damage - DefenseStat.Value) / DefenseStat.Percent
- 最终伤害最小值为 1。
### 主线程必须做的事
- 应用输出到仿真主容器
- 命中结算与伤害计算
- 投射物失效回收
- 命中表现事件派发
- `LateUpdate` 表现写回
## 已修复问题(纳入长期约束)
1. 闪避语义修正:使用 Value 而非 Percent。
2. UseSimulationMovement / UseJobSimulation 战斗内禁止热切换。
3. MaxTargets 统计覆盖玩家候选,避免超额候选。
4. Area 查询引入来源活跃快照并按快照结算。
## 子系统约束
### 敌人仿真
固定接入点:
- BuildInput
- Movement
- Separation
- WriteBack
后续改动若触碰这些路径,必须保持行为不回退。
约束:
- 敌人状态必须以 `EnemySimData` 为中心流动。
- 互斥与移动结果必须先写入输出缓冲,再统一提交。
- 与目标选择相关的空间索引脏标记必须在主容器变更时维护。
## 扩展开发 SOP
### Step 0:定义模式和回滚
- 明确功能在哪条路径生效(Simulation / Job / Burst)。
- 明确开关关闭后的回退行为。
### 投射物仿真
固定接入点:
- BuildInput
- Movement
- Collision Query
- Resolve
- Recycle
### Step 1:扩数据
- 先改 SimData 和 JobData。
- 再改 CreateInitialSimData 与转换函数。
约束:
- 投射物生命周期状态必须由 `ProjectileSimData.Active` 和 `State` 共同表达。
- 投射物实际隐藏与移除只能在主线程回收阶段完成。
### 碰撞管线
职责:
- 构建投射物查询和区域查询
- 生成 broad-phase 候选
- 在主线程做最终业务结算
约束:
- Broad-phase 只能筛候选,不能替代最终命中判定。
- Area Query 必须保留 `SourceWasActiveAtQueryTime` 快照语义。
- 候选去重与区域命中去重只能在主线程收口。
### 目标选择空间索引
职责:
- 提供按位置查询最近敌人的能力。
约束:
- 仅在仿真启用时对外提供结果。
- 敌人主容器变更后必须标记脏。
- 索引是缓存,不是源数据;源数据仍是 `_enemies`。
### Presentation
职责:
- 将仿真层结果写回表现层。
- 消费命中表现事件。
约束:
- Presentation 只消费仿真结果,不反向修改仿真逻辑状态。
- 任何新增表现都应接在 `Presentation` 子模块,而不是接回 Job 或业务结算热路径。
## 不可破坏的不变量
### 生命周期单入口
仿真容器的增删必须只经过 `EntitySync` 和 `SimEntityState`。
### 数据单一事实来源
主容器是持续态事实来源,Job 缓冲只是帧级副本。
### 逻辑与表现分离
逻辑阶段不写 `Transform`,表现阶段不做业务结算。
### 索引一致性
任何 `swap-back` 删除都必须同步 remap,否则视为架构级错误。
### 主线程结算收口
伤害、事件派发、实体隐藏和回收必须回到主线程。
### 空间索引与碰撞桶是缓存
它们可以重建,不可被外部业务当作持久数据依赖。
## 扩展开发流程
### Step 0:判定接入位置
先判断新需求属于:
- 新仿真态字段
- 新执行阶段逻辑
- 新碰撞查询类型
- 新表现桥接
不要一开始就直接改 `Tick` 主流程。
### Step 1:扩状态
先补 `SimData`、必要的 Job 输入输出结构和转换逻辑。
### Step 2:接生命周期
- 只在 EntitySync 增加注册/反注册。
- 保持 group 到容器映射清晰。
如果是新实体类型,先定义 show/hide 到仿真态的映射,再进入执行阶段。
### Step 3:接执行阶段
- 优先放入现有阶段(Build/Schedule/Commit)。
- 新阶段必须补 ProfilerMarker。
### Step 3:接执行管线
优先复用已有阶段;只有确实无法收纳时才新增阶段,并补可观测的 profiler 标记。
### Step 4:接结算和表现
- 逻辑结算收口到主线程。
- 表现写回放在 Presentation。
### Step 4:接主线程结算
需要业务判定、伤害、事件派发、实体隐藏时,一律回主线程收口。
### Step 5:补测试
EditMode 和 PlayMode 同步补回归,至少覆盖:
- 行为正确性
- 开关路径
- 索引稳定性
- 新增边界条件
### Step 5:接表现
视觉写回、命中反馈、临时特效都放在 `Presentation` 侧。
### Step 6:更新文档
- 更新本文件。
- 需要性能结论时,同步更新 docs/P2 Job System + Burst 落地.md。
### Step 6:补测试
至少覆盖:
- 正常行为
- 空容器和边界条件
- 删除后的索引稳定性
- 碰撞去重或快照语义
- 与旧路径一致的关键行为
## 测试命令
- PlayMode
- Unity -batchmode -nographics -projectPath . -runTests -testPlatform PlayMode -testResults Logs/playmode-test-results.xml -logFile Logs/playmode-tests.log
- EditMode
- Unity -batchmode -nographics -projectPath . -runTests -testPlatform EditMode -testResults Logs/editmode-test-results.xml -logFile Logs/editmode-tests.log
### Step 7:更新文档
修改模块边界、数据契约、不变量或执行阶段时,必须同步更新本文件。
## 关键回归用例(必须保留)
- TickEnemies_MatchesOutput_WhenBurstJobsToggled
- TryGetNearestEnemyEntityId_SelectsNearestBucketCandidate_WhenJobSimulationEnabled
- TickProjectiles_LimitsCandidatesToMaxTargets_IncludingPlayerCandidate
- SetUseSimulationAndJob_AreIgnored_WhenBattleStateIsActive
- EnqueueAreaQuery_CapturesInactiveSourceSnapshot_WhenSourceEntityUnavailable
## 回归关注点
- `ClearSimulationState` 是否把主容器、缓存和 binding 一并清干净。
- 删除路径是否保持 `swap-back + remap` 一致。
- Job Native 容器是否有泄漏或容量管理回退。
- 是否在热路径引入托管分配。
- 是否让表现逻辑重新侵入仿真逻辑。
- 是否破坏 Area Query 快照语义。
- 是否破坏碰撞候选与命中去重。
对应文件:
- Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs
- Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs
## 测试建议
至少保留并持续扩展以下类型的测试:
- Tick 行为正确性
- 主线程与 Job 管线一致性
- 最近敌查询正确性
- 投射物候选上限与玩家候选覆盖
- Area Query 快照语义
- 清场和 Battle 循环稳定性
## P2 验收口径
- 3k 敌人下 Main Thread 明显下降(目标 >= 30%)。
- 战斗持续帧 GC Alloc 接近 0。
- Battle -> LevelUp -> Shop -> Battle 循环稳定。
## 提交前门禁清单
- 关闭 UseSimulationMovement 是否完全回退旧链路。
- EntityBinding 是否保持双向一致,删除后是否正确 remap。
- Job 容器是否无泄漏(Persistent 都可 Dispose)。
- 是否引入新热路径 GC(LINQ、临时集合、装箱)。
- 是否破坏“战斗内不热切换 UseSimulationMovement/UseJobSimulation”。
- 是否同步更新测试和本设计文档。
## 维护原则
如果未来需要继续扩展为多模式仿真开关、更多 Job 管线层级或新的仿真对象类型,应优先维护以下三点:
- `Tick` 仍然只有一个主入口
- 仿真态生命周期仍然只有一个注册/反注册入口
- 主线程结算与表现写回边界不被打穿