攀爬与跑酷模块
脚本入口: eve.Climbing();默认加载槽位为 climbing。
Climbing 将跑酷动作拆为“几何探测 → 硬校验 → 确定性选择 → 胶囊体约束执行”。当前能力覆盖矮墙翻越、撑越、登台、空中抓边、Braced/Free Hang、climb-up、drop、沿边横移、内外转角、跨锚点跳跃、向下攀爬,以及梯子的 mount/climb/dismount。动作差异由 definition 描述,不写死在角色控制器里。
最小用法
完整动作可以通过 upsertActionKind 指定 vault、mantle、ledge_grab 或 climb_up,并提供悬挂偏移、双手间距和取消窗口。该入口会自动建立稳定动画 notify 契约;调用 validateActionClip(actionId, clip) 验证 clip 后才能启动动作。缺失 contact.left_hand、contact.right_hand 或 land 会返回 climbing.animation.notify_missing,不会在运行时猜测默认帧。
角色请求动作时,将脚底世界坐标、水平朝向、速度、角色刚体 ID 和确定性 tick 传给 tryBegin:
空中抓边使用 tryBeginMode,额外提交垂直速度与 grounded 状态。到达 hanging 后可调用 climbUp(tick) 或 drop(tick);悬挂会持续验证 body-local 动态锚点、平台 速度和角色 capsule clearance。
执行期间,每个模拟 tick 调一次 advance:
desiredDelta 是轨迹/动画希望得到的位移,actualDelta 是物理胶囊体真正允许的位移,warpResidual 是两者之差。超过 profile 的 maxWarpResidual 时 phase 变为 failed,不会穿透障碍。
API 快查
Climbing.getName()→"Climbing"Climbing.newRuntime()→Result<owned ClimbingRuntime>Climbing.newAnchorGraphJson(json, world, body)→Result<owned ClimbingAnchorGraph>Climbing.bakeAnchorGraphJson(requestJson)→Result<{ graphJson, buildSettingsHash, ledgeNodeCount, ladderNodeCount }>Climbing.inspectAnchorGraphJson(graphJson)→ renderer-neutral owning editor overlayClimbingAnchorGraph.node(nodeId)/generation()/reservationCount()ClimbingAnchorGraph.planRoute(startNodeId, goalNodeId, avoidFull, agentId, executionId, maxVisitedNodes):在当前 graph generation 上返回确定性最短 owning route;owner id 必须同时为零或同时非零ClimbingAnchorGraph.reloadJson(json):原子发布新图并返回invalidatedOccupantsClimbingAnchorGraph.release()/isStale()ClimbingRuntime.ownership()→"owned"ClimbingRuntime.setProfile(radius, height, skin, probeDistance, obstacleHeight, minTopNormalY, maxWarpResidual, maskBits)ClimbingRuntime.setProfileJson(json)/reloadProfileJson(json):规范 profile JSON 的初始安装与原子热重载ClimbingRuntime.upsertAction(id, minHeight, maxHeight, minSpeed, duration, landingForward, apexHeight, selectionBias)ClimbingRuntime.upsertActionKind(id, kind, ..., hangBodyOffset, hangFeetBelowLedge, handSpacing, cancelStart, cancelEnd)ClimbingRuntime.validateActionClip(actionId, clip)ClimbingRuntime.tryBegin(world, feetX, feetY, feetZ, forwardX, forwardZ, speed, ignoredBodyId, tick)ClimbingRuntime.tryBeginMode(world, ..., ignoredBodyId, verticalSpeed, grounded, tick)ClimbingRuntime.tryBeginAnchor(graph, world, nodeId, agentId, actionId, ..., tick)ClimbingRuntime.transitionAnchor(graph, world, targetNodeId, edgeKind, actionId, tick)ClimbingRuntime.currentAnchor()→{ graphId, nodeId, graphGeneration }ClimbingRuntime.advance(world, tick, fixedDt)ClimbingRuntime.climbUp(tick)/drop(tick)ClimbingRuntime.cancel(reason, tick):稳定值包括player_request、drop_requested、link_stale、anchor_stale、motion_blocked、warp_budget_exceeded、definition_reloadedClimbingRuntime.drainEvents():仿真后原子取走 owning 事件批次;事件含稳定 kind、actionId、tick 与 executionIdClimbingRuntime.inspect()→ owning debug snapshotClimbingRuntime.setDebugCapture(enabled):关闭时立即释放候选、query、cost 与 motion evidenceClimbingRuntime.snapshotJson()/restoreJson(json, world)ClimbingRuntime.definitionGeneration()→ 十进制字符串(避免脚本整数溢出)ClimbingRuntime.release()/isStale()
C++ definition 与存档契约
ClimbingProfileDefinition 和 ClimbingActionDefinition 使用规范化 eve::Value codec:schema 分别为 evengine.climbing-profile v4 与 evengine.climbing-action v4;解码同时接受 N-1(v3)。解码会先构造并验证完整 owning candidate,再一次性返回; 已知字段有误时整份拒绝。未知根字段保存在 extensionMetadata 并在再次编码时原样输出, 方便编辑器和插件往返处理更高层 metadata。
inputBufferTicks 与 coyoteTicks 是 profile 的版本化权威配置,默认分别为 8 和 6 tick。输入窗口只使用 SimulationTick,不读取 wall clock。普通 Grounded/Airborne 移动也由同一 profile 的加速度、制动、空中控制、重力、跳跃速度、最大坡度、step height 和 ground snap 驱动,并通过 Physics owning capsule mover 写回实际位移。
C++ 输入适配层提交 ClimbingCommand::Climb,所有公开模块与类型统一使用 climbing 命名; parkour 仅作为动作分类和 action id/tag 使用,不存在公开 traversal 模块或 API。 ClimbingInputMode::Flow 偏向速度连续性和 combo,Precision 提高显式朝向、镜头意图与 目标距离的权重。评分在量化后以 action/body/shape identity 稳定决胜,并对刚执行过的 动作施加 definition 中声明的重复惩罚。 候选准备成功后才消费输入并提交 ClimbingExecutionId,所以预检失败不会吞键或部分改变 权威状态。
ClimbingRuntime::snapshot() 输出 evengine.climbing-runtime v4,并包含当前 profile、 definition generation、活动 action snapshot 与尚未派发的事件队列。restore(value, world) 支持 v4 与 N-1(v3;历史 v2/v1/v0 仍可迁移), 并在发布前重新解析 profile、pinned action、candidate、计时、动态平台 body/shape/world 句柄; 未知新版本或 stale link 都不会部分改变当前 runtime。Physics 句柄是 进程内身份,跨进程存档需要场景层先通过持久 ID 重建 Physics world,再调用 restore。
活动 execution 固定开始/分支时的不可变 action snapshot。reloadProfile、 reloadProfileJson 与 upsertAction 都先完整验证候选定义,再递增 generation 并一次发布; 活动动作继续使用旧 snapshot,新动作使用新 generation。普通 setProfile 仍是初始配置入口, 活动期调用返回 conflict。
动画与姿态适配
Climbing 直接验证 AnimClip 的语义 notify,但不会跨帧持有 clip 指针。 applyClimbingPose 是同步 C++ adapter:把 ClimbingAdvance 的左右手 anchor 与 contactWeight 交给 Animation 内置 TwoBoneIK constraint stack。它不保存 skeleton/pose 裸指针;游戏应在动画图输出 base pose 后、skinning 前调用。最终角色位移始终使用 Physics 返回的 actualDelta。
ClimbingMotionInput 可为每个 fixed tick 提交动画 root translation、root yaw、朝向与 pelvis offset。Definition 用 warpWindows 显式开启 horizontal、vertical、facing 通道,并分别限制逐 tick 修正和整段动作预算。Mantle/climb-up 的完整胶囊路径采用 “先抬升到台面净空、再越过边缘、最后落脚”的确定性轨迹;预检与实际执行共享同一函数。 pelvis 超出 maxPelvisDeviation 时本 tick 原子拒绝,warp 在最后窗口仍不能收敛时以 climbing.warp.budget_exceeded 结束,不会传送穿墙。
collision.compact notify 在同一 simulation tick 把物理 mover 切换到 profile 的 compactCapsuleHeight,状态保持到 land;branch.open / branch.close 按 notify 顺序 更新 branchWindowOpen。这些状态进入 runtime snapshot,而不只是一帧动画提示。
Animation binding 会把稳定 clip identity、graph node id、root bone 和 mirror policy 固定进 活动 execution;graphNodeId 是由游戏动画 adapter 解析的外部稳定 ID,Climbing 不持有 动画图节点指针。ClimbingAdvance 输出已解析的 clip/graph/root/mirror 信息与 branchComboTag,镜像 pose 的真正生成仍由 presentation adapter 负责。使用 MotionMatcher 的角色在动作开始时显式让出 locomotion provider 给 AnimGraph one-shot,动作结束后再按 fixed tick 恢复 MotionMatcher,不让两套系统同时拥有姿态。
inspect() 的 bounded owning snapshot 包含 probe/query 线段与胶囊、每个 action 的 hard reject code、量化 bias/height/distance cost,以及 planned/actual/residual capsule motion。 它还包含 broad-phase/query/mover 计数与预算状态。探测先执行固定容量、稳定排序的 owning broad phase,再进行窄相查询;热路径不读取 Physics 的“最近查询”缓存。关闭 HUD 时调用 setDebugCapture(false),稳定预热后的 debug-off probe 路径不会产生逐 tick heap allocation。
显式 Anchor Graph
复杂 ledge、转角、梯子、pole 与 beam 使用版本化 C++ ClimbingAnchorGraphDefinition (schema evengine.climbing-anchor-graph v2,接受 N-1 v1)。节点保存 body-local position、法线/切线、 左右手与脚 socket、语义 kind、tags 和 1–8 个占用 slot;有向边描述 shimmy、corner、 jump、drop、mount、dismount 或 climb。codec 会保留未知 metadata,并规范化节点、边和 tag 顺序;重复 identity、悬空 edge、非正交 frame 或越界 slot 会整图拒绝。
ClimbingAnchorGraphInstance::bind 只保存 Physics world/body 的代际句柄,不跨帧持有 World3D/Body3D 指针。nodeRef 返回 graph-generation-qualified reference; resolveNode 每次从 body-local frame 重建世界 socket 与 point velocity,所以运动平台 平移/旋转后仍使用当前锚点,body 销毁或旧 generation 会返回 stale_handle。
ClimbingAnchorNodeRef 同时带 graphId、node id 与 graph generation,不同图即使节点同名、 代数相同也不能混用。reserve 为 agent + execution 确定性选择最低空闲 slot,返回不可伪造的 owning credential; release 校验 reservation id、generation、node、slot、agent 和 execution。图热重载先完整验证 新候选,再一次发布并返回排序后的 invalidatedOccupants,由 gameplay 在安全窗口取消 或显式迁移;旧 node reference 与 reservation token 均立即 stale。
runtime v4 snapshot 保存 graph handle/epoch、node identity、slot、occupant 与 reservation claim generation。恢复到另一个 runtime 时,仍存活的 claim 会原子转移并使旧 runtime 的凭证 stale; 旧 runtime 已析构时只会重获原 slot,不会挤掉已有占用。重复恢复同一旧 snapshot 返回 conflict, 因此任意时刻只有一个 runtime 能释放该 slot。graph 与 Physics handle 仍是进程内 identity; 跨进程读取前必须由场景层按持久资源 ID 重建并重映射这些 link。
编辑器或关卡工具使用 evengine.climbing-anchor-bake-request v1 提交 body-local 语义几何: ledge 折线可带逐点墙面法线,ladder 提交底部、up/normal、宽度、间距和 rung 数量。 bakeClimbingAnchorGraph 确定性生成 ledge/corner、shimmy 边、ladder rungs 以及 mount/climb/dismount 边,并将 source geometry content id 与全部 build settings/source 采样纳入稳定 fnv1a64 build hash。输入候选完整验证成功后才返回 graph,不会发布半张图。
inspectClimbingAnchorGraphAuthoring(脚本入口 inspectAnchorGraphJson)返回完整 owning、 渲染无关的 node/edge overlay,包含 frame、手脚 socket、slot 和语义类型。高层 Editor 或 Graphics adapter 可以选择颜色与绘制方式;climbing 本身不反向依赖这两个高层模块。
生命周期与确定性
- Runtime 由
Climbing模块独占,脚本代理是 owned;不要保存 C++ 裸指针。 - Runtime 不持有
World3D指针。候选保存 world/body/shape 的代际句柄,跨帧使用时重新验证。 - 探测会临时设置 Physics query filter 和忽略角色自身,返回前恢复原状态。
- 选择分数按毫米量化,并以 action ID、body ID、shape ID 稳定决胜;同一输入与世界快照产生同一排序。
- 时间只来自调用方注入的 tick 与 fixed dt;重复或倒退 tick 返回
conflict。 - started/contact/hanging/drop/land/completed/cancelled/failed 由 runtime 的单一有界队列拥有;
advance、drop、cancel不调用未知代码。游戏必须在仿真阶段结束后调用drainEvents(), 再派发脚本或回调。批次会先从 runtime 脱离,因此回调重入产生的新事件留给下一次 drain; 队列达到 64 条时,新状态变更以conflict原子退回,不会静默丢事件。 - 游戏适配层把
actionId + normalizedTime + desired/actualDelta喂给动画图,并在同一帧应用可选手部 pose constraint;物理位移始终以actualDelta为准。 - stamina 不在 Climbing 内复制。profile 采用
RequireProvider时必须声明稳定staminaAdapter,运行时只接受 identity 完全匹配的 authority;不匹配会在输入消费和 execution 创建之前返回错误。 - action 的
eventMetadata随 started/contact/land/terminal 事件一同进入 owning 队列和 snapshot;事件消费者不必再反查可能已热重载的 definition。
已变形的根运动输入
ClimbingMotionInput::rootMotionPolicy 默认 ApplyActionWarp,维持原有轨迹与 warp 行为。动画适配层已经按接触窗口完成变形时,可选择 PreserveSuppliedDelta, 并设置 hasRootMotion:runtime 消费本帧提供的位移,不再叠加程序轨迹或要求它 收敛到程序轨迹终点;实际运动仍受胶囊碰撞及残差预算约束。
obstacleCollision 默认 Collide。IgnoreTraversedShape 只对当前 advance 查询 排除 execution 保存且经代际验证的障碍 shape,保留角色自身过滤和所有其他 shape 的碰撞。它不修改世界过滤器或持久碰撞状态。无根运动、无效枚举或过期 shape 在推进 tick 前失败。上述值由调用方拥有,在世界的仿真线程同步消费,不保存指针、 不调用回调,不新增持久格式;时间来自注入的 tick/dt。
Squirrel 对应入口为 runtime.advanceWarped(world, tick, dt, dx, dy, dz, facingX, facingZ, ignoreObstacle), 返回与 advance 相同的结构化结果。游戏必须使用返回的实际位置,并在动作结束后 恢复普通移动与地面检测。此入口只负责运动消费,动画窗口与骨骼约束属于适配层。
runtime.probeMode(world, feetX, feetY, feetZ, facingX, facingZ, speed, verticalSpeed, grounded) evaluates a traversal candidate for the supplied movement state without starting execution; inspect ok and diagnostics before consuming value.