UMassBattlePhysicsSubsystem
Source/MassBattle/Public/Subsystems/MassBattlePhysicsSubsystem.h
概述 Overview
承载独立 PBD 物理引擎持久化 2D 物理网格的世界子系统。网格按 block 组织:每个 FPhysicsGridBlock 持有一片预分配的 FPhysicsGridCell,格子内是内联支撑的成员数组 + 细粒度自旋锁(AgentGrid 的 cell/block 模式)。每帧由物理处理器驱动「BeginFrame 复位 → 并行 RegisterEntry 注册 → 串行 FlushPending → FinishFrame 排序 → ShrinkGrid 收缩」的构建循环;构建完成到下一次复位之间格子不可变,任何需要点查询的代码可无锁读取。
World subsystem hosting the persistent 2D physics grid of the standalone PBD physics engine. The grid is block-organized: each FPhysicsGridBlock owns a pre-allocated slab of FPhysicsGridCell, and each cell is an inline-backed member array with a fine-grained spinlock (the AgentGrid cell/block pattern). The physics processor drives a per-frame build loop — BeginFrame reset → parallel RegisterEntry → serial FlushPending → FinishFrame sort → ShrinkGrid shrink; between the build and the next reset the cells are immutable, so anything needing point queries may read them lock-free.
确定性设计:每个成员打包为一个 64 位字 (BiasedUID << 32) | Slot,FinishFrame 的逐格升序排序即得 UID 序——槽位编号本身由票号分配、依赖线程调度,确定性「住在格子里」而非槽位序里。格子占用 ⇔ Members.Num() > 0,无需占用位图,因为物理网格只做点查询、从不做范围迭代。
Determinism design: each member packs into one 64-bit word (BiasedUID << 32) | Slot; FinishFrame's per-cell ascending sort yields UID order — slot numbering itself is ticket-assigned and scheduling-dependent, so determinism "lives in the cells", not in slot order. Occupancy ⇔ Members.Num() > 0; no occupancy bitmap is needed because the physics grid is only point-queried, never range-iterated.
继承 Inheritance
结构体 Structs
单个持久化格子(普通 C++ 结构,非 USTRUCT):内联支撑的成员数组 + 细粒度自旋锁,支持并发追加——即 FHashGridAgentCell 模式。成员字打包 (偏置UID << 32) | 槽位。拷贝语义镜像 FHashGridAgentCell:数据照拷、锁重新初始化;block 由 TSharedPtr 持有,实践上从不被拷贝,拷贝构造仅为保证数组重定位的完备性。
A single persistent cell (plain C++ struct, not a USTRUCT): inline-backed member array + fine-grained spinlock for concurrent appends — the FHashGridAgentCell pattern. Each member word packs (BiasedUID << 32) | Slot. Copy semantics mirror FHashGridAgentCell: data is copied, the lock is re-initialized; blocks are held by TSharedPtr and never copied in practice — the copy constructor exists only so array relocation stays well-formed.
成员变量 Members
| 名称 Name | 类型 Type | 默认值 | 说明 Description |
|---|---|---|---|
LockFlag | mutable std::atomic<bool>(private) | false | 格子自旋锁标志;acquire 交换自旋上锁、release 释放。Cell spinlock flag; acquired via exchange-spin, released via store with release ordering. |
Members | TArray<uint64, TInlineAllocator<8>> | 空 | 成员字数组(内联 8 个,超出转堆);每项为打包的 (BiasedUID<<32)|Slot。占用判定即 Num()>0。Member-word array (8 inline, heap beyond that); each entry is the packed (BiasedUID<<32)|Slot. Occupancy is exactly Num()>0. |
方法 Methods
自旋锁:Lock 以 exchange(true, memory_order_acquire) 自旋等待;Unlock 以 store(false, memory_order_release) 释放。仅并行注册波(RegisterEntry)对「已存在的格子」追加成员时使用;串行 FlushPending 落格不加锁。
Spinlock pair: Lock spins on exchange(true, memory_order_acquire); Unlock releases via store(false, memory_order_release). Used only when the parallel register wave (RegisterEntry) appends to an existing cell; the serial FlushPending appends without locking.
Input
无参数。
No parameters.
Output
无返回值。副作用:置位/清零 LockFlag(自旋期间阻塞其它上锁者)。
No return value. Side effect: sets/clears LockFlag (spinners block while held).
预分配格子块——连续内联内存(AgentGrid block 模式)。每个 block 是 BlockDimensions × BlockDimensions 见方的一片格子,由 Blocks 表以 block 坐标为键、TSharedPtr 持有;指针同时进入平坦表 BlockList 供逐 block 并行 pass 使用。
A block of pre-allocated cells — contiguous inline memory (the AgentGrid block pattern). Each block is a BlockDimensions × BlockDimensions slab of cells, held by TSharedPtr in the Blocks map keyed by block coordinate; its pointer also enters the flat BlockList used by the per-block parallel passes.
成员变量 Members
| 名称 Name | 类型 Type | 默认值 | 说明 Description |
|---|---|---|---|
Cells | TArray<FPhysicsGridCell> | 空(构造时定长) | block 内格子数组;构造函数 FPhysicsGridBlock(int32 InBlockSize) 以 SetNum 一次性定长(InBlockSize = BlockCellCountCache)。Cells inside the block; the constructor FPhysicsGridBlock(int32 InBlockSize) sizes it once via SetNum (InBlockSize = BlockCellCountCache). |
公开方法 Public Methods
USubsystem 生命周期入口:先调父类,再把配置一次性派生为热路径缓存(AgentGrid 的 Initialize 配方)。① CellSize 钳制到 ≥ 1 并求倒数得 InvCellSizeCache;② BlockDimensions 钳制到 [4, 64] 后向下取整为 2 的幂(位涂抹 + 回落),写回成员并派生 BlockDimCache(边长)、BlockDimLog2Cache(移位数)、BlockCellCountCache(每 block 格数)。向下取整保证 ini 永不悄然超出所求的内存量。
USubsystem lifecycle entry: calls the parent first, then derives config into hot-path caches once (the AgentGrid Initialize recipe). ① CellSize is clamped to ≥ 1 and inverted into InvCellSizeCache; ② BlockDimensions is clamped into [4, 64] and rounded DOWN to a power of two (bit-smear + fallback), written back to the member, deriving BlockDimCache (edge), BlockDimLog2Cache (shift) and BlockCellCountCache (cells per block). Rounding down means the ini never silently grows memory beyond the asked value.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
Collection | FSubsystemCollectionBase& | 子系统集合,由引擎传入。Subsystem collection provided by the engine. |
Output
无返回值。副作用:可能改写 CellSize / BlockDimensions(钳制/取幂),并填充 4 个缓存成员。
No return value. Side effects: may rewrite CellSize / BlockDimensions (clamp/pow2) and fills the four cache members.
USubsystem 生命周期出口:调用 ClearAll() 清空全部 block、平坦表与待处理收集器,再调父类。
USubsystem lifecycle exit: calls ClearAll() to wipe all blocks, the flat list and the pending gatherer, then calls the parent.
Input
无参数。
No parameters.
Output
无返回值。副作用:释放全部网格内存。
No return value. Side effect: releases all grid memory.
静态访问器:世界上下文无效时直接返回 nullptr(早退对齐 UMassBattleSubsystem::GetPtr——PIE 关闭期的异步任务上下文可能解析为空);否则经 GEngine->GetWorldFromContextObject(LogAndReturnNull)解析世界并取本子系统。
Static accessor: returns nullptr immediately on an invalid world context (early-out aligned with UMassBattleSubsystem::GetPtr — async-task contexts during PIE shutdown may resolve to null); otherwise resolves the world via GEngine->GetWorldFromContextObject (LogAndReturnNull) and returns this subsystem.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
WorldContext | const UObject* | 任意 UObject 世界上下文。Any UObject world context. |
Output
返回子系统指针;上下文无效或世界解析失败时返回 nullptr,不崩溃。
Returns the subsystem pointer, or nullptr when the context is invalid or the world fails to resolve; never crashes.
静态访问器的引用版本:内部即 GetPtr + check 断言——用于「子系统必须存在」的不变式路径,取不到即崩溃。
Reference version of the static accessor: internally GetPtr + a check assertion — for invariant paths where the subsystem must exist; failing to obtain it crashes by design.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
WorldContext | const UObject* | 任意 UObject 世界上下文。Any UObject world context. |
Output
返回子系统引用;指针为空时触发 check 断言。
Returns a subsystem reference; triggers a check assertion when the pointer is null.
每帧构建第一步:对 block 并行(ParallelFor over BlockList)把每个格子的成员数组 Reset()——只清数量、保留容量,内联数组根本不碰分配器,往昔密集帧的堆增长留作复用。必须在并行注册波开始之前调用(复位先于波内联注册)。
First step of the per-frame build: in parallel over blocks (ParallelFor over BlockList), Reset() every cell's member array — count cleared, capacity kept; inline arrays never touch the allocator, and heap growth from past dense frames is retained for reuse. Must run before the parallel register wave (the reset precedes the wave's inline registrations).
Input
无参数。
No parameters.
Output
无返回值。副作用:全部格子的 Members.Num() 归零(容量保留);block 结构不变。
No return value. Side effect: every cell's Members.Num() drops to zero (capacity kept); the block structure is untouched.
并行安全的注册入口,注册波期间任意线程可调。目标 block 已存在:对目标格上自旋锁后追加打包成员字 ((uint64)BiasedUID << 32) | (uint32)Slot,再解锁;目标 block 缺失:把 FPhysicsCellEntry{Cell, Slot, BiasedUID} 推进 Pending 收集器的线程本地缓冲,由 FlushPending 串行处理。块表在此路径上只做 const Find——变更仅发生在串行 FlushPending。
Parallel-safe registration entry, callable from any thread during the register wave. Block exists: spinlock the target cell, append the packed member word ((uint64)BiasedUID << 32) | (uint32)Slot, unlock. Block missing: push FPhysicsCellEntry{Cell, Slot, BiasedUID} into the Pending gatherer's thread-local buffer for the serial FlushPending. This path only does a const Find on the block map — mutation happens solely in serial FlushPending.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
Cell | const FIntPoint& | 目标格坐标(由 LocationToCell 换算)。Target cell coordinate (derived via LocationToCell). |
Slot | int32 | 注册体的槽位索引(槽位数组下标)。Slot index of the registered body (index into the slot arrays). |
BiasedUID | uint32 | 偏置 UID(uint32(UniqueID ^ 0x80000000),见 FPhysicsCellEntry),作无符号排序键打包进成员字高位。Biased UID (uint32(UniqueID ^ 0x80000000), see FPhysicsCellEntry); packed into the high bits of the member word as the unsigned sort key. |
Output
无返回值。副作用:已有 block 时向目标格追加一个成员字(持格锁);缺失 block 时向 Pending 的线程本地槽追加一条待处理项。
No return value. Side effects: appends one member word to the target cell (under the cell lock) when the block exists; otherwise appends one pending entry to the thread-local slot of Pending.
串行排空缺失 block 的待处理注册项。流程:① Pending.Merge() 合并各线程本地槽(合并后为空则直接返回);② 按确定性序排序——先 Cell.Y,再 Cell.X,最后按打包成员字(BiasedUID|Slot)——待处理项落格顺序与 worker 调度无关;③ 逐项定位/创建 block(新建的 FPhysicsGridBlock 以 BlockCellCountCache 定长,并追加进 BlockList),串行落格——无需格锁。
Serially drains the pending registrations for missing blocks. Flow: ① Pending.Merge() merges per-thread slots (returns immediately if empty after merge); ② sort in deterministic order — Cell.Y, then Cell.X, then the packed member word (BiasedUID|Slot) — so pending entries land in cells in a fixed order regardless of worker scheduling; ③ per entry, locate/create the block (new FPhysicsGridBlock is sized with BlockCellCountCache and appended to BlockList) and append serially — no cell lock needed.
Input
无参数(待处理项来自成员收集器 Pending)。
No parameters (entries come from the member gatherer Pending).
Output
无返回值。副作用:Pending.Merge() 即清空合并输出并搬空各线程本地槽(收集器由此排空);可能新建 block 并写入 Blocks 表与 BlockList;全部待处理项按确定性序落格。
No return value. Side effects: Pending.Merge() itself clears the merged output and drains the per-thread slots (the gatherer is thereby emptied); may create blocks written into the Blocks map and BlockList; all pending entries land in cells in deterministic order.
每帧构建收尾:对 block 并行,把成员数 > 1 的格子用 Algo::IntroSort 按成员字升序排序——升序字序即(偏置UID, 槽位)序,查询侧由此获得 UID 排名的成员。排序后到下一次 BeginFrame 之间格子不可变,可无锁读取。
Per-frame build finish: in parallel over blocks, cells with more than one member are sorted ascending by member word via Algo::IntroSort — ascending word order equals (BiasedUID, Slot) order, giving UID-ranked members to the query side. After the sort and until the next BeginFrame, cells are immutable and read lock-free.
Input
无参数。
No parameters.
Output
无返回值。副作用:重排每个多成员格子内的成员字顺序。
No return value. Side effect: reorders member words within every multi-member cell.
串行收缩(AgentGrid ShrinkGrids 配方),应在 FinishFrame 之后调用:遍历 Blocks,收集「无任何成员」的 block 坐标并从表中移除,随后按 map 顺序重建平坦表 BlockList。效果是让 Blocks 表与每帧 BeginFrame/FinishFrame 的 block 扫描贴合当前帧的活跃范围,而非随历史最大值单调膨胀。帧内网格语义下,被清空的 block 按定义已死,下一帧经 FlushPending 按需重建。列表顺序只喂逐 block 并行 pass,从不参与确定性迭代,故 map 顺序即可。
Serial shrink (the AgentGrid ShrinkGrids recipe), to be called after FinishFrame: walks Blocks, collects coordinates of blocks with no members anywhere, removes them from the map, then rebuilds the flat BlockList in map order. The effect: the Blocks map and the per-frame BeginFrame/FinishFrame block sweeps track the current frame's live extent instead of the monotonic historical maximum. Under frame-internal grid semantics an emptied block is dead by definition and re-creates on demand via FlushPending next frame. List order only feeds per-block parallel passes, never deterministic iteration, so map order is fine.
Input
无参数。
No parameters.
Output
无返回值。副作用:移除全部空 block,重建 BlockList(Blocks 为空时无操作)。
No return value. Side effects: removes all member-less blocks and rebuilds BlockList (no-op when Blocks is empty).
点查询:构建之间无锁可调(const)。目标 block 不存在直接返回;否则遍历该格成员字,把低 32 位解包出的槽位索引逐个传给回调——成员此时已由 FinishFrame 排为 UID 序。这是求解段邻居采集的唯一读入口。
Point query: lock-free between builds (const). Returns immediately if the target block is missing; otherwise walks the cell's member words and passes the unpacked slot index (low 32 bits) to the callback one by one — members are in UID order at this point thanks to FinishFrame. This is the sole read entry for the solve-phase neighbor gather.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
Cell | const FIntPoint& | 查询格坐标。Cell coordinate to query. |
Callback | Func&& | 逐成员回调,签名接受 int32 槽位索引。Per-member callback accepting the int32 slot index. |
Output
无返回值。副作用:无(纯读;block 缺失时不调用回调)。
No return value. Side effects: none (pure read; the callback is not invoked when the block is missing).
清空全部网格状态:Blocks.Empty()、BlockList.Empty()、Pending.Reset()。世界拆除(Deinitialize)与诊断场景使用。
Wipes all grid state: Blocks.Empty(), BlockList.Empty(), Pending.Reset(). Used on world teardown (Deinitialize) and for diagnostics.
Input
无参数。
No parameters.
Output
无返回值。副作用:释放全部 block 与收集器缓冲。
No return value. Side effect: releases all blocks and the gatherer buffers.
缓存格子数学访问器。GetInvCellSize 返回 InvCellSizeCache(= 1/CellSize,乘法换算比除法快);GetMultiCellThreshold 返回 CellSize——候选界(Bound)超过该阈值的巨型单位按多格注册整个圆盘脚印。
Cached cell-math accessors. GetInvCellSize returns InvCellSizeCache (= 1/CellSize; multiplication beats division); GetMultiCellThreshold returns CellSize — giants whose candidacy bound exceeds this threshold register their whole disc footprint across multiple cells.
Input
无参数。
No parameters.
Output
分别返回单元尺寸倒数(cm⁻¹)与多格阈值(cm),均为 Initialize 缓存后的只读值。
Returns the inverse cell size (cm⁻¹) and the multi-cell threshold (cm) respectively — read-only values cached by Initialize.
世界 XY → 格坐标:对 X、Y 各做 FloorToInt(P * InvCellSizeCache)。与旧 FPhysicsGridUtil 数学逐位一致。
World XY → cell coordinate: FloorToInt(P * InvCellSizeCache) per axis. Bit-identical to the legacy FPhysicsGridUtil math.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
P | const FVector& | 世界坐标(cm);仅使用 X/Y。World coordinate (cm); only X/Y are used. |
Output
返回格坐标(可为负;负数由 CellToBlock 的算术右移正确处理)。
Returns the cell coordinate (may be negative; negatives are handled correctly by CellToBlock's arithmetic shift).
邻居遍历界(原 FPhysicsGridUtil::WalkReach):返回 SelfBound + CellSize * WalkMarginCells + MaxCumDisp + 8.f。语义:给定自身候选界与本帧最大累计漂移,以该半径的方形范围覆盖所有格即可保证不丢任何接触对——接触 ⇒ ∃ 点 p 距 self ≤ SelfBound 且落在邻居注册脚印内(帧初圆盘被其累计漂移扩张)⇒ p 所在 cell 必在此界内。WalkMarginCells = 1.5 为编译期常量。
Neighbor walk reach (formerly FPhysicsGridUtil::WalkReach): returns SelfBound + CellSize * WalkMarginCells + MaxCumDisp + 8.f. Semantics: given self's bound and the frame's max cumulative drift, sweeping all cells within this square radius guarantees no contact pair is dropped — contact ⇒ ∃ point p within SelfBound of self AND inside the neighbor's registered footprint (frame-start disc dilated by its cumulative drift) ⇒ p's cell lies within this reach. WalkMarginCells = 1.5 is a compile-time constant.
Input
| 参数 Param | 类型 Type | 说明 Description |
|---|---|---|
SelfBound | float | 自身候选界(cm,= 轴半高 + 半径)。Self candidacy bound (cm; shaft half-height + radius). |
MaxCumDisp | float | 本帧最大累计位移(cm,帧内随子步自适应增长)。Frame's max cumulative displacement (cm; grows adaptively across substeps). |
Output
返回遍历半径(cm);查询侧以此为半边长计算 Min/Max 格范围。
Returns the walk radius (cm); the query side derives its Min/Max cell range from it as a half-extent.
成员变量 Members
配置(Config)
| 名称 Name | 类型 Type | 默认值 | 说明 Description |
|---|---|---|---|
CellSize | float (UPROPERTY Config, EditAnywhere) | 256.f | XY 单元尺寸(cm)。策划可调(配置于 DefaultMassBattle.ini);锁步双端必须同值——格子数学参与模拟。Initialize 中钳制到 ≥ 1。XY cell size (cm). Designer-tunable (configured in DefaultMassBattle.ini); must match across lockstep peers — cell math feeds the sim. Clamped to ≥ 1 in Initialize. |
BlockDimensions | int32 (UPROPERTY Config, EditAnywhere, ClampMin 4 / ClampMax 64) | 16 | block 边长(格,2D 正方形)。Initialize 中向下取整为 [4,64] 内的 2 的幂。越大 Blocks 表越小(Find 缓存命中越高);越小逐 block 并行粒度越细。默认与 Agent 网格 XY 平面对齐(cell 256、block 16)。Block edge in cells (square 2D). Rounded down to a power of two in [4, 64] by Initialize. Larger = smaller Blocks map (fewer Finds miss cache); smaller = finer per-block parallelism. Default mirrors the agent grid's XY plane (cell 256, block 16). |
私有缓存与状态(Private)
Initialize 时自配置一次性派生的热路径常量,默认分别为 16 / 4 / 16×16=256 / 1/256。帧间只读——并行注册/求解波可安全读取,与 AgentGrid 缓存同契约。CellToBlock(算术右移 = 负数地板除)与 CellToIndex(掩码 = 非负取模,补码)依赖它们。
Hot-path constants derived once from config in Initialize; defaults 16 / 4 / 16×16=256 / 1/256 respectively. Read-only during frames — safe from the parallel register/solve waves, same contract as the AgentGrid caches. CellToBlock (arithmetic shift = floor division for negatives) and CellToIndex (mask = non-negative modulo, two's complement) rely on them.
block 表,键为 block 坐标。并行 RegisterEntry 只做 const Find——变更仅发生在串行 FlushPending(与 AgentGrid block 表同契约)。
Block map keyed by block coordinate. Parallel RegisterEntry only does const Find — mutation happens ONLY in serial FlushPending (same contract as the AgentGrid block map).
平坦 block 指针表,供逐 block 并行 pass(BeginFrame/FinishFrame)遍历。FlushPending 追加、ShrinkGrid 重建,flush 之间稳定。
Flat block-pointer list iterated by the per-block parallel passes (BeginFrame/FinishFrame). Appended in FlushPending, rebuilt by ShrinkGrid; stable between flushes.
并行注册波收集的「缺失 block」注册项(FPhysicsCellEntry 定义于 Physics/MassBattlePhysicsTypes.h:cell 坐标 + 槽位 + 偏置 UID)。每线程一槽,由 FlushPending 串行 Merge/排序/落格。
Missing-block registrations gathered from the parallel register wave (FPhysicsCellEntry is defined in Physics/MassBattlePhysicsTypes.h: cell coordinate + slot + biased UID). One slot per thread; merged/sorted/drained serially by FlushPending.
遍历余量常量 1.5f(格),参与 WalkReach 公式。
Walk-margin constant 1.5f (cells); participates in the WalkReach formula.
管线角色 Pipeline Role
在整个发布树中,本子系统的唯一外部使用者是 UMassBattlePhysicsProcessor(其头文件持有 TObjectPtr<UMassBattlePhysicsSubsystem> PhysGrid 成员,cpp 中经 GetPtr 缓存)。每帧调用链(均在 MassBattlePhysicsProcessor.cpp 中验证):
Across the release tree, the sole external consumer of this subsystem is UMassBattlePhysicsProcessor (its header holds a TObjectPtr<UMassBattlePhysicsSubsystem> PhysGrid member, cached via GetPtr in the cpp). Per-frame call chain (all verified in MassBattlePhysicsProcessor.cpp):
| 阶段 Phase | 调用 Calls | 说明 Description |
|---|---|---|
| 注册波头Register wave head | BeginFrame() | 复位先于波内联注册。Reset precedes the wave's inline registrations. |
| 并行注册波Parallel register wave | LocationToCell() / GetMultiCellThreshold() / RegisterEntry() | 普通单位注册中心一格;候选界 > 阈值的巨型单位对 Min/Max 格范围内的每格各发一条注册(整个圆盘脚印)。Regular bodies register their single cell; giants with bound above the threshold register one entry per cell in the Min/Max range (whole disc footprint). |
| 注册波尾(串行)Register wave tail (serial) | FlushPending() → FinishFrame() → ShrinkGrid() | 每帧依次执行;收缩使扫描贴合当前帧活跃范围。Run in order every frame; the shrink keeps sweeps sized to the live extent. |
| 求解段(并行子步)Solve phase (parallel substeps) | WalkReach() + LocationToCell() → ForEachBodyInCell() | 以遍历界算出 Min/Max 格范围,逐格点查询采集邻居槽位。Derives the Min/Max cell range from the walk reach, then point-queries cells to gather neighbor slots. |
| 世界拆除World teardown | Deinitialize() → ClearAll() | 释放全部网格内存。Releases all grid memory. |
补充说明 Notes
① 网格数据属于帧内派生数据,不注册进网络 hash/快照——锁步安全来自双端从相同同步 fragment 跑相同计算(通道规则见 Physics/MassBattlePhysicsTypes.h 头注释)。② FPhysicsGridCell 的拷贝构造刻意不拷贝锁:锁重新初始化,数据照拷。③ 物理网格只做点查询、从不做范围迭代,这是「无占用位图」设计的前提。④ UCLASS 标记为 Config = MassBattle, defaultconfig,显示名 "Mass Physics Grid Subsystem"。
① Grid data is frame-internal derived data and is never registered into the network hash/snapshot — lockstep safety comes from both peers running identical computation from the same synced fragments (channel rule per the Physics/MassBattlePhysicsTypes.h header comment). ② FPhysicsGridCell's copy constructor deliberately does not copy the lock: the lock is re-initialized while the data is copied. ③ The physics grid is only point-queried, never range-iterated — the premise of the "no occupancy bitmap" design. ④ The UCLASS is marked Config = MassBattle, defaultconfig with display name "Mass Physics Grid Subsystem".