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
+76 -75
View File
@@ -1,102 +1,103 @@
---
---
name: simulation-development
description: Maintain and extend VampireLike SimulationWorld (P2 baseline). Use for Simulation data contracts, lifecycle sync, Job/Burst pipeline, collision settlement, and rollback-safe runtime switches.
description: Maintain, review, refactor, and extend VampireLike SimulationWorld architecture. Use when working on SimulationWorld data ownership, entity lifecycle sync, tick pipeline orchestration, Job/Burst data channels, projectile or area collision settlement, target-selection indexing, presentation write-back, or Simulation regression tests and architecture documentation that must preserve core invariants.
---
# Simulation Development
## Quick Start
1. Read the design spec first: `./references/SimulationDevelopmentSkill.md`.
2. If performance conclusions change, sync evidence to `../../docs/P2 Job System + Burst 落地.md`.
3. Classify change scope before coding:
- `SimData/JobData` contracts
- lifecycle sync (`SimulationWorld.EntitySync`)
- Job/Burst execution pipeline (`SimulationWorld.EnemyJobs`, `SimulationWorld.ProjectileJobs`)
- collision query/settlement semantics
- presentation write-back (`SimulationWorld.Presentation`)
4. Decide rollback behavior up front:
- `UseSimulationMovement` off path
- `UseJobSimulation` off path
5. Add/adjust both EditMode and PlayMode regression tests.
1. Read `./references/SimulationDevelopmentSkill.md` first.
2. Treat current code as the source of truth when the reference and implementation diverge.
3. Load only the source files needed for the task from the map below.
4. Keep architecture changes and behavior changes explicit; do not hide them inside unrelated edits.
## Source Map
- Simulation core: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`
- Job data channel: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.JobDataChannel.cs`
- Enemy jobs: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.EnemyJobs.cs`
- Projectile jobs: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.ProjectileJobs.cs`
- Lifecycle sync: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs`
- Presentation sync: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.Presentation.cs`
- Core entry: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.cs`
- Sim state lifecycle: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.SimEntityState.cs`
- Entity lifecycle bridge: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.EntitySync.cs`
- Job data channel: `../../Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs`
- Enemy pipeline: `../../Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.EnemyJobs.cs`
- Projectile pipeline: `../../Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.ProjectileJobs.cs`
- Collision pipeline: `../../Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionPipeline.cs`
- Target selection index: `../../Assets/GameMain/Scripts/Simulation/SimulationWorld.TargetSelectionSpatialIndex.cs`
- Transform write-back: `../../Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.TransformSync.cs`
- Hit presentation bridge: `../../Assets/GameMain/Scripts/Simulation/Presentation/SimulationWorld.HitPresentation.cs`
- Tick context: `../../Assets/GameMain/Scripts/Simulation/SimulationTickContext.cs`
- Index binding: `../../Assets/GameMain/Scripts/Simulation/EntityBinding.cs`
- Battle entry: `../../Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs`
- Battle state gate: `../../Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs`
- Damage/collision utility: `../../Assets/GameMain/Scripts/Utility/AIUtility.cs`
- Global component init: `../../Assets/GameMain/Scripts/Base/GameEntry.Custom.cs`
- Enemy old path gate:
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/MeleeEnemy.cs`
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Enemy/RemoteEnemy.cs`
- Entity index binding: `../../Assets/GameMain/Scripts/Simulation/EntityBinding.cs`
- Battle update entry: `../../Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.cs`
- Procedure-level cleanup: `../../Assets/GameMain/Scripts/Procedure/Game/ProcedureGame.cs`
- Damage and collision utility: `../../Assets/GameMain/Scripts/Utility/AIUtility.cs`
- Regression tests:
- `../../Assets/Tests/Simulation/EditMode/SimulationWorldTickTests.cs`
- `../../Assets/Tests/Simulation/PlayMode/SimulationWorldPlayModeTests.cs`
## Workflow
1. Classify the change before editing:
- simulation state contract
- entity lifecycle mapping
- tick pipeline stage
- collision or area query semantics
- presentation write-back
- test or architecture doc maintenance
2. Preserve the main boundaries:
- `Tick` remains the only simulation logic entry
- lifecycle registration and removal remain centralized
- logic does not write `Transform`
- damage, event dispatch, entity hiding, and recycle stay on the main thread
3. Extend data first when behavior depends on new state:
- update `SimData`
- update job input/output structs
- update conversion and initialization paths
4. Reuse an existing pipeline stage before adding a new one.
5. Update `./references/SimulationDevelopmentSkill.md` when module boundaries, invariants, or execution flow change.
6. Add or adjust Simulation tests for every behavior change.
## Non-Negotiable Invariants
- Maintain `EntityId <-> SimulationIndex` consistency.
- Use swap-back removal (`move last -> remove last -> remap index`).
- Keep lifecycle registration/removal inside `EntitySync` event flow; do not double-write containers from gameplay code.
- Keep logic/presentation boundary:
- Simulation computes logical outputs.
- Presentation writes back `Transform`.
- Keep A/B rollback path (`UseSimulationMovement`/`UseJobSimulation`).
- `SetUseSimulationMovement` and `SetUseJobSimulation` must not hot-switch during `Battle`.
- Keep area query snapshot semantics (`SourceWasActiveAtQueryTime`) intact.
- Keep dodge semantics using `Value` (additive), not `Percent`.
- Avoid new managed allocations in Tick hot paths.
- Keep `_enemies`, `_projectiles`, and `_pickups` as the persistent source of truth.
- Keep `EntityBinding` consistent with container indices.
- Use swap-back removal with remap before unbind.
- Drive container add/remove only through lifecycle sync and sim state helpers.
- Keep target-selection buckets and collision buckets as rebuildable caches, not persistent business state.
- Keep area query snapshot semantics intact.
- Avoid managed allocations and LINQ in hot paths.
## Change Recipes
## Change Guidance
### Add or Change SimData Fields
### Extend Simulation State
1. Update target structs in `Simulation/SimData/` and Job channel structs.
2. Populate defaults in `Create*InitialSimData` / lifecycle registration path.
3. Apply runtime updates in simulation stages.
4. Consume visual fields in `Presentation` only.
5. Ensure backward compatibility when `UseSimulationMovement` is off.
1. Add fields to the relevant sim data and job structs.
2. Populate defaults in the lifecycle registration path.
3. Flow the data through the execution stage that owns it.
4. Consume presentation-only values in Presentation code, not in simulation jobs.
### Extend Job/Burst Pipeline
### Extend Lifecycle Mapping
1. Keep deterministic stage ownership (Build/Schedule/Complete/Commit).
2. Preserve state semantics; avoid UI/audio/effect side effects in simulation loops.
3. Keep `ProfilerMarker` coverage for new or changed stages.
4. Keep hot paths data-driven (no direct `Transform` reads/writes).
1. Add the entity group mapping in `SimulationWorld.EntitySync.cs`.
2. Register and unregister through dedicated sim state helpers.
3. Preserve clear ownership over which container the entity enters.
### Modify Collision / Area Query Behavior
### Extend Tick Pipeline
1. Treat broad phase candidate generation and main-thread settlement as separate steps.
2. Preserve `MaxTargets` semantics across player + enemy candidates.
3. If adding query metadata, flow it through:
- request buffer -> collision query input -> candidate -> settlement.
4. Keep area-source snapshot behavior and avoid runtime-state race regressions.
1. Place logic inside the smallest existing stage that fits.
2. Keep job work data-oriented and side-effect free.
3. Apply outputs back to sim state before any presentation write-back.
### Add or Adjust Runtime Switches
### Extend Collision Behavior
1. Define exact effective timing (`Battle` or out-of-`Battle`) before implementation.
2. For high-risk switches, enforce out-of-battle-only changes.
3. Provide clear warning logs for ignored runtime switch attempts.
1. Separate broad-phase candidate generation from final settlement.
2. Preserve dedup and snapshot behavior on the main thread.
3. Route gameplay effects through the existing main-thread settlement path.
## Validation Checklist
### Extend Presentation
- `UseSimulationMovement = false` and `true` both run correctly.
- `UseJobSimulation = false` and `true` both run correctly under simulation mode.
- No duplicate registration or stale index after entity hide/destroy.
- Battle loop remains stable (`Battle -> LevelUp -> Shop -> Battle`).
- No new per-frame GC spikes in hot paths.
- Main flow has no new Error/Exception logs.
- Keep these regression tests green in both EditMode and PlayMode:
- `TickProjectiles_LimitsCandidatesToMaxTargets_IncludingPlayerCandidate`
- `SetUseSimulationAndJob_AreIgnored_WhenBattleStateIsActive`
- `EnqueueAreaQuery_CapturesInactiveSourceSnapshot_WhenSourceEntityUnavailable`
- Update `./references/SimulationDevelopmentSkill.md` when contracts, boundaries, or rules change.
1. Read simulation output only after logic settlement is complete.
2. Do not mutate simulation state from presentation code.
## Validation
- Verify index stability after removal paths.
- Verify clear/reset paths leave no stale bindings or transient buffers.
- Verify behavior under the relevant Simulation tests.
- Verify the reference doc still matches the code after architectural edits.
@@ -1,4 +1,4 @@
interface:
display_name: "Simulation Development"
short_description: "Maintain and extend VampireLike Simulation architecture"
default_prompt: "Use $simulation-development to implement and validate a Simulation layer change with rollback safety."
short_description: "Extend VampireLike SimulationWorld safely"
default_prompt: "Use $simulation-development to implement, review, or extend a SimulationWorld change while preserving architecture invariants."
@@ -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` 仍然只有一个主入口
- 仿真态生命周期仍然只有一个注册/反注册入口
- 主线程结算与表现写回边界不被打穿