MassBattle Docs

MassBattle 插件文档

基于 UE5.6 Mass Entity 框架的大规模战斗插件:确定性锁步网络单一渲染管线数据驱动 ECS。本站在同一份代码上呈现完整的设计理念与逐函数 API 参考,支持中英双语切换。 A large-scale battle plugin built on UE5.6's Mass Entity framework: deterministic lockstep networking, a single rendering pipeline, and a data-driven ECS. This site presents the full design philosophy and per-function API reference from the same codebase, with Chinese/English language switching.

从哪里开始 Where to Start

模块总览 Module Overview

设计理念 Design Principles

以下是 MassBattle 的核心设计理念,提炼自 MassBattleSkills 参考库、CLAUDE.md 开发原则与项目记忆中的架构决策记录。

The core design principles of MassBattle below are distilled from the MassBattleSkills reference library, CLAUDE.md development principles, and the project memory of architectural decisions.

1. 数据驱动 ECS:配置与运行时分离 Data-Driven ECS — Configuration Separated from Runtime

MassBattle 的一切行为都由数据配置驱动:实体从 EntityConfig 模板构建,行为、动画、特效、投射物、掉落等全部挂在 DataAsset 上,处理器只消费 fragment 数据,不持有游戏逻辑。查询声明走 FEntityQueryBuilder 流式 API,access 模式(MARO/MARW)与 tag 过滤在 ConfigureQueries 一处写清。fragment 是数据载体,也有严格的生命周期纪律:spawn 调用会使缓存 fragment 引用失效,每次迭代必须重新获取;archetype 变更(增删 tag)后指针失效。跨版本数据演进用 DataVersion 整数门控的 PostLoad 迁移,取代哨兵字段。与 Blueprint 交互的类型必须映射到 BP 引脚(uint8/int32/float 等),不兼容类型在 UHT 期即报错。

Everything in MassBattle is configuration-driven: entities are built from EntityConfig templates, and behavior, animation, FX, projectile and loot data all live in DataAssets; processors only consume fragment data and hold no game logic. Queries are declared through the FEntityQueryBuilder fluent API, with access modes (MARO/MARW) and tag filters spelled out once in ConfigureQueries. Fragments are the data carrier with strict lifetime discipline: spawn calls invalidate cached fragment references (refetch every iteration), and pointers go stale after an archetype composition change. Cross-version data evolution uses DataVersion-gated PostLoad migration instead of sentinel flags. Types exposed to Blueprint must map to BP pin types (uint8/int32/float, etc.); incompatible types fail at UHT time.

来源:Sources: references/patterns.md + references/mass-safety.md + references/data-migration.md + references/engine-compat.md + references/blueprint-exposure.md + memory/query-struct-snapshot-sync.md
关键事实(点击展开)Key facts (click to expand)
  • 查询声明必须用 FEntityQueryBuilder,禁止裸 AddRequirement/AddTagRequirement/AddSharedRequirement;ConstSharedFragment + MARW 触发 static_assert(references/patterns.md ConfigureQueries Fluent Builder)
  • SpawnActorHost/SpawnFxHost/SpawnSoundHost 内部用 BuildEntityDefer,可使缓存 fragment 引用失效,必须每次迭代重新获取(references/mass-safety.md)
  • 通过 GetFragmentPtr<T> 获得的指针在 archetype 变更(已执行的 tag/fragment 增删)后不可再用;Defer 操作未执行前指针仍有效(references/mass-safety.md)
  • 数据迁移一律用 DataVersion 整数比较(major*10000+minor*100+patch),1.17.6 起禁止新增 bMigrated* 哨兵;MarkPackageDirty() 保证迁移落盘(references/data-migration.md)
  • BP 不兼容类型清单:uint16/uint32/uint64、int8/int16、FFloat16、裸指针、智能指针、STL 类型;uint32 bitmask/bitfield 是例外(references/blueprint-exposure.md)
  • FMassBattleQuery/FEntityQuery 的 fragment 进快照需 QueryStruct policy + FStructSerializer + 恢复端 MarkCacheDirty() 惰性重建(memory/query-struct-snapshot-sync.md)
  • UE 5.7+ fragment 需 trivially-copyable,非平凡拷贝用集中宏 MASS_FRAGMENT_ACCEPT_NOT_TRIVIALLY_COPYABLE,禁止手写特化(references/engine-compat.md)
2. 确定性锁步网络:hash 折叠、快照恢复、resync Deterministic Lockstep Networking — Hash Folding, Snapshot Restore, Resync

锁步网络的核心命题是「同输入同 seed 必得位级相同状态」。GameState 承载下行 Multicast(命令广播、同步 tick、失步检测、agreed hash),PlayerController 承载上行 Server RPC(命令上报、tick 确认、resync 请求),所有对等端在同一 ExecuteTick 消费命令。最关键的纪律是:hash 折叠的是状态,命令是输入、永远不进 hash;RPC 处理器与引擎回调(PostLogin/BeginPlay/OnRep)禁止直接查询 EntityManager,只能置标志或读预计算缓存,快照必须由固定执行序的 Processor 捕获。客户端有四档调速/踢出地平线(落后 ≤3K 加速、3K~300 本地快进、≥300 服务器快照、ack 停滞 600+ 踢出)。恢复路径(FF/快照 apply)必须与正常模拟同源同时序;失步检测用「未来 tick break」规避 PIE 同进程 tick 速率差;TCP 快照通道是 resync 硬依赖,端口选择零失败容忍(范围扫描 + port 0 回读)。

The core proposition of lockstep networking is "same input on the same seed yields byte-identical state." GameState carries downlink multicasts (command broadcast, sync tick, desync check, agreed hashes); PlayerController carries uplink Server RPCs (command reporting, tick ack, resync requests); all peers consume commands at the same ExecuteTick. The cardinal discipline: the hash folds STATE; commands are INPUTS and never enter the hash. RPC handlers and engine callbacks must never query the EntityManager directly — they set flags or read precomputed caches, and snapshots are captured by a Processor at a fixed point in the pipeline. Clients pace through a four-tier horizon (≤3K ticks behind: accelerate; 3K~300: local fast-forward; ≥300: server snapshot; ack stall 600+: kick). Recovery paths must mirror normal paths in source and timing; desync checks use "future-tick break" to survive PIE same-process tick-rate skew; the TCP snapshot channel is a hard dependency of resync — port selection tolerates zero failure (range scan + port-0 readback).

来源:Sources: references/network-lockstep.md + references/determinism.md + references/lockstep-lessons.md + references/network-registries.md + memory/pie-desync-check-fix.md + memory/tcp-snapshot-port-conflict-pie.md + memory/tcp-transport-socket-lifecycle.md + memory/local-snapshot-gapcommands-apply-lag.md + memory/pressure-field-resync-mismatch.md
关键事实(点击展开)Key facts (click to expand)
  • "If two players run the same input on the same seed, will this produce byte-identical state?" 是每条新代码路径的检验标准(references/determinism.md)
  • RPC 处理器/引擎回调禁调 GetMatchingEntities/ForEachEntityChunk/MassAPI 查询;按需快照是反模式,正确模式是入队 + 下一确定性 tick 捕获(references/determinism.md Execution Order Determinism)
  • 快照捕获的 CaptureAgentResyncSnapshot 因从 RPC handler 直查实体而被删除,替换为 bForceSnapshotNextTick + CacheLatestSnapshot()(references/determinism.md Concrete Lesson)
  • "hash matched" 永远不证明 FF 收敛:两帧快照实体状态相同但客户端瞬态不同,快照 tick 处 hash 相同,重放一步后即发散(references/lockstep-lessons.md rule 1)
  • PIE 同进程双端 tick 速率不同步(server 30-44Hz / client 2-3Hz),同一 tick 号不代表同一模拟时刻;EvaluateDeferredDesyncChecks 升序遍历 ServerHashHistory 遇 TickCount > GetTickCount() 立即 break(memory/pie-desync-check-fix.md)
  • TCP 快照发送失败 = 客户端永久死锁:端口用 URL.Port+100 范围扫描到 +109,失败 fallback port 0 并读回 OS 实际分配端口经 RPC 传给客户端(memory/tcp-snapshot-port-conflict-pie.md)
  • 死 socket 残留(UE GetConnectionState 永不报告断开)导致重连卡死;修复 = recv 探测 + send 失败自愈 + Release/Reuse 槽位闭环(memory/tcp-transport-socket-lifecycle.md)
  • 本地恢复的 GapCommands 半开区间 [T0, N) + apply 滞后 1 tick 会漏掉 ExecuteTick == N+1 的命令;修复 = apply 当刻重捕 CopyReceivedCommandsRange(PayloadTick, TickCount + 1)(memory/local-snapshot-gapcommands-apply-lag.md)
  • resync 路径帧末 RebuildAllGrids 用快照基准重建压力场,与服务器 MoveA 帧初基准差 1 tick → pressure 消费者每帧放大 → 失步循环;修复 = 删除 resync 路径重建(memory/pressure-field-resync-mismatch.md)
3. 确定性纪律:UID 分配、排序键、并行聚合 Determinism Discipline — UID Allocation, Sort Keys, Parallel Gathering

确定性不是靠运气,而是靠一套可审计的纪律。全库排序的硬规则是:排序键禁用实体句柄 Index——resync 重建按 (EntityType, UID) 序 BuildEntity,恢复端句柄序与服务器历史创建序不同,任何以 Index 为键的排序恢复后必然分叉(用户硬规则:连「理论安全」也不允许)。跨实体排序一律用 FDeterminism::UniqueID 做主键,本地实体(UID=-1)映射 MIN_INT 排前。UID 规范:invalid=-1;通用实体 UID 走 FEntityUIDAllocator 槽位分配器(2^24 槽位图 + 循环利用,首个 valid=0,占用计数折叠进每 tick 哈希,恢复端经 ReconcileUIDAllocator 从重建映射收敛——2026-08-16 取代退役的 NextGlobalUID 计数器),网络 ID 走服务器权威的 NextLockstepNetID 计数器,两空间互不消耗;障碍物 UID 另用独立槽位空间 [2^30, 2^31)。并行跨 chunk 聚合用 FParallelGatherer<T>(thread_local 槽 + 首次访问一次原子;超 64 并发线程透明扩出按硬件定容的溢出表),消费前按稳定键排序——并行 ID 分配、TSet/TMap 遍历顺序都是非确定性的。同构硬件上 IEEE 754 float 位级确定,定点只保留给物理积分核心 MoveProcessorFP;排查发散时禁止归因 1 ULP 浮点差,位级差异必然是确定性逻辑 bug。

Determinism is not luck — it is an auditable discipline. The hard rule for all sorting: sort keys must NEVER use entity handle Index — resync rebuilds entities in (EntityType, UID) order, so handle ordering on the restored peer differs from the server's historical creation order, and any Index-keyed sort diverges after restore (user rule: not allowed even when provably safe). Cross-entity sorts use FDeterminism::UniqueID as primary key; local entities (UID=-1) map to MIN_INT to sort first. UID convention: invalid=-1; general-entity UIDs come from the FEntityUIDAllocator slot allocator (2^24-slot bitmap with recycling, first valid=0; the occupied count is folded into the per-tick hash and the restore side converges it via ReconcileUIDAllocator — it replaced the retired NextGlobalUID counter on 2026-08-16), while network ids use the server-authoritative NextLockstepNetID counter — the two spaces never consume each other; obstacle UIDs use their own slot space [2^30, 2^31). Parallel cross-chunk aggregation uses FParallelGatherer<T> (thread-local slots, one atomic on first access; machines beyond 64 concurrent threads transparently grow a hardware-sized overflow table), sorted by stable key before consumption — parallel ID allocation and TSet/TMap iteration order are non-deterministic. On homogeneous hardware IEEE 754 float is bit-deterministic; fixed-point is reserved for the physics-integration core (MoveProcessorFP). When hunting divergence, explaining away ~1 ULP float differences is forbidden — a bit-level difference is a deterministic logic bug.

来源:Sources: references/determinism.md + references/lockstep-lessons.md + references/patterns.md + memory/uid-system-convention.md + memory/handle-index-sort-resync.md + memory/loot-determinism-lessons.md + memory/no-ulp-explanations-rule.md + memory/float-timer-non-physics-processors.md + memory/pool-heap-replaces-sort.md
关键事实(点击展开)Key facts (click to expand)
  • 10 点确定性检查清单:并行槽不相交、Index-order reduce(禁 TSet/TMap)、UniqueID 决胜键、gatherer 消费前排序、并行 ID 生成禁、per-entity RandomStream(按 UID 播种)、确定性模式 if (bDeterministic) 门控、稳定原地移除(references/determinism.md 10-Point Checklist)
  • 三种 Index 排序反模式全部已修复:纯 Index 键(HostEntityRecycleQueue)、主键唯一+Index 次键(FxEntitySpawns)、UID/-1 混合 fallback Index(ValidTargets/TraceResults/EntitiesToDestroyQueue)(references/determinism.md Sort-Key 硬规则)
  • 排序比较器内禁止 GetFragmentPtr(池内含已销毁实体残留句柄会触发 Mass IsEntityValid 断言崩溃);先 RemoveAt 失效句柄再访问 fragment(references/determinism.md 2026-08-05 崩溃教训)
  • NextGlobalUID 曾是 int64 + reinterpret_cast<volatile int32*> 原子增,初始 -1 时低 32 位 0xFFFFFFFF → 首次自增后为巨负数进快照,<= ReservedUID 恒真;已改 int32(memory/uid-system-convention.md)。该计数器本身已于 2026-08-16 退役——通用实体 UID 改由 FEntityUIDAllocator 槽位分配器分配(循环利用),此坑随计数器成为历史
  • 并行 UID 分配纪律:所有并行处理器内的 AllocNextUID 必须走 threadlocal gatherer → sort by uid → 串行处理(固定段序 = 全局 UID 消费序)(memory/loot-determinism-lessons.md)
  • 并行 TSet::Emplace 竞争 SparseArray 分配器 → !AllocationFlags[Index] 崩溃,RegisterUIDEntity 必须 MBForEachEntityChunk<true> 串行(memory/loot-determinism-lessons.md)
  • 同构硬件(float 决定论):只有 MoveProcessorFP 保留 FFix64 定点,其余 7 个 processor 的 timer 全部 float(memory/float-timer-non-physics-processors.md;references/lockstep-lessons.md rule 6)
  • 用户规则(2026-08-07):排查发散禁止归因 ~1 ULP;出现位级差异 = 确定性逻辑 bug,「不进 hash 以防 1 ULP 假分叉」的注释是过度保护,已清除(memory/no-ulp-explanations-rule.md)
  • host 池从「TArray+每帧 5 次全量 UID 排序」改为 max-heap by UID:弹池结果只依赖内容不依赖 push 历史 → 双端无需排序,弹池 O(log P)(memory/pool-heap-replaces-sort.md)
4. 单一渲染管线:fragment → slot arrays → Niagara → Material The Single Rendering Pipeline — Fragment → Slot Arrays → Niagara → Material

数千实体(agent、FX、血条、文字弹窗)通过同一条管线渲染:无 ISMC、无逐实例 SetTransform、无渲染线程命令。处理器并行按 chunk 收集,每个实体拥有固定 slot(InstanceId),并行 worker 写不相交的数组元素——无锁收集;串行 pass 按 UID 排序注册后整批 SetNiagaraArray* 推给 GPU。CPU 只推原始目标(ground truth + 速度 + 插值参数),插值/外推/吸附全在 Niagara CustomHLSL 逐粒子节点内完成,身份令牌(UniqueID/epoch+tag)检测池复用并 snap。粒子到材质的桥梁是 DynamicParams0 四个 float 的位打包(动画帧、材质特效、队伍/LOD/选中位),CPU 打包器、GPU 预测器、材质解码器三端必须共享同一布局——布局注释只写一处,解码器放文件化 .ush 以便维护。渲染侧数据永不进模拟 hash;渲染处理器只在 PostCombat 子帧运行。

Thousands of entities (agents, FX, health bars, text pops) render through one canonical pipeline: no ISMC, no per-instance SetTransform, no render-thread commands in plugin code. Processors collect in parallel per chunk; each entity owns a fixed slot (InstanceId), parallel workers write disjoint array elements — lock-free collection; a serial pass sorts registrations by UniqueID, then pushes whole batches via SetNiagaraArray*. The CPU pushes only raw targets (ground truth + velocity + interp params); interpolation, extrapolation and snapping live in per-particle CustomHLSL nodes on the GPU, with identity tokens (UniqueID / epoch+tag) detecting pool reuse to snap. The bridge from particle to material is a bit-packed four-float DynamicParams0 (anim frames, material FX, team/LOD/selection bits); the CPU packer, GPU predictor and material decoders must agree on the exact layout — documented once, decoders kept in file-based .ush. Render-side data never enters the simulation hash; render processors run only on the PostCombat sub-frame.

来源:Sources: references/rendering-pipeline.md + references/bit-packing-pattern.md + memory/non-pool-fx-entity-missing-parentgt.md
关键事实(点击展开)Key facts (click to expand)
  • 管线链:fragment → RenderProcessor(EntityQuery 并行 chunk 遍历)→ slot-indexed TArray 批 → SetNiagaraArray* 整批推送 → GPU CustomHLSL 预测节点 → OutDynamicParams0 → 材质 DynamicParameter 解码(references/rendering-pipeline.md 概述)
  • slot 数组无锁原理:并行 worker 写各自 InstanceId 元素,TArray 元素赋值不重分配;唯一共享多 slot 数据(Fx 注册队列)走 FParallelGatherer<FFxRegistration>(references/rendering-pipeline.md §1)
  • 注册顺序在 bDeterministic 下按 FDeterminism::UniqueID 排序,禁 Entity.Index(references/rendering-pipeline.md §6 Determinism Rules)
  • GPU 身份令牌取代旧 WasHidden 边沿检测:agent 比 UniqueID,Fx 比 (NiagaraIdIndex, NiagaraAcquireTag) 的 (Epoch, Counter) 对——池复用同 id 会重复,epoch/counter 永不重复(references/rendering-pipeline.md §3)
  • DP0 布局:W=Team(8b)|Dissolve(8b)|LODIndex(4b)|DrawLOD(20)|BeingSelect(21)|Selected(22)|Reserved(23-31);Z=HitGlow|IceFx|FireFx|PoisonFx(u8 each);X=Frame0|Frame1(u16);Y=Lerp0|Lerp1|Frame2(references/bit-packing-pattern.md §2)
  • 布局变更必须三端同步(CPU Encode*/GPU Unpack-Pack/材质 .ush),改后 grep 旧掩码零残留;2026-08 起材质解码器文件化(references/bit-packing-pattern.md §4)
  • bRequiresGameThreadExecution = true:Niagara 组件推送仅限游戏线程;整批推送的 ParallelFor 线程安全,但 NS 组件创建/销毁必须串行(references/rendering-pipeline.md §6/§8)
  • 非 pool 的 FX 新建分支漏设 ParentGTLoc(默认 0,0,0)→ ribbon 从世界原点拉丝;lazy pool 新建分支是渲染字段遗漏高危区(memory/non-pool-fx-entity-missing-parentgt.md)
5. 有序处理器管线:固定执行序与子帧调度 The Ordered Processor Pipeline — Fixed Execution Order and Sub-Frame Scheduling

确定性要求一切实体读写发生在固定执行序的处理器内,管线本身是架构设计:哪些处理器在哪些子帧(ESubFrame)跑、哪个处理器先跑,都是会改变模拟结果的决策。典型例证:网格注册必须放在 movement 之前(注册上一帧结果 = 当前位置),这样快照 apply 后下一帧自然注册快照状态——恢复路径无需专用注册函数;ForceGridRegistration 因注册了不同时机的状态造成 FF 发散死循环而被删除。渲染处理器只在 PostCombat 子帧收集(其他引擎 tick 跳过,由 GPU 自插值);Trace 处理器在 Move 之前写 LastKnownTargetLocation(跨实体读用前一串行 pass 的快照字段,禁止并行内跨实体读)。spawn 也走确定性边界:agent spawn 必须经服务器锁步命令,双端同 ExecuteTick 执行。

Determinism requires all entity reads/writes to happen inside processors with a fixed ExecutionOrder; the pipeline itself is an architecture decision — which processors run in which sub-frame (ESubFrame) and which runs first are choices that change simulation results. Canonical example: grid registration must run BEFORE movement (registering the previous frame's result = current position), so that after a snapshot apply the next frame naturally registers snapshot state — no recovery-only registration needed; ForceGridRegistration was deleted because it registered state at a different timing and caused an FF divergence dead-loop. Render processors collect only on the PostCombat sub-frame (other engine ticks skip; the GPU self-interpolates); TraceProcessor writes LastKnownTargetLocation before MoveProcessor runs (cross-entity reads use snapshot fields written by a prior serial pass — reading another entity's field inside a parallel loop is forbidden). Spawning also runs on deterministic boundaries: agent spawns must go through server lockstep commands, executed by all peers at the same ExecuteTick.

来源:Sources: references/determinism.md + references/rendering-pipeline.md + references/lockstep-lessons.md + memory/ff-desync-grid-registration-timing.md
关键事实(点击展开)Key facts (click to expand)
  • 最高准则:任何 EntityManager 读写必须在固定 ExecutionOrder 的 Processor 或 Tick 管线内良定义边界处发生;RPC/引擎回调/计时器委托都是不可预测执行点(references/determinism.md Execution Order Determinism)
  • 并行循环内写(ReadWrite)的 fragment 字段,禁止任何 agent 在同一并行循环里从别的实体读它——读值取决于对方 chunk 先跑还是后跑;修复 = 前一个串行 pass 写快照字段(references/determinism.md Cross-Entity Fragment Read Rule;references/mass-safety.md 同条)
  • 网格注册在 movement 前:注册「上一帧 movement 后」的状态会与快照状态(帧末)不一致;ForceGridRegistration + bForceGridRegistrationOnce 已删除,grid reg 移到帧首(memory/ff-desync-grid-registration-timing.md;references/lockstep-lessons.md rule 2)
  • Render processors MB.IsSubFrameScheduled(ESubFrame::PostCombat) 早退,渲染收集不占每 tick 锁步关键路径(references/rendering-pipeline.md §6)
  • 跨实体读的正确形态:PreLocation(GridReg 主 pass 后快照)、LastKnownTargetLocation(TraceProcessor 在 MoveProcessor 前写入)(references/determinism.md)
  • agent spawn 必须走服务器锁步命令,客户端绝不独立 spawn;服务器按 PlayerIndex 排序控制生成顺序,所有客户端同 ExecuteTick 执行(references/network-lockstep.md Core Principles 2)
6. Tag vs Flag:过滤(tag)+ 测试(flag) Tag vs Flag — Filter (tag) + Test (flag)

tag 与 flag 是 Mass 两种正交的状态机制,不是同一选择的两种表示。tag 是 archetype 级过滤器(.All<T>()/.None<T>() 在实体被处理前整 chunk 跳过),flag 是实体级测试(每实体 HasFlag 决定)。tag 增删 = archetype 迁移(实体搬 chunk、全部 fragment 拷贝,帧末生效),同帧批量迁移 = 可见卡顿;flag 是原地位翻转,零迁移、同帧可见。决策规则:终生不变 + 有查询过滤 + 非同帧批量 → 双注册同名 tag+flag;不满足任一 → 只用 flag,绝不发明没有过滤者的 tag。MassBattle 删除了 4 个渲染 tag(FVisualizingTag 等)——它们的「过滤」只是自己处理器内的 chunk 早退,不是真正的查询过滤。

Tags and flags are two orthogonal Mass state mechanisms, not two representations of the same choice. A tag is an archetype-level FILTER (.All<T>()/.None<T>() skips whole chunks before entities are processed); a flag is an entity-level TEST (per-entity HasFlag decides). Tag changes are archetype migrations (the entity is re-homed into another chunk, all fragments copied, effective at frame end) — a same-frame batch of migrations is a visible hitch; flags are in-place bit flips — zero migration, same-frame visibility. Decision rule: static lifetime + used as a query filter + not batch-changed in one frame → dual-register same-name tag+flag; anything else → flag only, never invent a tag with no filter consumer. MassBattle removed 4 render tags (FVisualizingTag etc.) — their "filter" was only a chunk early-out inside their own processor, not a real query filter.

来源:Sources: references/tag-vs-flag.md + references/network-registries.md
关键事实(点击展开)Key facts (click to expand)
  • 核心论断:"tag = filter, flag = test"。tag 在实体被处理前排除大组实体;flag 让每个实体都跑、逐实体判定(references/tag-vs-flag.md 表)
  • 决策表:终生不变+有过滤+非同帧批量 → 双注册;终生不变+无过滤 → flag only(tag 无过滤器是浪费);会变 → flag only(每次 tag 变更都迁移);有过滤+同帧批量 → flag only(批量迁移卡顿超过过滤收益)(references/tag-vs-flag.md Decision Rule)
  • 反模式 1:不为无查询过滤的标记发明 tag——4 个渲染 tag 因「过滤」只是自家处理器 chunk 早退而被删,改 flag-only(references/tag-vs-flag.md Hard Anti-Patterns)
  • 反模式 2:Optional<> 里的 tag 不构成过滤(Mass 查询只认 All/None);反模式 3:只服务自家处理器的 DoesArchetypeHaveTag 不是过滤器(references/tag-vs-flag.md)
  • 双注册对 = struct FXxxTag : FA_MassBattleBaseTag + EBattleFlags::Xxx 一位,同帧测试永远翻 flag,不依赖 tag(references/tag-vs-flag.md Caveats)
  • 网络侧:只有 MB_FOR_EACH_SNAPSHOT_TAG 列的 tag 才复制(上限 16 个,Record.TagDelta uint16);flag 经 FlagsLow/FlagsHigh 走快照(references/tag-vs-flag.md Caveats;references/network-registries.md §2)
7. 网络注册表:一行注册,自动接线 Network Registries — One Line Registers, the Pipeline Auto-Wires

快照捕获、逐实体 hash、布局、恢复全部由单一来源文件 MassBattleSnapshotRegistry.h 驱动。新增一个快照 fragment = 在注册表加一行 SYNC(FragType, Field1, ...),查询(MB_SNAP_QUERY_ALL)、视图、捕获、布局、恢复指针、tag 增量、hash 站点全部由宏展开,不需要改任何 processor。hash 注册表独立于快照注册表且策略相反:hash 规范化 -0.0f→0.0fFRandomStream 哈希 GetCurrentSeed()FEntityHandle 折叠目标 UID;快照 blob 保留原始字节。两条硬性不变量:agent fragment 必须存在于该类别所有 archetype(缺一个实体就静默掉出网络同步);hash 字段必须是快照字段的子集(否则编译错,fail loud)。这条「一行接线」的设计使新增字段成为纯声明工作,但也要求纪律:新增 fragment 字段忘加注册表行 = 恢复端模板默认值 = resync 后必失步循环,且正常模拟永不暴露。

Snapshot capture, per-entity hashing, layout and restore are all driven by a single source file, MassBattleSnapshotRegistry.h. Adding a snapshot fragment means adding one line SYNC(FragType, Field1, ...) — the query (MB_SNAP_QUERY_ALL), views, capture, layout, restore pointers, tag delta and hash sites all expand from macros; no processor edits. The hash registry is separate from the snapshot registry with opposite policies: hash canonicalizes -0.0f→0.0f, hashes FRandomStream via GetCurrentSeed(), folds FEntityHandle to the target UniqueID; the snapshot blob preserves raw bytes. Two hard invariants: an agent fragment must exist on ALL archetypes of its category (a missing fragment silently drops entities out of network sync), and hash fields must be a subset of snapshot fields (else a compile error — fail loud). This one-line wiring makes adding a field a pure declaration, but demands discipline: forgetting the registry line for a newly added fragment field means the restored peer gets template defaults → guaranteed resync desync loop that normal simulation never exposes.

来源:Sources: references/network-registries.md + references/network-lockstep.md + memory/resync-field-not-in-snapshot-registry.md
关键事实(点击展开)Key facts (click to expand)
  • 四个注册表 + 一个 hash tag 注册表全在 MassBattleSnapshotRegistry.h:agent fragment(27)/debuff fragment(2)/并集/shared fragment/hash 字段(6)/hash tag(references/network-registries.md §1-§3.1;references/network-lockstep.md §4)
  • 2026-08-12 事故:FMoving 新增 SpeedHistory[5]/SpeedHistoryIndex/AverageSpeed,注释自称 "Snapshot-synced via UPROPERTY" 但注册表未加 → 恢复端 AverageSpeed=0 → 卡住检测误判 → 行为分叉 → resync 后必失步;教训:「Snapshot-synced via UPROPERTY」是谎言,同步由注册表行决定(memory/resync-field-not-in-snapshot-registry.md;references/network-registries.md §3.5)
  • 快照字段类型由 GetSnapshotFieldPolicy 自动分类(POD/FString/FName/FEntityHandle/对象引用,单值与 TArray;_Previous 结尾跳过);嵌套 struct 须过 IsSafeForDirectValue(references/network-registries.md §1)
  • hash 两站点(NetworkProcessor 主循环 + ApplyEntitySnapshotPayload 恢复校验)消费同一注册表,加字段天然同步;hash tag 必须 ⊆ 快照 tag(无编译期强制,靠注释约定)(references/network-registries.md §3/§3.1)
  • 失步排查提示:resync 后失步循环 + 正常模拟不发散 + 某功能开关强相关 → 先查该功能写入的 fragment 字段是否全在快照注册表行(references/network-registries.md §3.5)
  • 变参上限:MB_SNAP_FOR_EACH 原 35,FMoving 行 35 字段加 3 超限 → C2065,已扩至 42(memory/resync-field-not-in-snapshot-registry.md)
  • 每个 SYNC 行 ≥1 字段(零字段 checkf 失败);tag 位索引按位置自动分配,上限 16(references/network-registries.md §1/§2)
8. 本地视觉层:四元分流与纯视觉隔离 The Local Visual Layer — Four-Way Split and Pure-Visual Isolation

不是所有视觉都需要同步。端侧本地视觉(框选高亮、点击特效、路径指示、伤害数字)是纯视觉、不参与同步/resync、不消耗 UID 的实体。判据:无任何模拟消费方(只有渲染/调试/UI 消费)的状态可以本地化。机制是「四元分流」:网络模式 × 锁步路径的四象限中,只有锁步内的生成才分配 UID(>=-0)并打 FNetworkTag;锁步外生成 = UID=-1 + 无 FNetworkTag → 自动不进快照/hash/resync。池槽隔离让同一池数组天然分本地/网络槽(按 HasTag<FNetworkTag> != bNetworkSync 过滤,互不复用)。两条设计规范:生成的 actor/fx/sound 不得影响模拟(零副作用于 UID 游标/快照/hash/池序);锁步 host 禁止 attach 到非锁步 host(UID=-1 从不注册 → resync 时父引用必然悬挂),反向(本地子 → 锁步父)允许且必需。

Not all visuals need syncing. Peer-local visuals (RTS selection highlights, click FX, path indicators, damage numbers) are pure-visual entities that participate in no sync/resync and consume no UID. The criterion: state with NO simulation consumer (only render/debug/UI) can be localized. The mechanism is the "four-way split": across the quadrants of network-mode × lockstep-path, only in-lockstep spawning allocates a UID (≥0) and gets an FNetworkTag; out-of-lockstep spawning gets UID=-1 and no FNetworkTag — automatically excluded from snapshot/hash/resync. Pool-slot isolation lets one pool array naturally separate local vs network slots (filtered by HasTag<FNetworkTag> != bNetworkSync, never reused across). Two design rules: spawned actors/FX/sounds must not affect simulation (zero side effects on the UID cursor/snapshot/hash/pool order); a lockstep host must never attach to a non-lockstep host (UID=-1 is never registered → the parent reference dangles on resync), while the reverse (local child → lockstep parent) is allowed and necessary.

来源:Sources: references/local-visual-layer.md + memory/developer-rule-visual-host-isolation.md
关键事实(点击展开)Key facts (click to expand)
  • 四元分流:bAllocIdentity = !bNetworkMode || bLockstepPath(锁步外 UID=-1,不调 AllocNextUID,游标不动);bSyncParticipant = bNetworkMode && bLockstepPath(锁步外无 FNetworkTag)(references/local-visual-layer.md §2①;memory/developer-rule-visual-host-isolation.md)
  • RULE8:锁步宿主禁止 attach 到本地宿主;VerifySyncHostAttachTarget 六个生成函数 fail-loud Error 日志;RULE7(保留变体):agent host 不得本地化(references/local-visual-layer.md §3)
  • 快照剥离掩码 SnapshotStrippedRenderFlagsMask:渲染层 flag(Selected/BeingSelect/Rendering*)恢复端恒清零;捕获端不动;hash 不含 flag(references/local-visual-layer.md §2④)
  • LocalVisualRegistry(AgentSubsystem 内嵌,键 = 父实体 UID)是本地状态唯一事实来源;resync 后 RebindLocalVisualsAfterResync 重指向,InitialRelativeTransform 不重算(重算引入视觉跳变)(references/local-visual-layer.md §2⑤/§4)
  • 全仓无 AttachToActor/AttachToComponent:「附着」由 Mass 层完成(AttachmentEntity 每 tick 解析 ResolvedWorldTransform)(references/local-visual-layer.md §4)
  • 池槽隔离:槽的 tag 创建时定死,AllocateHostEntityHasTag<FNetworkTag> != bNetworkSync 过滤——同一池数组本地/网络槽互不复用(references/local-visual-layer.md §2②)
9. 池化与挂起状态同步:确定性状态进快照 Pool & Pending-State Sync — Deterministic State Enters the Snapshot

设计立场:池按锁步设计是确定性的,分歧 = bug,应被同步与被检测——「池可能跨端分歧所以不进快照」是把 bug 写成设计。三类池(host/Projectile/Loot)的回收与弹池都在确定性 tick 路径上,双端同代码 → 内容必然一致。池与挂起队列(deferred spawn)都是「命令已执行、状态未完成」的挂起状态,resync 会回滚实体但不回滚它们 → 必须进快照 + hash,否则恢复后双端分歧。诚实恢复三原则:实体要恢复、关系要恢复(引用池内实体的句柄折叠真实 UID,不做压 -1 掩盖)、池也要恢复(重建实体放回池,池序对齐)。池序契约:恢复端按 UID 升序放回,服务器池也必须按 UID 升序(LIFO 弹池才一致)。PoolCountdown 等池化字段必须进 hash——懒池销毁是确定性 tick 逻辑,不进则恢复端 PoolCountdown=0 → 立即销毁 → 生命周期分歧。

The design stance: pools are deterministic under lockstep — divergence is a BUG and should be synced and detected; "pools may diverge so they stay out of the snapshot" writes a bug into the design. All three pool kinds (host / projectile / loot) recycle and pop on deterministic tick paths with identical code on both peers — contents must match. Pools and pending queues (deferred spawns) are "command executed, state incomplete" pending states: resync rolls back entities but not these — they must enter the snapshot AND hash, or the peers diverge after restore. Three honest-restore principles: entities restore, relationships restore (handles into pooled entities fold REAL UIDs, never squashed to -1), pools restore (rebuilt entities return to pools, pool order aligned). The pool-order contract: the restoring peer returns in UID-ascending order, so the server pool must also be UID-ascending (LIFO pop then matches). Pool fields like PoolCountdown must enter the hash — lazy-pool destruction is deterministic tick logic; without it the restored peer's PoolCountdown=0 → immediate destroy → lifetime divergence.

来源:Sources: references/pool-sync-pattern.md + references/lockstep-lessons.md + memory/pool-heap-replaces-sort.md + memory/handle-index-sort-resync.md
关键事实(点击展开)Key facts (click to expand)
  • 核心原则三句话:池/队列是锁步确定性状态;确定性挂起状态进快照;诚实恢复三原则(实体/关系/池全恢复)(references/pool-sync-pattern.md §1)
  • 弹池不耗 AllocNextUID(fallback 新建才耗,固定 drain 段序内);FxEntity 无池,靠 Activated 清休眠(references/pool-sync-pattern.md §2)
  • 服务器回收段后 bDeterministic 对池组整体按 UniqueID 升序排序;恢复端放回 pass 遍历 SortedIndices(=(EntityType, UID) 升序)(references/pool-sync-pattern.md §3)
  • 恢复端 resync 后先 ClearPoolArraysForRestore(清 DestroyQuery 残留,不销毁——实体已被销毁)再由放回 pass 重建;[RESYNC_ACTIVATE] 诊断块必须跳过池内/休眠实体(references/pool-sync-pattern.md §4)
  • 池内实体保留 UID 映射(删回收时 UnregisterUIDEntity 被删除)→ 宿主引用折叠真实 UID → 恢复端 FindEntityByUID 命中(references/pool-sync-pattern.md §5)
  • 挂起队列(deferred agent spawn)进快照(FDeferredAgentSpawnSnapshot),否则 resync 回滚实体/游标但队列进度不回滚 → 后续批次 UID 消费错位 → 永久 desync(references/pool-sync-pattern.md §8)
  • 池放回 pass 曾用 Flags & static_cast<int64>(EBattleFlags::PendingDestroy)(序号≠掩码)判定休眠 → 恒判非休眠 → 恢复端池空 → FF 弹池 UID 错位;修复 = HasFlag(memory/handle-index-sort-resync.md 关联;memory/projectile-active-divergence-big-world.md 位掩码教训)
10. Fail Loud:无防御回退,错误当场爆炸 Fail Loud — No Defensive Fallbacks, Errors Explode on the Spot

对不变式用 check()/checkf()/ensure() 而不是静默 if (ptr) 守卫——防御性回退隐藏 bug 并在帧间累积,热路径要精简,只防真正可能发生的事。Fail loud 落在一系列具体机制上:注册表宏对零字段 SYNC 行 checkf 失败;hash 字段不在快照子集则编译错;恢复校验 hash 不匹配则拒绝应用;锁步宿主 attach 到本地宿主时六个生成函数打 Error 日志;Mass 对已销毁实体的 GetFragmentPtr 断言崩溃(即「比较器内禁 GetFragmentPtr」规则的强制力)。排查纪律同源:真出现位级差异就按确定性逻辑 bug 查,不花时间论证浮点行为——「1 ULP」不是 fail loud 的例外,是逃避根因的出口。

Use check()/checkf()/ensure() for invariants instead of silent if (ptr) guards — defensive fallbacks hide bugs and compound across frames; hot paths stay lean, guarding only what can actually happen. Fail loud lands in concrete mechanisms: the registry macro checkf-fails on zero-field SYNC rows; hash fields outside the snapshot subset fail to compile; restore validation rejects on hash mismatch; six spawn functions Error-log when a lockstep host attaches to a local host; Mass asserts on GetFragmentPtr of destroyed entities (which is what enforces "no GetFragmentPtr inside comparators"). The debugging discipline is the same root: a real bit-level difference is a deterministic logic bug — "1 ULP" is not an exception to fail loud, it is an exit from root-cause hunting.

来源:Sources: CLAUDE.md §3.1 + references/network-registries.md + references/local-visual-layer.md + references/determinism.md + references/lockstep-lessons.md + memory/no-ulp-explanations-rule.md
关键事实(点击展开)Key facts (click to expand)
  • "No silent if (ptr) { ... } guards for invariants. Use check()/checkf()/ensure()/ensureMsgf(). Defensive fallbacks hide bugs and compound across frames. Performance over defensive programming."(CLAUDE.md §3.1)
  • 每个 SYNC 行 ≥1 字段(MB_SNAP_LAYOUT 对零字段 checkf 失败)(references/network-registries.md §1)
  • hash registry 的 fragment 必须同时在 agent fragment registry,否则 MB_HASH_ONE 编译错(fail loud)(references/network-registries.md §3)
  • 恢复校验:ApplyEntitySnapshotPayload 对比 bRestoreMatch vs ServerHashAtCapture,不匹配拒绝应用;hash tag 不在快照 tag 里 → 恢复 hash 不匹配 → 拒绝(fail loud)(references/network-registries.md §3/§3.1)
  • VerifySyncHostAttachTarget 六个生成函数 fail-loud Error 日志(references/local-visual-layer.md §3)
  • Mass GetFragmentPtr 对已销毁实体断言(IsEntityValid)——池排序比较器访问失效句柄必崩,因此规则是「先清失效句柄再访问 fragment」(references/determinism.md 2026-08-05 崩溃教训;references/pool-sync-pattern.md §7)
  • 用户规则:任何「有意不进 hash 的字段以 ~1 ULP 假分叉为理由」都是过度保护,应清除(memory/no-ulp-explanations-rule.md)
  • 调试纪律(用户拍板):排查 desync 默认信任框架确定性、chunk 遍历顺序、环境物理检测、组件锁步——不花调查时间,除非出现直接反证(references/determinism.md 调试纪律)
11. AI-Friendly 代码:类型即文档,显式优于隐式 AI-Friendly Code — Type as Documentation, Explicit over Implicit

MassBattle 明确把「AI 可读性」当作一等设计目标:AIs 是模式匹配机器,容易读的代码也更容易被人类推理。具体规则:每类问题只有一条规范写法(如 FParallelGatherer 是并行聚合的唯一模式,AI 永远不用猜互斥锁/原子/thread_local 该用哪个);用类型编码意图(enum class 状态机代替两个 bool,AI 会穷举枚举分支但发现不了 bIsMoving && bIsStopped 这种非法态);显式命名替代隐式(中间变量、命名常量而非魔法数);禁宏魔法、禁 2 层以上模板元编程、禁 5 层以上继承、禁单字母模板参数、禁 stringly-typed API。禁止用 {} scope 掩盖重定义/命名冲突——编译器报错是信号,说明代码有真问题(重复计算或命名混淆),改掉而非围起来。代码审查与文化同源:外部反馈是建议不是命令,技术正确性高于社交舒适,接受正确意见就干净地改,错误意见用技术理由推回。

MassBattle treats "AI readability" as a first-class design goal: AIs are pattern-matching machines, and code that is easy for an AI to read is also easier for humans to reason about. The rules: one canonical pattern per problem class (e.g. FParallelGatherer is the only parallel-aggregation pattern — an AI never wonders mutex/atomic/thread_local); encode intent in types (enum class state machines instead of two bools — an AI exhausts enum cases but won't spot bIsMoving && bIsStopped as a logic bug); explicit over implicit (named constants over magic numbers, intermediate variables over 4-deep nesting); no macro magic, no template metaprogramming beyond 2 levels, no 5+ level inheritance, no single-letter template params, no stringly-typed APIs. Never scope with {} to silence redefinition/name-clash errors — a compiler error is a signal of a real problem (redundant computation or name confusion): fix it, don't hide it. The review culture is the same root: external feedback is a suggestion to evaluate, not a command; technical correctness ranks above social comfort; accept correct feedback cleanly, push back on wrong feedback with technical reasoning.

来源:Sources: CLAUDE.md §3.5/§3.6/§2.2 + references/code-review.md + references/parallel-investigation.md + memory/network-legacy-cleanup-todo.md
关键事实(点击展开)Key facts (click to expand)
  • 反模式表:宏魔法/2 层以上模板元编程/5+ 层继承/单字母模板参数/隐式转换滥用/God files(5000+ 行)/stringly-typed API(CLAUDE.md §3.5)
  • "当编辑旧代码时,把触碰到的区域重构到这些规则"——只重构正在编辑的函数/区域,不做无关的清扫(CLAUDE.md §3.5)
  • {} scope 掩盖重定义:删除重复定义或改名,不隔离;视觉折叠用 #pragma region(CLAUDE.md §3.6)
  • 代码风格:tab 缩进、布尔链一行、单语句 if 单行、嵌套 if 外层带括号、早退扁平化优先于深嵌套(CLAUDE.md §2.2)
  • 反馈处理流程:Read → Understand → Verify → Evaluate → Respond → Implement(一项一项,逐个测试);不清晰就停手澄清;YAGNI 检查(无调用者即删除候选)(references/code-review.md)
  • 子代理排查纪律:首轮判定常过度激进,删除/改名 agent 标记的东西前必须自己核实语义——区分「机制层合理标识 / client 标准模式」与「真游戏层耦合 / 单机假设」(references/parallel-investigation.md Verify First-Pass Findings;memory/network-legacy-cleanup-todo.md 13 处 GetFirstPC 与硬编码模板 key 均系误判实例)
12. 根因思维:不修症状,全局思考 Root-Cause Thinking — Never Patch Symptoms, Think Globally

症状级修复隐藏真 bug 并滋生更多补丁;每条设计都该回溯到源头。项目历史本身就是根因思维的案例库:网格注册时机(2 周排查,根因 = 恢复专用注册路径与正常路径时机不同,修复 = 删专用路径)、压力场 resync 错位(根因不是压力场代码,是帧中间重建的基准时机,修复 = 删重建)、FMoving 字段漏注册表(根因 = 同步由注册表决定,注释说谎)、GapCommands 半开区间(根因 = apply 滞后 1 tick 的区间边界)、位掩码序号误用(根因 = Flags & 35 而非 1LL<<35,且诊断块也用错掩码掩盖了两轮)。方法上:跨 3+ 文件/2+ 子系统的 bug 并行扇出子代理再综合;排查发散先怀疑恢复保真、后怀疑模拟逻辑;性能/行为变更方案先给用户确认再实施(2026-08-11 事件驱动优化反噬教训)。

Symptom-level fixes hide the real bug and breed more patches downstream; every design should trace back to its origin. The project history is itself a casebook of root-cause thinking: grid registration timing (2 weeks of investigation; root cause = a recovery-only registration path registering state at a different timing; fix = delete the dedicated path), pressure-field resync mismatch (root cause was not the pressure code but the mid-frame rebuild timing; fix = delete the rebuild), FMoving fields missing from the registry (root cause = sync is decided by registry lines; the comment lied), GapCommands half-open interval (root cause = apply-lag-1-tick boundary), bitmask ordinal misuse (root cause = Flags & 35 instead of 1LL<<35, masked for two rounds because the diagnostic used the same wrong mask). Method-wise: fan out parallel subagents for bugs spanning 3+ files or 2+ subsystems, then synthesize; when hunting divergence, suspect restore fidelity first, sim logic second; present performance/behavior-change plans to the user before implementing (the 2026-08-11 event-driven optimization backfire).

来源:Sources: CLAUDE.md §3.3 + memory/ff-desync-grid-registration-timing.md + memory/pressure-field-resync-mismatch.md + memory/resync-field-not-in-snapshot-registry.md + memory/local-snapshot-gapcommands-apply-lag.md + memory/projectile-active-divergence-big-world.md + references/parallel-investigation.md + memory/feedback-ask-before-code-changes.md
关键事实(点击展开)Key facts (click to expand)
  • "Find and resolve the root cause — never patch symptoms. A symptom-level fix hides the real bug and breeds more patches downstream."(CLAUDE.md §3.3)
  • 网格注册:曾怀疑 RandomStream 拆分/cell pruning/deferred registration 全非根因,「被动 agent 一致、主动 agent 发散」是放大器类输入(RVO 依赖精确网格邻居)的特征信号(memory/ff-desync-grid-registration-timing.md)
  • 压力场:网格对 1 tick 位置差免疫(邻居集合不变),压力场直接反映 deposit 位置差 → 梯度差 → 速度注入放大;「关掉 pressure 不失步」是切断传播不是根因(memory/pressure-field-resync-mismatch.md)
  • 位掩码修复纪律:必须 grep 全库同类误用点 + git diff 复核最终提交——「诊断计数修好 ≠ 放回逻辑修好」(8/5 的修复从未真正进入放回 pass,8/7 经 git blame 发现)(memory/projectile-active-divergence-big-world.md)
  • 排查优先级:resync 失步循环 + 正常模拟不发散 + 功能开关强相关 → 先查恢复保真(注册表行),后查模拟逻辑(memory/resync-field-not-in-snapshot-registry.md;references/network-registries.md §3.5)
  • 并行子代理适用条件:3+ 独立问题域、互不相关、不共享状态;失败相关时串行(references/parallel-investigation.md)
  • 用户纪律(2026-08-11):性能优化/行为变更方案必须停手等确认再实施,热路径改动编译器/缓存行为无法静态预测,实测可能反直觉(memory/feedback-ask-before-code-changes.md)
13. 恢复路径必须镜像正常路径(同源同时序) Recovery Paths Must Mirror Normal Paths (Same Source, Same Timing)

快照恢复喂给的任何派生状态(网格注册、RVO 输入、缓冲)都必须走与正常模拟完全相同的路径和时机,不设「恢复专用」注册。两条真实事故:① 恢复专用 ForceGridRegistration 注册的是快照状态(帧末),正常注册注册的是本帧 movement 后状态 → FF 第一帧 RVO 读到不同邻居 → 发散死循环;修复 = 网格注册移到 movement 前 + 删除专用函数,快照 apply 后下一帧自然注册快照状态。② resync 帧末 RebuildAllGrids 用快照 tick 基准重建压力场 vs 服务器帧初基准 → 1 tick 错位被 pressure 消费者放大;修复 = 删 resync 路径重建,下帧帧初统一重建。恢复后的校验逻辑也一样:恢复端重折叠 hash 与捕获端用同一注册表、同一公式,才可能匹配。

Any derived state a snapshot restore feeds (grid registration, RVO inputs, buffers) must be populated through the exact same path and timing as normal simulation — no dedicated recovery-only registration. Two real incidents: ① the recovery-only ForceGridRegistration registered snapshot state (end-of-frame) while normal registration registered post-movement state → the FF's first RVO read saw different neighbors → divergence dead-loop; fix = move grid registration before movement and delete the dedicated function, so after a snapshot apply the next frame naturally registers snapshot state. ② The resync-path RebuildAllGrids rebuilt the pressure field on the snapshot-tick baseline while the server uses the frame-start baseline → 1-tick skew amplified by pressure consumers; fix = delete the resync-path rebuild; the next frame's start-of-frame rebuild covers it. The same principle covers validation: the restoring peer folds the hash with the same registry and formulas as the capture side, or it can never match.

来源:Sources: references/lockstep-lessons.md + memory/ff-desync-grid-registration-timing.md + memory/pressure-field-resync-mismatch.md
关键事实(点击展开)Key facts (click to expand)
  • "Any derived state that a snapshot restore feeds (grid registration, RVO inputs, buffers) must be populated through the exact same path and timing as normal simulation. No dedicated 'recovery-only' registration."(references/lockstep-lessons.md rule 2)
  • 诊断特征:被动(idle)实体保持收敛、主动实体发散 → 是放大器类输入(RVO 依赖精确网格邻居值),不是状态本身错;去输入里找,别去状态里找(references/lockstep-lessons.md rule 2 Diagnostic tell)
  • 快照 apply 仍会调一次正常 RebuildAllGrids(与正常模拟同一函数,非专用路径)——「同源」精神成立(references/lockstep-lessons.md rule 2 Verified update)
  • 压力场修复后验证:下帧 MoveA 帧初必然全量重建网格+压力场,双端同基准;帧末重建结果无任何消费者,纯冗余(memory/pressure-field-resync-mismatch.md)
  • 任何「帧中间全量重建」的聚合数值场都有此风险——聚合场不进快照,其基准由重建时机决定(memory/pressure-field-resync-mismatch.md 教训 1)
14. 薄层化架构:领域知识不绑定引擎 Thin-Layer Architecture — Domain Knowledge Not Bound to the Engine

2026-08 引擎评估结论:留在 UE,不转 Godot/Bevy/Unity——MassBattle 的核心资产是确定性模拟架构 + 网络协议设计,依赖 UE 源码可读性与 Mass 官方演进红利(UE6 方向 = Verse + Scene Graph + ECS,Mass 是路线图核心)。薄层化的正确粒度:薄的是领域知识,不是存储层。三明治结构 = 纯规则层(伤害公式/确定性算法/协议格式,标准 C++ 不 include UE 头)+ 薄适配壳(从 Mass fragment 取数喂规则)+ Mass Entity 保持绑定(吃官方红利,不为可移植性抽象掉性能)。明确不做:不用 Flecs 等替代 Mass(全量重写 + 失去 UE 集成,收益只有「存储层可带走」——知识靠文档就能带走)。换引擎的唯一合理场景是确定离开 UE 生态,那时直接去 Bevy 而非 UE+Flecs。

The 2026-08 engine evaluation concluded: stay on UE, don't migrate to Godot/Bevy/Unity — MassBattle's core assets are the deterministic simulation architecture and network protocol design, which depend on UE's source readability and Mass's official evolution (UE6 direction = Verse + Scene Graph + ECS; Mass is on the roadmap core). The correct granularity for thinning: thin out DOMAIN KNOWLEDGE, not the storage layer. The sandwich = a pure rules layer (damage formulas / deterministic algorithms / protocol formats, standard C++ that doesn't include UE headers) + a thin adapter shell (pull data from Mass fragments into the rules) + Mass Entity stays bound (eat the official evolution; never abstract away performance for portability). Explicitly not doing: replacing Mass with Flecs etc. (full rewrite + losing UE integration; the only gain is a "portable storage layer" — knowledge travels via docs anyway). The only sane reason to switch engines is committing to leaving the UE ecosystem — then go straight to Bevy, not UE+Flecs.

来源:Sources: memory/engine-strategy-thin-layer.md
关键事实(点击展开)Key facts (click to expand)
  • 留 UE 理由:确定性模拟架构 + 网络协议设计是核心资产;Godot 无官方 ECS(重造 Mass)、Bevy 确定性默认不保证 + 无编辑器、Unity 核心闭源 + 条款禁止源码喂 AI(memory/engine-strategy-thin-layer.md)
  • 薄层化 = 纯规则层(标准 C++ 不 include UE 头)+ 薄适配壳(从 fragment 取数)+ Mass Entity 保持绑定(memory/engine-strategy-thin-layer.md)
  • 待办:新纯逻辑一律标准 C++;序列化保持字节级自研;架构决策(lockstep 时序/快照区间/resync 语义)沉淀成文档(memory/engine-strategy-thin-layer.md)
15. 空间数学:实体局部坐标系是唯一事实 Spatial Math — Entity-Local Space Is the Only Ground Truth

实体与组件/actor 之间的相对变换必须在实体局部系存储与应用——EntityRot.Inverse().RotateVector(WorldOffset) 捕获、EntityRot.RotateVector(LocalOffset) 每帧应用;永不依赖 FTransform::GetTranslation() 或 FTransform 乘法语义(它与 UE 场景组件 attachment 的语义不同:attachment 的平移按旋转旋转,而 FTransform 乘法按子旋转旋转)。这两个数学陷阱(AI 高错误率区)的共同根因:在 upright/identity 情况下所有公式等价,倾斜时才露馅——所以检查清单强制测试 45° 倾斜 + 倾斜移动 + ComponentToEntity 倾斜。同类原则:FCollider 可任意朝向,禁止假设轴对齐/world-up;尺寸必须运行时乘 Scaling.Scale。编写涉及四元数/向量运算的空间代码时,先在 Python 里原型验证(UE 数学就是标准数学),再翻译 C++。

Relative transforms between entities and components/actors must be stored and applied in ENTITY-LOCAL space — capture with EntityRot.Inverse().RotateVector(WorldOffset), apply each frame with EntityRot.RotateVector(LocalOffset); never rely on FTransform::GetTranslation() or FTransform multiplication semantics (they differ from UE scene-component attachment: attachment rotates the translation by the PARENT's rotation; FTransform multiplication rotates it by the child's). These two math pitfalls (the AI high-error-rate areas) share one root cause: every formula is equivalent on upright/identity cases and only breaks under tilt — so the checklists force 45°-tilt, tilt+move, and ComponentToEntity-tilt tests. Same-family rules: FCollider is arbitrarily oriented (never assume axis-aligned/world-up); dimensions must be runtime-scaled by Scaling.Scale. When writing quaternion/vector-heavy spatial code, prototype in Python first (UE math is standard math), then translate to C++.

来源:Sources: references/relative-transform.md + references/mass-safety.md
关键事实(点击展开)Key facts (click to expand)
  • 实体↔组件平移的规则:在实体局部系存储与应用;GetRelativeTransform().GetTranslation() 存的平移在 T.Rotation 的坐标系(不是父系),旋转后不跟随(references/relative-transform.md Part 1)
  • UE attachment 语义:ChildWorldLoc = ParentWorldLoc + ParentWorldRot.Rotate(ChildRelativeLoc * ParentWorldScale)——平移按父旋转旋转,与 FTransform 乘法不同(references/relative-transform.md Part 1)
  • 表面贴合:两约束而非一约束,Z = 表面法线(倾斜)、X = 投影方向(面内旋转);MakeFromZX(Normal, ProjectedDir) 或双四元数组合,顺序 = InPlane * AlignToNormal;先 VectorPlaneProject(references/relative-transform.md Part 2)
  • 红旗:「sync 循环里出现 SomeTransform.GetTranslation()」且旋转可能变化 → 大概率错了;「upright 正常、倾斜就坏」= 参考系 bug(references/relative-transform.md Part 1 Checklist)
  • FCollider 不可假设轴对齐/world-up/规范 forward,一律经自身旋转变换;Radius/Height 必须乘 Scaling.Scale(及 ColliderMult)后用于运行时计算(references/mass-safety.md)
  • Python-first:旋转偏移跨参考系、四元数组合、VectorPlaneProject+FindBetweenNormals、Inverse()+RotateVector 链——一律先 Python 多场景验证再 C++(references/relative-transform.md Part 3)

本站说明 About This Site

文档全部源自真实源码(.h/.cpp 及其注释),每个函数都注明总体介绍、逐参数 Input 与 Output(返回值 + 副作用),每个成员变量注明类型与默认值。页面右上角按钮切换中英显示,偏好会记住;左侧目录按模块分组、可折叠、支持搜索;右侧是本页小节目录。

Every page is derived from the real source code (.h/.cpp and their comments). Each function documents its overall purpose, per-parameter Input, and Output (return value + side effects); every member documents its type and default value. The top-right button toggles Chinese/English (remembered); the sidebar groups pages by module with collapsing and search; the right side shows the on-page section index.