16 KiB
攀爬与跑酷模块
脚本入口: eve.Climbing();默认加载槽位为 climbing。
Climbing 将跑酷动作拆为“几何探测 → 硬校验 → 确定性选择 → 胶囊体约束执行”。当前能力覆盖矮墙翻越、撑越、登台、空中抓边、Braced/Free Hang、climb-up、drop、沿边横移、内外转角、跨锚点跳跃、向下攀爬,以及梯子的 mount/climb/dismount。动作差异由 definition 描述,不写死在角色控制器里。
最小用法
local physics = eve.Physics();
local climbing = eve.Climbing();
local world = physics.newWorld3D(0, -9.8, 0, true);
local created = climbing.newRuntime();
if (!created.ok) throw created.error.message;
local mover = created.value; // owned;模块卸载或 release 后句柄失效
local configured = mover.setProfile(
0.30, 1.80, 0.03, // capsule radius / height / skin
1.20, 1.80, // forward probe / max obstacle height
0.65, 0.20, -1); // top normal, warp budget, physics mask
if (!configured.ok) throw configured.error.message;
local mantle = mover.upsertAction(
"core:mantle", 0.55, 1.25, 0.0,
0.60, 0.60, 0.70, 0); // duration, landing forward, apex, selection bias
if (!mantle.ok) throw mantle.error.message;
完整动作可以通过 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:
local begin = mover.tryBegin(world, feetX, feetY, feetZ,
forwardX, forwardZ, speed,
characterBodyId, simulationTick);
if (begin.ok) {
// begin.value.actionId / topPoint / landingFeet 是 owning Value,能安全跨查询读取。
}
空中抓边使用 tryBeginMode,额外提交垂直速度与 grounded 状态。到达 hanging
后可调用 climbUp(tick) 或 drop(tick);悬挂会持续验证 body-local 动态锚点、平台
速度和角色 capsule clearance。
执行期间,每个模拟 tick 调一次 advance:
local step = mover.advance(world, simulationTick, fixedDt);
if (step.ok) {
character.setPosition(step.value.feet.x, step.value.feet.y, step.value.feet.z);
animGraph.setParameter("climbingAction", step.value.actionId);
animGraph.setParameter("climbingTime", step.value.normalizedTime);
}
local events = mover.drainEvents(); // PostPhysics/PostSimulation 阶段
if (!events.ok) throw events.error.message;
foreach (event in events.value) gameEvents.publish(event);
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.