PX PROJECT · WALKINGSURFACE · PERFORMANCE ENGINEERING

WalkingSurface 优化实现逻辑

基于 F:\PX_Trunk\PX\Plugins\WalkingSurface 与 GPCharacterMovementComponent.cpp 源码,梳理这套"用离线烘焙结构替代逐帧物理扫掠"的优化实现:热数据内存布局、4-ary BVH 剪枝、FindFloor / SweepHorizontal 查询链、生产接入的 CVar 模式、采信与回退边界,以及专题实测结论。

12 张图示 7 张循环动图 Tile 3200cm · Grid 20cm · StepLength 100cm 4-ary BVH · 64B 缓存行 默认关闭(CVar = 0) 3D 交互图示 ↗

00一句话总览与图集

WalkingSurface 的核心思路是:把静态可走地形的几何信息在 Editor 里离线烘焙成紧凑的只读结构,运行时用一次缓存友好的数据结构查询替代 UE 的 ComponentSweepMulti 物理扫掠。收益来自"预计算 + 缓存局部性 + 早退剪枝",代价是必须严格界定"什么时候这个结果可信",不可信就回退 UE。

🌀 3D 交互图示(推荐配合阅读)
静态图示难以表达「XY 墙脚线段 × Z 模拟墙高度带」、「贴地窗口上界含 R + L 因而又长又高」的空间关系。另有基于 Three.js 的 WalkingSurface 3D 几何图示(单文件、内嵌 Three.js、可拖拽旋转、切俯视/侧视、切图层、播放胶囊动画)。它按 UE 的 Z-up 坐标系建模,1 单位 = 1 m,所有几何按 Types.h:495-529 / Runtime.cpp:2715-2811 的口径绘制。
先厘清一个容易踩的坑:「三接口」已退役
专题文档里反复出现的 FindFloor / ProbeForward / MoveAlongFloor「三接口」是 v5–v7 时代的旧设计。当前代码(kBuildVersion = 13)中 ProbeForward、MoveAlongFloor 的运行接口,以及 FEdgePrim / EdgeBVH / FWS*Result 等结构全库 0 命中(Types.h:113-115、00-brief.md:38-40):前者已被 SweepHorizontal(水平胶囊扫掠 + 烘焙线索)取代,后者只剩枚举与 StepLength / MinProgressEps 配置残留。当前生产实现 = FindFloor + SweepHorizontal(对外由 FWalkingSurfaceMove 的 9 个静态方法暴露)。本文第 6 章保留旧设计用于说明方案演进,并明确标注「已退役」。
fig1_architecture.png
图 1系统架构:Editor 离线烘焙 → 落盘数据(Patches / Tiles / 精度块 / 水平线索)→ Runtime 热结构 → GP CMC 两处接入(FindFloor 与 3P 前向 Sweep)。
优化对象
3P(SimulatedProxy)包间外推帧里 MoveSmooth 的那一次 SafeMoveUpdatedComponent(Delta, bSweep=true)——scope 内唯一一次带 sweep 的移动,也是 SweepMulti 稳态耗时的主要来源。收包帧本来就是无 sweep 直拍,不在此列。
优化手段
离线把地形剖成 Patch / Tile,构建 4-ary 量化 BVH + 连续凸块 blob + 精度槽位 + 水平阻挡线索;运行时只读查询,Clear 直接无 sweep 落位,Hit 截断落位,Fallback 回退 UE。
安全边界
只覆盖静态几何、水平位移、胶囊碰撞契约与烘焙一致;动态阻挡、竖直分量、起点穿透、悬空、精度不足一律回退。IsAuthoritativeFloor 再对 FindFloor 结果做一次稳态区间校验。

图集索引

01烘焙侧:数据从哪来

运行时所有"快"都建立在 Editor 期的离线预计算上。烘焙器把场景中符合 Profile 的静态几何转成可查询的 Patch / Tile,并额外产出精度块与水平线索。图 10 / 图 11 / 图 12 与动图 7 先给出烘焙产物的空间几何形态(俯视与剖面,含线索生成),图 8 与动图 5 给出完整流水线,图 9 与动图 6(第 5 章)展开线索的运行时判定。

fig10_geometry_topdown.png
图 10烘焙几何结构(俯视 XY):1 个 32 m Tile 内的 20 cm Grid、若干 FGroundPatch 平面片(FPlane FitPlane + int32 世界 cm 顶点)、Boundaries[0]=Outer(CCW) 与几何洞 (CW),以及 Grid → Marching Squares → Simplify → BuildRings → 凸剖分 / QueryBVH 的几何演化。
fig11_geometry_profile.png
图 11几何生成链(XZ 剖面):源三角面与竖墙 → 共面合并成 FitPlane 平面片(残差约 0.29 cm,由 BuildPatchPrecision 补顶点偏差)→ 阻挡边(旧「净空轮廓」已退役 vs 当前 G1 真实 [Zlo, Zhi] 足迹)→ FGroundClue 的墙脚接触边与模拟墙 Z 带。
fig12_clue_bake_geometry.png
图 12FGroundClue 烘焙生成几何:接触带(本格 + 8 邻格 FindFloor 凸块叶盒紧贴地面上方一层)→ 源面 XY 投影凸包边界 → 相邻面片共用边成对抵消 → 共线段合并 → ClampEdgesToGroundLayers 收成模拟墙(不留真实高度)→ uint16 量化落盘。下方对照 FBlockEdge(离线 G1 校验夹具,真实 MinZ/MaxZ)与 FGroundClue(生产落盘,模拟墙 Z 带)。
gif7_boundary_evolution.gif
动图 7几何演化循环:Grid cell 采样(Walkable / Block / Empty)→ Marching Squares 锯齿边界 → 按 RegionId 拉直 → 闭合环与凸剖分(≤32 顶点)→ 4-ary QueryBVH 叶子。
fig8_bake_pipeline.png
图 8烘焙流水线:Baker::Bake 两阶段分桶(轮 1 MergeOnly → 轮 2a ToStepA3 → 全局复判 → 轮 2b AfterStepA3)→ CompactWallEdges / FinishBuild → 落盘。右侧为 G1 几何驱动阻挡边的 B1~B6 六阶段。
gif5_bake_stages.gif
动图 5烘焙四阶段循环:三角面共面合并 → 净空 / StepA3 复判 → Marching Squares 边界与 Patch 组装 → 精度块 / 线索落盘。

1.1 数据流

FWalkingSurfaceBuilder::BuildFull / BuildIncremental   ← EditorModule 绑定 GetBuildEntry() delegate
  └─ FromDataActor(拉取 Build_* 设置)
       └─ FWalkingSurfaceBaker::Bake(World, DataActor)         ★ 主烘焙,两阶段分桶
            ├─ 轮1 MergeOnly:三角面采集 → 可走面过滤 → 建全量平面池
            ├─ 轮2a ToStepA3 → 全局复判 → 轮2b AfterStepA3
            ├─ Grid 20cm / Tile 3200cm(32m,160×160 格)
            ├─ 边界:BoundarySimplify / WallEdgeAligner / ObstacleEdgeBake
            └─ CompactWallEdges → FinishBuild
                 ├─ Horizontal::BuildPatchPrecision → 精度块 + 水平线索
                 ├─ RunPostBakeFindFloorValidation
                 └─ MarkPackageDirty(随 .uasset 保存)
            ↓ 落盘(kBuildVersion = 13 / kPrecisionLayoutVersion = 18)
  AWalkingSurfaceData(Patches + Tiles + Precision + Clues)
            ↓ Runtime Build
  FWSHotArena(见第 2 章)

1.2 关键产物

产物内容被谁消费
FGroundPatch地面多边形 outer(世界 XY 顶点)、拟合平面 A/B/C、法线;NoHole 方案下只含 outerFindFloor 的平面求 Z、SweepHorizontal 的 patch 扫掠
FGroundTileTile 坐标、凸块记录、量化参数 QuantOrigin / QuantFactor按 XY 的 O(1) 定位
精度块每 patch 拟合误差界、顶点偏差、模型偏差界;B5 槽位预展开(O(1) 取值)临界接触判定、PrecisionBudget 回退
水平线索 CluesFGroundClue(12 B,真实接触边 + Z 带)+ FGroundClueNode BVH(16 B);按 tile 组织,运行时由 ClueTileGrid_ 扁平网格直接寻址IntersectsClues,仅在首命中邻域内查询

版本与数据模型:kBuildVersion = 13(v6 起 Tile 由 16m 改为 32m,需重 Bake)、kPrecisionLayoutVersion = 18(精度块独立布局版本,旧精度块整块跳过而 Patches/Tiles 照常读取)。数据模型是平面片(FPlane 拟合平面 + int32 世界 cm 的 XY 顶点),不是栅格高度图;边界为 Boundaries[0] = Outer(CCW) + 几何洞。量化 0.5cm,无 LOD、无通用压缩。注意 Runtime.h:32 注释里的「1600cm / 16m」是 v6 之前的旧描述,实际常量为 3200cm(见第 6 章「注释漂移」)。

BlockEdge 的两代语义(容易混淆):v5–v12 的 FGroundPatch::BlockEdges 由「Block cell 净空轮廓」派生,专供已退役的 ProbeForward / FEdgePrim,v13 已删除;当前生产形态的「G1 几何驱动重设计」是 WalkingSurfaceObstacleEdgeBake 的 FBlockEdge(直接取自真实非可走碰撞面的真实 Z 区间),再收敛为烘焙线索 FGroundClue + FGroundClueNode BVH。G1 的烘焙阶段:B1 采集 N.z < 0.71 的非可站立面(含朝下面)→ B2 面合并 → B3 足迹投影(真实 Z 区间 / 朝外法线,无栅格拉直)→ B4 可达性裁剪(唯一剔除规则)→ B5 共线合并 → B6 新结构 + 版本升级(01a-blockedge-audit.md:73-89)。

坐标口径的坑
Runtime.h:26-28 明确写:FGroundPatch::AllVertices 实际是世界 XY,类型头注释里的"UV 局部坐标"是历史遗留描述,Runtime 一律按世界 XY 处理。烘焙与运行时的 Tile 尺寸必须一致(Runtime.h:31-38)。

1.3 增量与重建

Editor 侧还有 DirtyTileTracker / GridBoundaryRebuild / BakerIncremental 支撑"只重建脏 tile",以及 Stepwise 分步烘焙、CoplanarMerge 共面合并(夹角容差 5°)、PatchAssemble 组装。烘焙侧还有 Stepwise 分步(单步已删,仅合并模拟)、DirtyTileTracker 的 ClosureMask 增量闭包、Co-planarMerge 共面合并(夹角容差 5°)、PatchAssemble 组装。这些不影响运行时查询语义,但决定了重 Bake 后旧 handler 如何失效:Runtime 用 BuildGeneration_ 代际号让 per-agent 缓存自动作废(QueryTypes.h:56),旧 handler 的 sweep 回退、原 FindFloor 继续用一致快照(MoveInterface.h:59-62)。

02热数据:为缓存行而设计

这是整个优化最"硬核"的部分:所有数据结构都围绕减少 cache miss 与指针追踪设计。目标是把逐帧查询从"多次散列 + 多次 scatter 读取"变成"一次连续内存上的顺序访问"。

fig2_memory_layout.png
图 2热数据内存布局:一次连续分配 + 64 字节对齐 + 4-ary BVH + 记录头与顶点连续存放。

2.1 一次连续分配(FWSHotArena)

// WalkingSurfaceRuntime.h:303-335(节选)
struct FWSHotArena
{
    uint8* Ptr = nullptr;  SIZE_T Size = 0, Used = 0;  bool bLargePages = false;
    void Allocate(SIZE_T Bytes, bool bTryLargePages);   // Windows: MEM_LARGE_PAGES,失败回退 64B 对齐
};
// 布局:[稠密热表][tile0 QueryBVH | tile0 凸块 blob][tile1 ...]  ← 一次分配,无拷贝

不可拷贝(内部视图指针指向本 arena),move-only。大页减少 TLB miss;连续布局让相邻 tile 的热数据物理相邻。

2.2 三条缓存行级优化

结构做法收益
FWSTileHot = 64 Bstatic_assert(sizeof(FWSTileHot) == 64)(Runtime.h:301);8 个 8B 槽放 tile 坐标 / BVH 偏移 / blob 偏移 / 精度槽位一次访存取齐整块元数据,避免多次指针追踪
4-ary BVH 节点 16 Buint16 BMin[3]/BMax[3] 量化 3D AABB + int32 i;4 个连续子节点 = 64 B 对齐(TAlignedHeapAllocator<64>);i ≥ 0 叶子、i < 0 内部用相对前向偏移(Runtime.h:66-89)每层一次访存测 4 个子节点;树深 log4 vs log2 减半;Z 与 XY 同量化体系 → 枝级 Z 剪枝
FConvexRecHeader 24 B + 顶点 blob记录头(PatchIndex / PlaneA/B/C / VertCount / OuterPrimIdx)与其顶点连续存放;QueryBVH 叶子 i 直接存 blob 槽偏移(Runtime.h:122-141)叶子 → 记录头 → 顶点一跳访存(旧布局 prim 数组 → 顶点池是两跳)

2.3 热路径零浮点(FWSInlinePoly)

// Runtime.h:91-108
struct FWSInlinePoly
{
    TArray<FIntPoint, TInlineAllocator<16>> Verts;  // int32 世界 cm(8B/顶点,原 double 16B)
    FIntPoint Center;  int64 R2;                     // 包围圆半径平方,早退用
    TArray<int64, TInlineAllocator<16>> EdgeSlopeX; // ((V[i].X-V[i-1].X)<<20)/(V[i].Y-V[i-1].Y)
};
// 量化误差 ≤ 0.5cm ≪ 烘焙 20cm grid;射线法全程 int32/int64,热路径无浮点

≤16 顶点内联存储,消除堆指针追踪的 cache miss;平面系数在 Build 期一次性折算为 A=−X/Z, B=−Y/Z, C=−W/Z,查询期只做 float 乘加、无 double、无除法,法线命中时现算一次(Runtime.h:110-120)。

2.4 索引与缓存

  • 稠密 tile 索引:DenseTileIndex_ 规则网格直接寻址替代 TMap 哈希;TileIndex_ 仅作覆盖范围过大时的兜底(Runtime.h:602-610)。线索同理有扁平网格 ClueTileGrid_,无哈希。
  • per-agent 查询缓存:FWalkingSurfaceQueryCache { LastTile, LastPatch, Generation }(QueryTypes.h:52-63)。Runtime 本体无 mutable 成员、无 agent 状态,因此查询可多线程并发;hint 短路失败自动回退完整路径,几何验证保证正确性。
  • 查询预热:WarmupQueryData(Center, FootRadius)(Runtime.h:504-515)把足迹覆盖 tile 批量预取至 L1/L2,把串行 miss 转成并行加载。源码注释也诚实指出:数据预取无法消除 I-cache/BTB 的逐帧首次调用冷启动税,那是实测中耗时的主体。

03FindFloor 查询链

FindFloor 用世界 XY 定位到一个 Tile,再在 4-ary BVH 里剪枝出候选凸块,最后用平面解析式求 Z——全程没有物理 sweep、没有 double 除法。

fig3_findfloor_flow.png
图 3FindFloor 五步查询链,以及 lastPatch 短路与 LineTrace / Capsule 两种模式的取舍。
gif1_bvh_prune.gif
动图 1胶囊沿路径前进,只有所属 tile 与被访问的 BVH 节点点亮;4-ary 每层一次访存测 4 个子节点,量化 AABB(含 Z)在枝级剪掉无关分支。

3.1 两种查询模型

Mode做法适用与风险
LineTrace以 agent 中心 XY 向下 trace,窗口 [Start.Z−DownDist, Start.Z+2cm];多候选取窗口内最高 Z(向下首个命中),同 Z ±0.5cm 保留先入选者(Runtime.h:462-476)最快;天然区分上下层(桥上只命中桥)。风险:球心悬空(悬崖边/洞口)会误判可走
Capsule胶囊足迹 AABB(球心 ± R)一次查询 + 逐 patch 解析求 Zsweep = Zplane + R·(|N|−1) 择优;闭式解而非多点采样;足迹内接触求解完备(面 / 边 / 角接触)(Runtime.h:478-502)语义对齐 UE 胶囊 sweep 停位,修正点查询的悬空误判;输出 Location 与 ImpactPoint 双点分离(见第 4 章)

3.2 回退契约

false 对上层可见即「需要回退」
源码明确约定(Runtime.h:494-496):本系统不做内部回退猜测。返回 false 时上层应回退 UE 原生 CharacterMovementComponent::FindFloor / 真实胶囊 sweep;上层也可按自身语义直接当作无地面(如 MoveAlongFloor 停止前进)。这是"不可信就交给 UE"的分层设计。

3.3 精度消费三路径

EPrecisionPath(Runtime.h:381-393):B3Only(逐顶点线性扫,仅作开销参照)/ Reference(B5 慢路径,逐顶点取偏差与模型偏差界)/ Fast(B5 槽位预展开,O(1) 取值,判定结果与 Reference 逐位一致)。默认 Fast——这是"精度不降、查询变 O(1)"的典型手法。

04Capsule FindFloor:Zsweep 与双点分离

要让 WS 的地面结果能直接透传给 CMC 消费,必须复刻 UE 胶囊 sweep 的几何语义,而不只是"采样一个地面高度"。

fig4_capsule_geometry.png
图 4斜面胶囊命中:Location 是胶囊中心停位,ImpactPoint 在上坡侧真实接触点,两者必须分离。
// WalkingSurfaceQueryTypes.h:16-40(字段语义)+ Runtime.h:481-489(Capsule 模式)
Zplane  = A·x + B·y + C                    // 拟合平面
N       = normalize(−A, −B, 1)
Zsweep  = Zplane + R·(|N| − 1)            // 胶囊底部触地时的底部 Z(内部择优基准)
Location.Z    = Zsweep + AgentHalfHeight − ShrinkHeight
ImpactPoint.Z = Zimpact = Zplane + R·(secθ − cosθ)  ⟺ Zsweep + R·(1 − cosθ)
ShrinkHeight  = max(AgentHalfHeight − AgentRadius, 0) × 0.1f   // 对齐 UE ComputeFloorDist 的 ShrinkScale=0.9
为什么必须分离
源码注释(QueryTypes.h:33-35)指出:下游 MoveAlongFloor 用 ImpactPoint.Z 追踪脚下地面,ShouldComputePerchResult / ComputePerchResult 用 ImpactPoint − Location 判 perch radius,IsValidLandingSpot 用 Location 作胶囊中心算下半球与边缘容差。若两者相等,DistFromCenter = 0 会恒过校验。

此外 FWalkingSurfaceFindFloorParams(QueryTypes.h:143-161)提供 bMatchUEImpactNormal:命中法线不可站立时按 UE FindMostOpposingFace 口径改取接触点附近最平的 patch 法线(停位不变)。GP CMC 走 IsAuthoritativeFloor 采信规则,保持 false。

05SweepHorizontal:只读查询替代物理扫掠

这是"优化实现"的主角,也是旧设计中 ProbeForward 的当前等价物。只读复用 QueryBVH / ConvexBlob + 精度槽位 + 烘焙线索,不调用 FindFloor、不改 Z,返回 Clear / Hit / Fallback 三态。

fig5_sweep_decision.png
图 5SweepHorizontal 主链与三分支,以及全部 EWalkingSurfaceSweepFallback 回退原因。
gif2_sweep_branches.gif
动图 2三种场景循环:Clear 直接落位、可信 Hit 按 Time 截断并回填、Fallback 改走 UE 物理扫掠。
fig9_clue_query.png
图 9Sweep 线索查询:FGroundClue 12 B(量化端点 + ZLo/ZHi)、FGroundClueNode BVH 16 B、贴地窗口 [脚底, 脚底 + R + 位移],以及 IntersectsClues 的四步判定与 ReachSq 余量构成。
gif6_clue_zband.gif
动图 6线索 Z 带判定三场景循环:Z 带在窗口上方(PlanePruned)→ Z 相交但 XY 距离超出 Reach(不阻挡)→ Z 相交且沿位移越走越近(VerticalSurface 回退 UE)。

5.1 求解链(Runtime.h:526-534)

1
准入
线索块已产出;Radius / HalfHeight / 位移长度不超过烘焙参数;起点脚下支撑带内有 patch 凸块。GP CMC 侧还要求 Delta.Z == 0、胶囊 Query 碰撞开启、非 bTraceComplexOnMove、烘焙通道与响应与胶囊一致(GP CMC :11377-11413)。
2
patch 扫掠得到首命中
沿水平位移在 QueryBVH 上扫掠,得到首个可走 patch 面接触,含 B5 精度判定(临界接触用拟合/模型偏差界决定是否采信)。
3
只在首命中邻域查线索
IntersectsClues(Query, MaxDistance, ...)(Runtime.h:409-417):MaxDistance = 首命中距离 + kCriticalContactTolCm。检查是否存在 Z 区间与贴地窗口 [脚底, 脚底+R+位移] 相交、且沿位移越走越近的接触边(墙脚 / 台阶侧面 / 未建模几何)。把昂贵的线索查询限制在极小邻域是这一步的关键优化。
4
结果分类
触到线索 → Unknown + 回退原因(调用方改做原 sweep);否则 Clear(全程无阻挡)或 Hit(命中可走 patch 面,带 Time / 停位 / 法线)。
求解顺序的两个口径
当前源码头文件注释(Runtime.h:526-534)为「先 patch 扫掠得首命中 → 再只在 [0, 首命中 + kCriticalContactTolCm] 邻域查线索」;而专题后期(迭代 12 起)记录的是「ClueFirst=1:先查烘焙线索 BVH,触到即回退 UE,否则才 patch 扫掠」。两份报告的实码核对进一步表明:Runtime.cpp:3495-3499 / :4168 的当前实现是先查线索 BVH、触到即回退(ClueFirst = 1),Runtime.h:529-530 的「patch 先」注释已漂移。本文图示按「邻域剪枝」的设计意图绘制,文字以实码为准:线索命中即回退 → 否则 patch 扫掠 + 精度判定 → Clear/Hit。读专题文档时请按迭代时间戳区分。

5.2 结果与回退类型

// WalkingSurfaceQueryTypes.h:163-194
enum class EWalkingSurfaceSweepResult : uint8 { Clear, Hit, Fallback };
enum class EWalkingSurfaceSweepFallback : uint8 {
    None, MissingData, InvalidInput, OutsideCoverage,
    VerticalSurface,        // 前方触到竖直阻挡的接触边(墙脚)
    UncollectedSurface,     // 台阶侧面、陡坡、碎石等 patch 未表达的静态几何
    SmallTriangle, StepA3Pruned, CoveredPruned, GridExcluded, UnsupportedGeometry,
    NoGroundSupport,        // 起点悬空 / 离开可走面
    InitialOverlap,         // 起点已与地面模型重叠
    OuterBoundaryContact,   // 首命中落在 patch 外边 / 顶点上
    PrecisionBudget, RoundShape, BudgetExceeded
};

FWalkingSurfaceSweepHit(QueryTypes.h:196-215)字段与 UE FHitResult 同口径:Time / Distance / Location / ImpactPoint / Normal / ImpactNormal / bBlockingHit / bStartPenetrating。但不含 Actor / Component / PhysMaterial / FaceIndex——上层若需要这些元数据,必须回退 UE。

5.3 对外接口

FWalkingSurfaceMove(MoveInterface.h:67-128)是纯静态工具类;FWalkingSurfaceMoveHandler 是 move-only 的不透明句柄,内部独占 Runtime 共享引用、agent 参数缓存与 per-agent 查询缓存。公开方法:Register / Unregister / UpdateAgentShape / SetFindFloorParams / FindFloor / HasSweepData / GetSweepCollisionFilter / SweepHorizontal / SweepHorizontalWithFallback。

线程语义
Runtime 只读、查询可并发;同一 handler 的查询缓存会被查询改写,故同一句柄应在单一线程上下文使用(与游戏 agent 逐帧 tick 语义一致)。SweepHorizontalWithFallback 在 WS 给出 Clear/Hit 时直接填 OutHit,否则用同一胶囊做 World->SweepSingleByChannel。

06MoveAlongFloor:单步迭代(已退役的旧设计)

本章是历史方案,不是当前生产实现。当前代码中 MoveAlongFloor 已退役,仅剩 EWalkingSurfaceMoveResult 枚举与 StepLength / MinProgressEps 配置残留;其「沿地面走一段」的职责由 SweepHorizontal + GP CMC 的落位逻辑承担(见第 5、8 章)。保留本章是为了说明方案演进与迭代常量的来龙去脉。

为什么还保留这一章
专题 01-clarify.md 关键事实 #6 与 03-plan.md 的三落地形态 A/B/C 都是围绕这套迭代设计的;理解它被否决、被 SweepHorizontal 取代的原因,才能理解当前「独立水平查询、不与 FindFloor 混用」这条约束的来由。图中所有步骤均为旧设计语义。
gif4_movealongfloor.gif
动图 4【历史方案示意 · 当前代码已无实现】单步五阶段:FindFloor 锚定 → ProbeForward 探测 → 允许位移 < 1cm 判失败 → 提交 XY → 终点再 FindFloor 锚定。

以下常量在当前代码中仍定义于 FWalkingSurfaceRuntimeConfig,但已无对应的 MoveAlongFloor 实现消费,属配置残留。

// WalkingSurfaceTypes.h:23-95 + Runtime.h:35-53
kGridCellSize        = 20.f;    // 烘焙栅格单格边长(cm),全局对齐
kTileWorldSizeX/Y    = 3200.f;  // 32m;v6 起由 16m 改为 32m(需重 Bake)
kTileGridSizeX/Y     = 160;     // 每 tile 160×160 格
StepLength           = 100.f;   // MoveAlongFloor 迭代步长(1m)
MinProgressEps       = 1.f;     // 单步允许位移 < 1cm 视为探测失败
kCollectSlopeCos     = 0.015f;  // ≈89.1° 采集阈值
kStandableSlopeCos   = 0.71f;   // 可站立法线余弦
kBuildVersion = 13;  kPrecisionLayoutVersion = 18;
kMaxSweepLengthCm    = 256.f;  // Runtime.cpp:42-43 单次水平扫掠位移上限
源码注释漂移(易踩)
Runtime.h:32 的注释仍写「默认 1600cm = 16m」,但 Types.h:27-28 的实际常量是 3200cm = 32m(v6 起改),本文以常量为准。同类漂移还有:QueryTypes.h:134-136 说 Capsule 模式「球心 XY 无覆盖时判无地面」,而 Runtime.cpp:5146-5151 实码已改为「不要求球心 XY 被可走 patch 覆盖」(旧门槛实测去掉后不引入差异,属对 LineTrace 中心悬空误判的历史修正)。

专题 01-clarify.md 关键事实 #6 描述的旧设计:MoveAlongFloor 每步 = FindFloor 锚定 → ProbeForward 探测(圆柱中心 = 地面 + 半高)→ 允许位移 < MinProgressEps(1cm) 判失败 → 提交 XY → 终点再 FindFloor 锚定保证贴地;3P 外推每帧约 10cm ≪ StepLength 100cm,因此单次迭代;无滑墙、只截断,与 3P 现状行为同构。

三个口径注意点
① 内部锚定固定 FindFloorLineTrace 单点查询,不走 GP CMC 注册的 Capsule 模式——悬崖/洞口两口径结论不同(LineTrace 中心悬空可能误判可走,是漏报方向);② FindFloorDownDist 默认 200cm,下坡落差超窗 → Failed/Partial → 回退(安全方向);③ Failed 输出原地,回退信号清晰。

EWalkingSurfaceMoveResult(QueryTypes.h:65-71):Completed(走完全程)/ Partial(被阻挡,OutNewLocation 为最远可达点)/ Failed(起步即失败,原地)。

07采信边界与回退

优化的成败不在"查得快",而在"知道什么时候不能信"。两道闸门:FindFloor 的 IsAuthoritativeFloor 与 SweepHorizontal 的 FallbackReason。

fig7_trust_boundary.png
图 7FloorDist 数轴:稳态区间 [1.8, 2.5] 内采信 WS,两侧(下台阶/穿透、下坡/悬空)回退 UE。
// WalkingSurfaceQueryTypes.h:95-127
constexpr float kFloorWalkableNormalMargin = 0.02f;
constexpr float kFloorTrustMinDist         = 1.8f;
constexpr float kFloorTrustMaxDist         = 2.5f;
FORCEINLINE bool IsAuthoritativeFloor(const FVector& HitNormal, float FloorDist, float WalkableFloorZ,
                                      float TrustMinDist = 1.8f, float TrustMaxDist = 2.5f)
{
    return HitNormal.Z >= WalkableFloorZ + 0.02f
        && FloorDist >= TrustMinDist && FloorDist <= TrustMaxDist;
}

为什么是 1.8~2.5:UE 的 AdjustFloorHeight 把稳态 FloorDist 维持在 [MIN_FLOOR_DIST 1.9, MAX_FLOOR_DIST 2.4],两端各放宽 0.1cm 浮点余量。稳态贴地时 UE 不做位移,WS 与 UE 等价;区间外(上坡/下台阶/下坡过渡帧)CMС 本来就要做 AdjustFloorHeight 物理 sweep,回退只增加同量级一次查询,且避免 WS 因看不到立面/陡坡而"悬停在坡上行走"或"停位偏高"。

7.1 GP CMC 里的实际采信/回退

情况动作代码位置
WS ClearSafeMoveUpdatedComponent(Delta, MoveQuat, false, OutHit) 直接落位,等价 MoveComponent 对 Clear sweep 的落位(无 PullBack、无阻挡派发)GP CMC :11442-11449
WS Hit 且可信IsTrustedWalkingSurfaceHit → MoveTime = PulledBackHitTime(WSHit.Time, Delta) → 截断落位 + MakeWalkingSurfaceMoveHit 回填 FHitResult:11451-11465, :942-983
WS Fallback / 不可信 Hit / 前置不满足SimProxyMoveSmoothSweepUE(SweepSingle 或原 SweepMulti),统计 Incompatible / HitRejected / NonHorizontal:11382-11413, :11466-11480
FindFloor 结果不权威IsAuthoritativeFloor 为 false → 返回 false → 回退 UE 原生 ComputeFloorDist:11923-11928
悬空放宽(P5)前帧有支撑且当前悬空 ≤ 25cm → 查询 Start.Z 钳到前帧地面;只加速 Clear,Hit/回退照走 UE:11416-11431, :304-314

08GP CMC 生产接入

专题 README 里"未接生产 GP"的结论已经过时:GPCharacterMovementComponent.cpp 里已经落地了完整接入,只是 CVar 默认 0(关闭),处于可随时灰度/回退的状态。

fig6_cmc_modes.png
图 63P 前向 Sweep 的三路分流,以及四个核心 CVar 的模式矩阵。

8.1 接入点

NewSimulateMovement → MoveSmooth → (deferred scope 内唯一一次带 sweep 的移动)
  └─ if (!SimProxyMoveSmoothSweep_WalkingSurface(Delta, Hit))     // :11641
        SimProxyMoveSmoothSweepUE(Delta, MoveQuat, Hit);           // UE 路径

// 另一条独立接入(地面检测):
FindFloor_WalkingSurface(CapsuleLocation, DownDist, OutFloorResult, SweepRadius)  // :11825
  ├─ EnsureWalkingSurfaceHandler() → FWalkingSurfaceMove::Register(..., Capsule)   // :11761-11789
  ├─ FWalkingSurfaceMove::FindFloor(*Handler, WSStart, WSHit, WSDownDist)          // :11884
  └─ IsAuthoritativeFloor(...) → 不权威则回退物理                                  // :11927

8.2 CVar 矩阵(GP CMC :265-368)

CVar默认模式
gp.Movement.WalkingSurfaceHorizontalSweep00 关闭 / 1 Clear+Hit(完整替换)/ 2 仅测量(非 Shipping,统计 WS/UE 耗时与 Hit 一致性)/ 3 仅 Clear(旧 A/B 模式)
gp.Movement.WalkingSurfaceFindFloor00 关闭 / 1 完全替换 FindFloor / 2 仅替换高度图失效时的回退调用
gp.Movement.SimProxySweepSingle10 原 SafeMoveUpdatedComponent(bSweep=true) / 1 用 SweepSingleByChannel 找首个阻挡后 bSweep=false 落位,避免收集沿途 touch
gp.Movement.WalkingSurfaceHorizontalSweep.HoverTolCm25悬空放宽阈值;0 关闭
gp.Movement.WalkingSurfaceFindFloor.TrustMinDist / TrustMaxDist1.8 / 2.5采信区间,与 kFloorTrust* 一致
gp.Movement.WalkingSurfaceFindFloor.Compare*0WS vs UE 原生 sweep 屏幕对照(开关 / 容差 / 绘制 / ForceSync 位掩码),调试用
px.SimProxy.MovementEnginePure0整条 SimProxy 链路一键回退 UE 原生实现(含 FindFloor / AdjustFloorHeight / SmoothCorrection)

8.3 统计与观测

FWalkingSurfaceSweepRuntimeStats(GP CMC :11254-11276)累计 Calls / WSMove / UEMove / NonHorizontal / Incompatible / HoverRelaxed / HoverClear / HitConsumed / HitRejected 等,控制台命令 gp.Movement.WalkingSurfaceHorizontalSweep.Stats 打印耗时直方图。Runtime 侧另有 FSweepStats(SupportNodes / Convex / GapSkipped / Sweeps / NearMiss / BudgetHit)与 FClueQueryStats(Nodes / Candidates / PlanePruned / Exact / HitTile)用于开销归因(Runtime.h:415-425)。

接入设计里最值得学的两点
① 模式 2「仅测量」:行为与关闭完全一致,但额外跑一次 WS 查询,统计两路耗时与 Clear/Hit 一致性——灰度上线前先量化风险,零行为风险。② 每一处降级都有统计:Incompatible 每进程只打一次日志,避免刷屏;所有回退都计数,可直接算出"回退率"这个决定收益上限的关键指标。

09优化前后与实测

收益不是"WS 查询比一次 sweep 快多少"这么简单,而是净收益 = 干净帧占比 × (Sweep − WS 查询) − 回退帧占比 × WS 查询,还要扣掉 I-cache 冷启动税。

gif3_ue_vs_ws.gif
动图 3上半 UE SweepMulti(多次物理扫描 + 收集 touch),下半 WS 只读查询(预烘焙结构、少量节点);下方计时条对比稳态耗时。

9.1 两条路径对照

UE 原生路径WalkingSurface 路径
数据来源运行时物理场景(Chaos),逐帧查询Editor 离线烘焙的 Patches / Tiles / Clues
查询方式ComponentSweepMulti + 强制收集 touch + 穿透处理4-ary BVH 剪枝 → 凸块/平面解析 → 邻域线索查询
覆盖静态 + 动态、复杂碰撞、材质/Actor/Component 元数据仅静态、简单碰撞、仅几何结果(无 Actor/Component/PhysMaterial)
落位按 Hit 截断 + 沿途 overlap 派发Clear → 无 sweep 直落;Hit → PullBack 截断并回填 FHitResult
失效处理天然正确Fallback → 回退 UE;FindFloor 另有 IsAuthoritativeFloor 闸门

9.2 专题实测结论(3p-movesmooth-sweep-skip)

指标数值口径 / 来源
70K 语料对照 UE 静态胶囊 sweep假 Clear 0、多报 0MS4 地图,迭代 12 / B5 / B7 多轮一致(04-conclusion.md:15,45)
采信命中停位误差max 0.0075 cm(p99 0.0031)迭代 12 ClueFirst=1,配对命中 4631 条
地面短步采信率(迭代演进)78.1% → 83.6% → 83.7%迭代 12(04-conclusion.md:50)→ 迭代 13(b7_result.md:41)→ b10(b10_result.md);目标 ≥90% 未达标(语料中 14% 为真碰台阶/陡面/竖面,上限约 86%)
含回退的净开销(迭代 12)0.75× UE(地面) / 0.94×(全部)Editor Development,reps=5;UE 静态 sweep 均值 11.0 / 4.8 µs(04-conclusion.md:16,51-53)
后续收敛(迭代 13 / b7)采信查询 845 ns vs UE 4630 ns(0.18×);地面净开销 0.556× UE、全部 0.911×MS4 70K,reps=5 / inner=8(b7_result.md:35-46)
tile 级线性扫 → 8×8 子格索引23.8 → 8.2 µs(p50 7.5 → 0.7 µs)32m tile 内 8×8(4m)子格(b5_runtime_cost.md:28-34)
生产形态总计 vs UE sweep23.9 → 8.45 µs vs 11.6 µs(首次 1.36× 快)「空夹具」Fast 路径(b5_runtime_cost.md:29-31、memory.md:45)
迭代 25 留出集 / 冷协议采信 ~70%、等价 A/B 全字段差异 0;采信总耗时 0.40× UE、全部查询净开销 0.84× UE2×20,000 独立种子,冷/热双协议(b22_review_retest.md:34-61)
关键口径修正SweepHorizontal 整体 175~240 µs 中 99% 是 Editor 校验夹具病态 scope 155 万图元,不是生产运行时开销(b5_runtime_cost.md:5-20、gotchas.md:28)
收益模型与折损项
专题 03-plan.md 的结论台账给出:收益上限 = sweep 段占比 × 干净帧占比;#11 指出不 sweep 路径仍要付"无 sweep 落位 + scope 析构传播"成本;02-review.md 指出 SimProxy 胶囊 GenerateOverlapEvents=false 时收集到的 touch 被丢弃("白收集的 touch"),正是可优化掉的部分。主要折损:回退率、悬空帧(NoGroundSupport)、I-cache/BTB 冷启动税、动态障碍密集场景。
必须说清的口径边界
· 全部性能数字均为 MS4 地图 + Editor Development 构建 + 离线 70K 语料(50K 异常轨迹 + 20K 地面短步)+ UE 静态胶囊 sweep 对照,不是真实 PIE 帧内收益(04-conclusion.md:41,83)。
· 目标「3p 稳态帧均下降 ≥50%」的真实实测数字:未找到;RF1 基线测量 0 有效样本后被用户标 defer;单客户端 PIE 场内无其它移动英雄导致接入点 calls = 0 无样本(rf-1-baseline.md:7,38-41、README.md:68)。
· 「慢样本 0」只在 70K 语料 + 16.67cm 单步 + 热缓存 min-of-reps 成立;冷协议逐样本中位数约 1%(原始)/ 2.5%(包装) 慢于 UE,30fps 长步长(33.33cm)退化到净开销 0.97×UE(b22_review_retest.md:65-68)。
· 「UE sweep 11.6 µs」是 bench 参照口径,不可当作 3p 帧内分项。启用前应以 gp.Movement.WalkingSurfaceHorizontalSweep = 2(仅测量)在目标场景重新采回退率与耗时直方图。

10风险台账与当前状态

风险表现缓解 / 现状
动态障碍不参与其它角色 / 小兵 / 可移动物不在烘焙数据内,包间可能视觉穿插直至下一个网络包专题澄清记录 #3:人堆场景风险已接受;收包帧本来就是无 sweep 直拍,有现成参照
BlockEdge 漏报方向坡下地面也会出边;LineTrace 中心悬空可能误判可走用户 2026-10-28 选定 G1 几何驱动重设计;GP CMC 默认注册 Capsule 模式规避点查询悬空
下坡落差超窗FindFloorDownDist 默认 200cm,超窗 → Failed/Partial → 回退(安全方向)回退契约保证正确性
I-cache 冷启动税逐帧首次调用的耗时主体是代码/数据冷启动,数据预取无法消除WarmupQueryData 只能缓解数据 miss;专题登记为 P2/P4 测量风险
碰撞契约不一致地图未烘焙水平扫掠,或胶囊通道/响应与烘焙不符GetSweepCollisionFilter 比对,整局走原 sweep,每进程只记一次日志
元数据缺失WS 命中不含 Actor/Component/PhysMaterial/FaceIndex上层需要这些字段时一律回退 UE(MoveInterface.h:120-122)
启用状态专题 README:未正式启用、未接生产 GP、22 文件 3114 行未提交、地图未保存;迭代 26 曾封装 SimProxyMoveSmoothSweep_WalkingSurface 但「封装后版本尚未编译」、PIE calls=0当前源码已落地完整接入(GP CMC :11348 起,含 2026-11-10 的 P5 悬空放宽),但 CVar 默认 0 = 生产未开启、真实帧内收益未验证;px.SimProxy.MovementEnginePure 可整链路回退
专题明确「未找到」的项
SweepMulti 稳态基线值、Clear 帧占比、单次 SweepHorizontal 基线耗时、以及开关开启后 3p 稳态帧均收益(≥50% 目标)均未找到实测值(00-brief.md:15-22 全 TODO、04-conclusion.md:83)。另已登记:冷缓存 I-cache/BTB 税、30fps 长步长退化、SweepHorizontalWithFallback 包装层 p50≈285ns 固定开销、栈占用 40–60KB 的小栈线程风险、单面薄片 Chaos 背面剔除差异、旧精度块读失败连带清空 Patches/Tiles(已修)。
口径提醒
专题文档是 2026-10 ~ 11 月的探索记录,而 GPCharacterMovementComponent.cpp 里已有 2026-11-04 / 11-10 的接入与"用户拍板默认启用悬空放宽"注释,说明实现后来继续推进过。本文以当前源码为准描述实现,以专题文档为准描述实验结论与风险,两者时间戳不同,不要混为"同一版本的状态"。

11代码索引与常量

模块文件 / 符号位置要点
RuntimeWalkingSurfaceRuntime.h:11-29总注释:QueryBVH / 双路径 / 世界 XY 口径
FWalkingSurfaceRuntimeConfig:35-53Tile 3200cm / Grid 20cm / StepLength 100 / MinProgressEps 1
FWalkingSurfaceBVHNode / FWSInlinePoly:66-1204-ary 量化 BVH、int32/int64 热路径
FWSHotArena / FWalkingSurfaceRuntime:303-372一次分配 / 只读并发 / Build
FindFloorLineTrace / FindFloor:473-502两种查询模型与回退契约
SweepHorizontal / IntersectsClues:409-417, :526-543邻域线索剪枝、生产形态入口
TypesFWalkingSurfaceFloorHitQueryTypes.h:16-40Location / ImpactPoint 双点分离
FWalkingSurfaceQueryCache:52-63lastPatch 短路 + Generation 失效
IsAuthoritativeFloor / kFloorTrust*:95-127法线余量 0.02 / 区间 1.8~2.5
EWalkingSurfaceSweepResult / Fallback / SweepHit:163-215三态与全部回退原因
InterfaceFWalkingSurfaceMoveHandlerMoveInterface.h:21-45move-only、per-agent 缓存
FWalkingSurfaceMove:67-128Register / FindFloor / SweepHorizontal / WithFallback
GP CMCCVar 组:265-368四个主开关 + 采信区间 + 对照调试
SimProxyMoveSmoothSweep_WalkingSurface:11348-115603P 前向 sweep 替换主实现
FindFloor_WalkingSurface:11825-11960地面检测替换 + 采信判定
SweepRuntimeStats:11254-11276耗时直方图与判定计数
专题01-clarify.md 关键事实 #1~#7Docs/PerfExplore/3p-movesmooth-sweep-skip基线、贴地、BlockEdge、三接口
04-conclusion.md / memory.md同上70K 对照、净开销、子格索引数字
插件侧 CVar(专题文档登记,判定类多为 ECVF_Cheat)
ws.Horizontal.ClueFirst(默认 1)、ws.Horizontal.WalkableBudget、ws.Sweep.HullPrune、ws.Clue.CheapBound、ws.Bake.Clue.RepresentTolCm / RepresentCos / MaxRadius / MaxHalfHeight / MaxDelta、ws.Horizontal.PerfAudit、ws.Ground.FindFloorFingerprint、ws.Horizontal.ProdRegression、ws.Precision.RoundTrip、gp.Movement.WalkingSurfaceHorizontalSweep.Stats [reset]。烘焙侧另有 Build_bBakeHorizontalSweep(默认开)、ws.Bake.EmitVerticalFaces 等。
版本口径
当前代码为 kBuildVersion = 13 / kPrecisionLayoutVersion = 18;ProbeForward 与 MoveAlongFloor 运行接口、FEdgePrim / EdgeBVH / FWS*Result 均已退役,旧布局 v7–v12 读入时丢弃相关字段(WalkingSurfaceModule.cpp:202-213)。引擎 F:\PX_Trunk\UE UE 5.8.2;插件 F:\PX_Trunk\PX\Plugins\WalkingSurface;集成 F:\PX_Trunk\PX\Source\GPFramework\GPGameplay\Character\Movement\GPCharacterMovementComponent.cpp(约 17500 行,分析期间被并发编辑,行号以函数名为准)。本文所有图示与动图由脚本按源码结构绘制,为示意性可视化,非引擎截图。