11 KiB
SimulationWorld Architecture Specification
文档定位
本文件是 SimulationWorld 的架构规范与扩展开发约束。
用途分为两部分:
- 作为当前
SimulationWorld实现的架构总览,说明模块职责、依赖边界、运行链路和数据所有权。 - 作为后续扩展、重构、性能优化和回归修复时的约束文档,防止破坏核心不变量。
文档与实现冲突时,当前分支源码优先;提交前必须同步修正文档。
适用范围
Assets/GameMain/Scripts/Simulation/*Assets/GameMain/Scripts/Procedure/Game/GameStateBattle.csAssets/GameMain/Scripts/Procedure/Game/ProcedureGame.csAssets/GameMain/Scripts/Utility/AIUtility.csAssets/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.csLateUpdate表现写回。
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
运行时执行链路
帧级入口
GameStateBattle.OnUpdate_enemyManager.OnUpdate(...)SimulationWorld.Tick(...)SimulationWorld.LateUpdate()
Tick 总流程
SimulationWorld.Tick 是战斗仿真的唯一主入口。
约束:
- 当
UseSimulationMovement == false时,直接返回。 Tick只负责逻辑仿真与结算,不直接写Transform。
每帧仿真管线
当前实现的标准顺序为:
- Early Return
DeltaTime <= 0时只清理碰撞临时通道和统计。
- BuildInput
- 将
_enemies/_projectiles同步为 Job 输入。 - 准备敌人输出、投射物输出、碰撞查询缓冲。
- 将
- StateUpdate
- 调度敌人移动 Job。
- 调度投射物移动 Job。
- Schedule
- 按需调度敌人互斥分离 Job。
- 合并敌人与投射物 Job 依赖。
- Complete
- 等待本帧仿真 Job 完成。
- Collision
- 构建碰撞查询。
- 构建敌人碰撞桶。
- 生成候选并统计。
- 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仍然只有一个主入口- 仿真态生命周期仍然只有一个注册/反注册入口
- 主线程结算与表现写回边界不被打穿