312 lines
11 KiB
Markdown
312 lines
11 KiB
Markdown
# SimulationWorld Architecture Specification
|
||
|
||
## 文档定位
|
||
本文件是 `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/*`
|
||
|
||
## 架构目标
|
||
- 将战斗中的敌人、投射物、掉落物运行时状态收口到统一仿真容器。
|
||
- 将热路径逻辑与 Unity 表现层解耦,避免在仿真阶段直接读写 `Transform`。
|
||
- 为 Job/Burst 提供稳定的数据通道、生命周期管理和主线程结算收口点。
|
||
- 保持 `SimulationWorld` 对外是单一战斗调度入口,而不是分散的业务入口集合。
|
||
- 保证扩展新仿真对象或新碰撞规则时,能够沿着既有管线接入,而不是旁路修改。
|
||
|
||
## 非目标
|
||
- 不负责完整战斗规则定义。伤害公式、碰撞业务语义仍由 `AIUtility` 和实体逻辑承担。
|
||
- 不负责实体创建策略。实体创建与隐藏仍由外部流程和 Entity 系统负责。
|
||
- 不追求全局 ECS 化。本模块仍以 `SimulationWorld + partial + Native 容器` 为中心组织。
|
||
- 不在 Job 中直接驱动表现层、事件系统或 Unity 对象生命周期。
|
||
|
||
## 外部依赖与系统边界
|
||
`SimulationWorld` 处于战斗流程中层,位于 `GameStateBattle` 和具体实体逻辑之间。
|
||
|
||
上游依赖:
|
||
- `GameStateBattle.OnUpdate` 驱动每帧 `Tick`。
|
||
- `GameEntry.Event` 提供实体显示/隐藏事件,用于同步仿真容器生命周期。
|
||
- `GameEntry.Entity` 提供实体查询、隐藏和表现事件消费。
|
||
|
||
下游协作:
|
||
- `AIUtility` 负责伤害与碰撞业务结算。
|
||
- Enemy/Projectile/Drop 实体提供初始化所需运行时数据。
|
||
- Presentation 子模块负责把仿真结果写回表现层。
|
||
|
||
边界要求:
|
||
- 外部业务代码不得直接增删 `_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`
|
||
- 命中事件的表现消费桥。
|
||
|
||
## 核心数据所有权
|
||
### 主容器
|
||
- `_enemies`
|
||
- `_projectiles`
|
||
- `_pickups`
|
||
|
||
这些容器是真实仿真态所有者。Job 输入输出缓冲只是当前帧的镜像通道,不是持久源数据。
|
||
|
||
### 绑定关系
|
||
- `EntityBinding` 维护 `EntityId <-> SimulationIndex` 双向映射。
|
||
- 容器删除使用 `swap-back`。
|
||
- 发生尾元素覆盖时,必须同步 `RemapIndex`。
|
||
- 删除完成后再 `Unbind`,避免索引悬挂。
|
||
|
||
### Native 容器
|
||
- Job 通道一律使用 `Allocator.Persistent`。
|
||
- 生命周期由 `InitializeJobDataChannels` / `DisposeJobDataChannels` 集中管理。
|
||
- 帧间复用时使用 `Clear`,不允许用临时重建替代正常复用。
|
||
- Job 数据与主容器数据之间的转换必须集中在 `JobDataChannel` 侧完成。
|
||
|
||
## 生命周期模型
|
||
### 实体进入仿真
|
||
统一由 `EntitySync` 监听实体显示事件后触发:
|
||
- Enemy group -> `RegisterEnemyLifecycle`
|
||
- Drop group -> `RegisterPickupLifecycle`
|
||
- Bullet / Projectile / EnemyProjectile group -> `RegisterProjectileLifecycle`
|
||
|
||
### 实体退出仿真
|
||
统一由 `EntitySync` 监听实体隐藏事件后触发:
|
||
- Enemy -> `UnregisterEnemyLifecycle`
|
||
- Drop -> `UnregisterPickupLifecycle`
|
||
- Projectile 相关 group -> `UnregisterProjectileLifecycle`
|
||
|
||
### 清场
|
||
`ClearSimulationState` 负责:
|
||
- 清空主容器
|
||
- 清空投射物回收与结算缓存
|
||
- 清空区域碰撞请求与命中缓存
|
||
- 清空 Job 通道
|
||
- 清空全部 `EntityBinding`
|
||
|
||
## 运行时执行链路
|
||
### 帧级入口
|
||
1. `GameStateBattle.OnUpdate`
|
||
2. `_enemyManager.OnUpdate(...)`
|
||
3. `SimulationWorld.Tick(...)`
|
||
4. `SimulationWorld.LateUpdate()`
|
||
|
||
### Tick 总流程
|
||
`SimulationWorld.Tick` 是战斗仿真的唯一主入口。
|
||
|
||
约束:
|
||
- 当 `UseSimulationMovement == false` 时,直接返回。
|
||
- `Tick` 只负责逻辑仿真与结算,不直接写 `Transform`。
|
||
|
||
### 每帧仿真管线
|
||
当前实现的标准顺序为:
|
||
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
|
||
- 把输出写回主容器。
|
||
- 在主线程结算碰撞与伤害。
|
||
- 回收失效投射物。
|
||
|
||
### LateUpdate
|
||
`LateUpdate` 只做表现写回,不做逻辑判定:
|
||
- 敌人位置/朝向写回
|
||
- 投射物位置/朝向写回
|
||
|
||
## 线程模型与边界
|
||
### Job/Burst 允许做的事
|
||
- 读取 Job 输入缓冲
|
||
- 写入 Job 输出缓冲
|
||
- 写入 NativeHashMap / NativeList 等碰撞与分桶数据
|
||
- 执行纯数据计算
|
||
|
||
### Job/Burst 禁止做的事
|
||
- 读写 `Transform`
|
||
- 操作 GameObject / Entity 生命周期
|
||
- 调用事件系统
|
||
- 直接调用 `AIUtility.PerformCollision`
|
||
- 进行托管分配、LINQ、装箱
|
||
|
||
### 主线程必须做的事
|
||
- 应用输出到仿真主容器
|
||
- 命中结算与伤害计算
|
||
- 投射物失效回收
|
||
- 命中表现事件派发
|
||
- `LateUpdate` 表现写回
|
||
|
||
## 子系统约束
|
||
### 敌人仿真
|
||
固定接入点:
|
||
- BuildInput
|
||
- Movement
|
||
- Separation
|
||
- WriteBack
|
||
|
||
约束:
|
||
- 敌人状态必须以 `EnemySimData` 为中心流动。
|
||
- 互斥与移动结果必须先写入输出缓冲,再统一提交。
|
||
- 与目标选择相关的空间索引脏标记必须在主容器变更时维护。
|
||
|
||
### 投射物仿真
|
||
固定接入点:
|
||
- BuildInput
|
||
- Movement
|
||
- Collision Query
|
||
- Resolve
|
||
- Recycle
|
||
|
||
约束:
|
||
- 投射物生命周期状态必须由 `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:接生命周期
|
||
如果是新实体类型,先定义 show/hide 到仿真态的映射,再进入执行阶段。
|
||
|
||
### Step 3:接执行管线
|
||
优先复用已有阶段;只有确实无法收纳时才新增阶段,并补可观测的 profiler 标记。
|
||
|
||
### Step 4:接主线程结算
|
||
需要业务判定、伤害、事件派发、实体隐藏时,一律回主线程收口。
|
||
|
||
### Step 5:接表现
|
||
视觉写回、命中反馈、临时特效都放在 `Presentation` 侧。
|
||
|
||
### Step 6:补测试
|
||
至少覆盖:
|
||
- 正常行为
|
||
- 空容器和边界条件
|
||
- 删除后的索引稳定性
|
||
- 碰撞去重或快照语义
|
||
- 与旧路径一致的关键行为
|
||
|
||
### Step 7:更新文档
|
||
修改模块边界、数据契约、不变量或执行阶段时,必须同步更新本文件。
|
||
|
||
## 回归关注点
|
||
- `ClearSimulationState` 是否把主容器、缓存和 binding 一并清干净。
|
||
- 删除路径是否保持 `swap-back + remap` 一致。
|
||
- Job Native 容器是否有泄漏或容量管理回退。
|
||
- 是否在热路径引入托管分配。
|
||
- 是否让表现逻辑重新侵入仿真逻辑。
|
||
- 是否破坏 Area Query 快照语义。
|
||
- 是否破坏碰撞候选与命中去重。
|
||
|
||
## 测试建议
|
||
至少保留并持续扩展以下类型的测试:
|
||
- Tick 行为正确性
|
||
- 主线程与 Job 管线一致性
|
||
- 最近敌查询正确性
|
||
- 投射物候选上限与玩家候选覆盖
|
||
- Area Query 快照语义
|
||
- 清场和 Battle 循环稳定性
|
||
|
||
## 维护原则
|
||
如果未来需要继续扩展为多模式仿真开关、更多 Job 管线层级或新的仿真对象类型,应优先维护以下三点:
|
||
- `Tick` 仍然只有一个主入口
|
||
- 仿真态生命周期仍然只有一个注册/反注册入口
|
||
- 主线程结算与表现写回边界不被打穿
|