载入中...
搜索中...
未找到
Climbing.cpp
浏览该文件的文档.
1#include "climbing/Climbing.h"
2
6
8#include "common/Assert.h"
9#include "physics/Body3D.h"
10#include "physics/World3D.h"
11
12#include <algorithm>
13#include <chrono>
14#include <charconv>
15#include <cmath>
16#include <limits>
17#include <utility>
18
19namespace eve::climbing {
20using namespace runtime_detail;
21
22
24 std::string actionId;
25 actionId.swap(candidate.actionId);
26 considerWithActionId(std::move(candidate), actionId);
27}
28
29void ClimbingCandidateSet::considerWithActionId(ClimbingCandidate candidate, std::string_view actionId) {
30 const auto store = [&](ClimbingCandidate& slot) {
31 std::string reused = std::move(slot.actionId);
32 slot = std::move(candidate);
33 if (reused.capacity() >= actionId.size()) {
34 reused.assign(actionId);
35 slot.actionId.swap(reused);
36 } else {
37 slot.actionId.assign(actionId);
38 }
39 };
40 if (size_ < Capacity) {
41 store(values_[size_++]);
42 return;
43 }
44 auto worst = std::max_element(values_.begin(), values_.end(), isCandidateLess);
45 const bool better = candidate.score != worst->score
46 ? candidate.score < worst->score
47 : actionId != worst->actionId
48 ? actionId < std::string_view(worst->actionId)
49 : candidate.obstacleBodyId != worst->obstacleBodyId
50 ? candidate.obstacleBodyId < worst->obstacleBodyId
51 : candidate.obstacleShapeId < worst->obstacleShapeId;
52 if (better) store(*worst);
53}
54
56 std::sort(values_.begin(), values_.begin() + static_cast<std::ptrdiff_t>(size_), isCandidateLess);
57 size_ = std::min(size_, std::min(limit, Capacity));
58}
59
61 values_.swap(other.values_);
62 std::swap(size_, other.size_);
63}
64
65eve::Result<void> ClimbingRuntime::requireEventCapacity(std::size_t count, eve::SimulationTick tick) const {
66 if (count > PendingEventCapacity - pendingEvents_.size())
68 eve::DiagnosticCode::Conflict, "climbing event queue must be drained before simulation can continue",
69 "runtime.pendingEvents", {}, "climbing"));
70 if (!pendingEvents_.empty() && tick < pendingEvents_.back().tick)
72 eve::DiagnosticCode::Conflict, "climbing event tick must not precede an undelivered event",
73 "runtime.pendingEvents.tick", {}, "climbing"));
75}
76
77void ClimbingRuntime::enqueueEvent(ClimbingEvent event) {
78 EV_ASSERT(pendingEvents_.size() < PendingEventCapacity, "climbing event queue capacity invariant");
79 const bool ordered = pendingEvents_.empty() || pendingEvents_.back().tick <= event.tick;
80 EV_ASSERT(ordered, "climbing event production order must be monotonic by tick");
81 if (event.metadata.empty() && execution_ && execution_->action.id == event.actionId)
82 event.metadata = execution_->action.eventMetadata;
83 pendingEvents_.push_back(std::move(event));
84}
85
92
94 return publishProfile(std::move(profile), false);
95}
96
98 return {profile_.groundAcceleration, profile_.groundBraking, profile_.airControl,
99 profile_.gravity, profile_.jumpSpeed, profile_.coyoteTicks, profile_.queryFilter};
100}
101
102eve::Result<void> ClimbingRuntime::publishProfile(ClimbingProfile profile, bool allowActiveExecution) {
104 if (!valid) return eve::Result<void>::failure(valid.status());
105 if (!allowActiveExecution && isActivePhase(phase_))
107 "profile cannot change while an execution is active",
108 "runtime.phase", {}, "climbing"));
109 if (definitionGeneration_ == std::numeric_limits<std::uint64_t>::max())
111 "climbing definition generation is exhausted",
112 "runtime.definitionGeneration", {}, "climbing"));
113 std::sort(profile.actions.begin(), profile.actions.end(),
114 [](const auto& lhs, const auto& rhs) { return lhs.id < rhs.id; });
115 const auto duplicate = std::adjacent_find(profile.actions.begin(), profile.actions.end(),
116 [](const auto& lhs, const auto& rhs) { return lhs.id == rhs.id; });
117 if (duplicate != profile.actions.end())
119 eve::DiagnosticCode::Conflict, "profile contains duplicate action ids", "profile.actions", {}, "climbing"));
120 profile_ = std::move(profile);
121 validatedAnimationActions_.clear();
122 ++definitionGeneration_;
124}
125
128 if (!valid) return eve::Result<void>::failure(valid.status());
129 if (definitionGeneration_ == std::numeric_limits<std::uint64_t>::max())
131 "climbing definition generation is exhausted",
132 "runtime.definitionGeneration", {}, "climbing"));
133 const std::string actionId = action.id;
134 auto found = std::lower_bound(profile_.actions.begin(), profile_.actions.end(), action.id,
135 [](const auto& entry, const std::string& id) { return entry.id < id; });
136 if (found != profile_.actions.end() && found->id == action.id)
137 *found = std::move(action);
138 else
139 profile_.actions.insert(found, std::move(action));
140 validatedAnimationActions_.erase(actionId);
141 ++definitionGeneration_;
143}
144
146 const animation::AnimClip& clip) {
147 const ClimbingActionDefinition* action = findAction(profile_, actionId);
148 if (!action)
150 "animation binding references an unknown action",
151 "actionId", {}, "climbing"));
152 if (!action->animation.clipId.empty() && clip.getName() != action->animation.clipId)
154 "animation clip does not match the action binding",
155 "action.animation.clipId", {}, "climbing"));
156 auto validated = clip.validateNotifyContract(action->requiredNotifies);
157 if (!validated)
160 "climbing.animation.notify_missing: " + validated.status().describe(),
161 "action.requiredNotifies", {}, "climbing"));
162 validatedAnimationActions_.insert(action->id);
164}
165
173
177 lastCandidates_.clear();
178 lastQueryCount_ = 0;
179 lastCounters_ = {};
182 RuntimeTelemetryScope telemetryScope(telemetry_, lastCounters_, tick);
183 if (debugCapture_ == ClimbingDebugCapture::Enabled) {
184 lastDebugQueries_.clear();
185 lastEvidence_.clear();
186 }
188 if (!valid) return eve::Result<void>::failure(valid.status());
189 if (!world.isValid())
191 eve::DiagnosticCode::StaleHandle, "physics world is no longer valid", "world", {}, "climbing"));
192 if (!isFinite(pose.feet) || !isFinite(pose.forward) || !isFinite(pose.speed) || pose.speed < 0.f ||
193 !isFinite(pose.verticalSpeed) || !isFinite(pose.moveIntent) || !isFinite(pose.lookIntent))
195 "pose values must be finite and speed non-negative",
196 "pose", {}, "climbing"));
197 const Vec3 forward = normalizedHorizontal(pose.forward);
198 if (lengthSquared(forward) <= epsilon)
200 "pose forward must have a horizontal direction",
201 "pose.forward", {}, "climbing"));
202
203 ClimbingCandidateSet& candidates = probeScratch_;
204 candidates.clear();
205 const auto reserveQuery = [&]() noexcept {
206 if (lastQueryCount_ >= lastCounters_.queryBudget) {
208 return false;
209 }
210 ++lastQueryCount_;
211 lastCounters_.queryCount = lastQueryCount_;
212 return true;
213 };
214 const auto rejectCandidate = [&]() noexcept { ++lastCounters_.rejectCount; };
215 const auto finalizeCandidates = [&]() -> eve::Result<void> {
216 candidates.sortAndLimit(profile_.maxCandidates);
217 if (!candidates.empty()) lastCounters_.selectedCost = candidates.front().score;
218 if (debugCapture_ == ClimbingDebugCapture::Enabled)
219 lastCandidates_.assign(candidates.begin(), candidates.end());
220 candidates.swap(output);
222 };
223
224 const auto captureBodyLocal = [&](ClimbingCandidate& candidate) -> eve::Result<void> {
225 if (physics::Body3D* body = world.findBody(candidate.obstacleBody)) {
226 auto localTop = body->worldToLocalPointOwned(candidate.topPoint.x, candidate.topPoint.y,
227 candidate.topPoint.z);
228 if (!localTop) return eve::Result<void>::failure(localTop.status());
229 auto localLanding = body->worldToLocalPointOwned(candidate.landingFeet.x, candidate.landingFeet.y,
230 candidate.landingFeet.z);
231 if (!localLanding) return eve::Result<void>::failure(localLanding.status());
232 candidate.bodyLocalTop = {localTop.value().x, localTop.value().y, localTop.value().z};
233 candidate.bodyLocalLanding = {localLanding.value().x, localLanding.value().y,
234 localLanding.value().z};
235 }
237 };
238
240 filter.ignoredBodyId = pose.ignoredBodyId;
241 const float broadRadius = profile_.maxProbeDistance + profile_.capsuleRadius + profile_.skin;
242 auto broadPhase = world.queryAabbBroadPhaseOwned(
243 pose.feet.x - broadRadius, pose.feet.y - profile_.skin, pose.feet.z - broadRadius,
244 pose.feet.x + broadRadius, pose.feet.y + profile_.maxObstacleHeight + profile_.capsuleHeight,
245 pose.feet.z + broadRadius, filter);
246 if (!broadPhase) return eve::Result<void>::failure(broadPhase.status());
247 lastCounters_.broadPhaseQueryCount = 1;
248 lastCounters_.broadPhaseHitCount = static_cast<std::uint32_t>(broadPhase.value().count);
249 if (broadPhase.value().count == 0) return finalizeCandidates();
250 const auto broadPhaseContains = [&](physics::PhysicsShapeHandle shape) noexcept {
251 if (broadPhase.value().truncated) return true;
252 return std::any_of(broadPhase.value().hits.begin(),
253 broadPhase.value().hits.begin() +
254 static_cast<std::ptrdiff_t>(broadPhase.value().count),
255 [&](const physics::BroadPhaseHit3D& hit) { return hit.shape == shape; });
256 };
257
258 const bool hasSlide = std::any_of(profile_.actions.begin(), profile_.actions.end(), [&](const auto& action) {
259 return action.kind == ClimbingActionKind::Slide && isActionEnabledForPose(profile_, action, pose) &&
260 isProbeRecipeMatchingKind(action);
261 });
262 if (hasSlide && pose.grounded) {
263 const Vec3 groundStart{pose.feet.x, pose.feet.y + 0.25f, pose.feet.z};
264 const Vec3 groundEnd{pose.feet.x, pose.feet.y - 0.35f, pose.feet.z};
265 if (!reserveQuery()) return finalizeCandidates();
266 auto groundResult = world.rayCastOwned(groundStart.x, groundStart.y, groundStart.z,
267 groundEnd.x, groundEnd.y, groundEnd.z, filter);
268 if (!groundResult) return eve::Result<void>::failure(groundResult.status());
269 const physics::RayHit3D ground = std::move(groundResult).takeValue();
270 if (debugCapture_ == ClimbingDebugCapture::Enabled)
271 boundedDebugPush(lastDebugQueries_, ClimbingDebugQuery{"", ground.hit ? "slide.ground" : "slide.no_ground",
272 groundStart, groundEnd});
273 if (ground.hit && broadPhaseContains(ground.shape) && ground.normalY >= profile_.minTopNormalY) {
274 for (const auto& action : profile_.actions) {
277 continue;
278 if (pose.speed + epsilon < action.minSpeed) {
279 rejectCandidate();
280 if (debugCapture_ == ClimbingDebugCapture::Enabled)
281 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "below_min_speed"});
282 continue;
283 }
284 if (!isSupportSelectorMatch(action, ground.shapeTag, ground.materialId)) {
285 rejectCandidate();
286 if (debugCapture_ == ClimbingDebugCapture::Enabled)
287 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "support_tag_rejected"});
288 continue;
289 }
290 const Vec3 landing = pose.feet + forward * action.landingForward;
291 const float lowerY = landing.y + profile_.capsuleRadius + profile_.skin;
292 const float upperY = landing.y + profile_.compactCapsuleHeight - profile_.capsuleRadius + profile_.skin;
293 if (!reserveQuery()) return finalizeCandidates();
294 auto overlap = world.queryCapsuleOwned(landing.x, lowerY, landing.z, landing.x, upperY, landing.z,
295 profile_.capsuleRadius, filter);
296 if (!overlap) return eve::Result<void>::failure(overlap.status());
297 if (overlap.value().bodyCount != 0) {
298 rejectCandidate();
299 if (debugCapture_ == ClimbingDebugCapture::Enabled)
300 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "clearance_blocked"});
301 continue;
302 }
303 ClimbingCandidate candidate;
304 candidate.definitionGeneration = definitionGeneration_;
305 candidate.world = world.runtimeHandle();
306 candidate.obstacleBody = ground.body;
307 candidate.obstacleShape = ground.shape;
308 candidate.obstacleBodyId = ground.bodyId;
309 candidate.obstacleShapeId = ground.shapeId;
310 candidate.ignoredBodyId = pose.ignoredBodyId;
311 candidate.frontPoint = {ground.x, ground.y, ground.z};
312 candidate.topPoint = candidate.frontPoint;
313 candidate.landingFeet = landing;
314 candidate.surfaceNormal = {ground.normalX, ground.normalY, ground.normalZ};
315 candidate.surfaceTangent = forward;
316 candidate.gapDistance = action.landingForward;
317 candidate.clearanceHeight = profile_.compactCapsuleHeight;
318 candidate.slopeRadians = std::acos(std::clamp(ground.normalY, -1.f, 1.f));
319 candidate.supportShapeTag = ground.shapeTag;
320 candidate.supportMaterialId = ground.materialId;
321 candidate.probeRecipe = action.probeRecipe;
322 candidate.score = selectionCost(profile_, action, pose, forward, landing - pose.feet,
323 0.f, action.landingForward) +
324 (previousActionId_ == action.id ? action.repetitionPenalty : 0);
325 candidate.kind = action.kind;
326 auto captured = captureBodyLocal(candidate);
327 if (!captured) return eve::Result<void>::failure(captured.status());
328 if (debugCapture_ == ClimbingDebugCapture::Enabled)
330 action.id, "accepted", static_cast<std::int64_t>(action.selectionBias) * 1000000ll,
331 0, quantizeMillimeters(action.landingForward), candidate.score});
332 candidates.considerWithActionId(std::move(candidate), action.id);
333 }
334 }
335 }
336
337 const bool hasWallRun = std::any_of(profile_.actions.begin(), profile_.actions.end(), [&](const auto& action) {
338 return action.kind == ClimbingActionKind::WallRun && isActionEnabledForPose(profile_, action, pose) &&
339 isProbeRecipeMatchingKind(action);
340 });
341 if (hasWallRun) {
342 const Vec3 chest{pose.feet.x, pose.feet.y + profile_.capsuleHeight * 0.55f, pose.feet.z};
343 const Vec3 sides[2] = {{-forward.z, 0.f, forward.x}, {forward.z, 0.f, -forward.x}};
344 for (std::size_t sideIndex = 0; sideIndex < 2; ++sideIndex) {
345 const Vec3 end = chest + sides[sideIndex] * profile_.maxProbeDistance;
346 if (!reserveQuery()) return finalizeCandidates();
347 auto sideResult = world.rayCastOwned(chest.x, chest.y, chest.z, end.x, end.y, end.z, filter);
348 if (!sideResult) return eve::Result<void>::failure(sideResult.status());
349 const physics::RayHit3D hit = std::move(sideResult).takeValue();
350 if (debugCapture_ == ClimbingDebugCapture::Enabled)
351 boundedDebugPush(lastDebugQueries_, ClimbingDebugQuery{"", hit.hit ? "wall_run.wall" : "wall_run.miss",
352 chest, end});
353 Vec3 normal = hit.hit ? normalizedHorizontal({hit.normalX, hit.normalY, hit.normalZ}) : Vec3{};
354 if (!hit.hit || !broadPhaseContains(hit.shape) || lengthSquared(normal) <= epsilon ||
355 std::fabs(hit.normalY) > 0.35f)
356 continue;
357 Vec3 tangent{normal.z, 0.f, -normal.x};
358 if (tangent.x * forward.x + tangent.z * forward.z < 0.f) tangent = tangent * -1.f;
359 for (const auto& action : profile_.actions) {
361 !isProbeRecipeMatchingKind(action) || pose.speed + epsilon < action.minSpeed)
362 continue;
363 if (!isSupportSelectorMatch(action, hit.shapeTag, hit.materialId)) {
364 rejectCandidate();
365 continue;
366 }
367 const float wallOffset = profile_.capsuleRadius + profile_.skin;
368 const Vec3 landing{hit.x + normal.x * wallOffset + tangent.x * action.landingForward,
369 pose.feet.y, hit.z + normal.z * wallOffset + tangent.z * action.landingForward};
370 const float lowerY = landing.y + profile_.capsuleRadius + profile_.skin;
371 const float upperY = landing.y + profile_.capsuleHeight - profile_.capsuleRadius + profile_.skin;
372 if (!reserveQuery()) return finalizeCandidates();
373 auto overlap = world.queryCapsuleOwned(landing.x, lowerY, landing.z, landing.x, upperY, landing.z,
374 profile_.capsuleRadius, filter);
375 if (!overlap) return eve::Result<void>::failure(overlap.status());
376 if (overlap.value().bodyCount != 0) {
377 rejectCandidate();
378 continue;
379 }
380 ClimbingCandidate candidate;
381 candidate.definitionGeneration = definitionGeneration_;
382 candidate.world = world.runtimeHandle();
383 candidate.obstacleBody = hit.body;
384 candidate.obstacleShape = hit.shape;
385 candidate.obstacleBodyId = hit.bodyId;
386 candidate.obstacleShapeId = hit.shapeId;
387 candidate.ignoredBodyId = pose.ignoredBodyId;
388 candidate.frontPoint = {hit.x, hit.y, hit.z};
389 candidate.topPoint = candidate.frontPoint;
390 candidate.landingFeet = landing;
391 candidate.surfaceNormal = normal;
392 candidate.surfaceTangent = tangent;
393 const float distance = std::sqrt((hit.x - chest.x) * (hit.x - chest.x) +
394 (hit.z - chest.z) * (hit.z - chest.z));
395 candidate.gapDistance = distance;
396 candidate.clearanceHeight = profile_.capsuleHeight;
397 candidate.slopeRadians = std::acos(std::clamp(hit.normalY, -1.f, 1.f));
398 candidate.supportShapeTag = hit.shapeTag;
399 candidate.supportMaterialId = hit.materialId;
400 candidate.probeRecipe = action.probeRecipe;
401 candidate.score = selectionCost(profile_, action, pose, tangent, landing - pose.feet,
402 0.f, distance, static_cast<std::int64_t>(sideIndex)) +
403 (previousActionId_ == action.id ? action.repetitionPenalty : 0) +
404 0;
405 candidate.kind = action.kind;
406 auto captured = captureBodyLocal(candidate);
407 if (!captured) return eve::Result<void>::failure(captured.status());
408 if (debugCapture_ == ClimbingDebugCapture::Enabled)
410 action.id, "accepted", static_cast<std::int64_t>(action.selectionBias) * 1000000ll,
411 0, quantizeMillimeters(distance), candidate.score});
412 candidates.considerWithActionId(std::move(candidate), action.id);
413 }
414 }
415 }
416
417 const bool hasObstacleProbe =
418 std::any_of(profile_.actions.begin(), profile_.actions.end(), [&](const auto& action) {
419 return isObstacleProbeKind(action.kind) && isActionEnabledForPose(profile_, action, pose) &&
420 isProbeRecipeMatchingKind(action);
421 });
422 if (!hasObstacleProbe) return finalizeCandidates();
423 // A single chest-height ray skips low vault obstacles entirely. The obstacle recipe starts just above the
424 // capsule's lower hemisphere so it can see every valid obstacle above the profile's physical skin; top and
425 // landing queries still provide the authoritative height and clearance rejection.
426 const float obstacleProbeY = pose.feet.y + profile_.capsuleRadius + profile_.skin;
427 const Vec3 frontStart{pose.feet.x, obstacleProbeY, pose.feet.z};
428 const Vec3 frontEnd = frontStart + forward * profile_.maxProbeDistance;
429 if (!reserveQuery()) return finalizeCandidates();
430 auto frontResult =
431 world.rayCastOwned(frontStart.x, frontStart.y, frontStart.z, frontEnd.x, frontEnd.y, frontEnd.z, filter);
432 if (!frontResult) return eve::Result<void>::failure(frontResult.status());
433 const physics::RayHit3D front = std::move(frontResult).takeValue();
434 if (debugCapture_ == ClimbingDebugCapture::Enabled)
435 boundedDebugPush(lastDebugQueries_, ClimbingDebugQuery{"", front.hit ? "front.hit" : "front.miss",
436 frontStart, frontEnd});
437 if (!front.hit || !broadPhaseContains(front.shape)) return finalizeCandidates();
438
439 const Vec3 overFront{front.x + forward.x * (profile_.capsuleRadius + profile_.skin),
440 pose.feet.y + profile_.maxObstacleHeight + profile_.skin,
441 front.z + forward.z * (profile_.capsuleRadius + profile_.skin)};
442 const Vec3 topEnd{overFront.x, pose.feet.y + profile_.skin, overFront.z};
443 if (!reserveQuery()) return finalizeCandidates();
444 auto topResult = world.rayCastOwned(overFront.x, overFront.y, overFront.z, topEnd.x, topEnd.y, topEnd.z, filter);
445 if (!topResult) return eve::Result<void>::failure(topResult.status());
446 const physics::RayHit3D top = std::move(topResult).takeValue();
447 if (debugCapture_ == ClimbingDebugCapture::Enabled)
448 boundedDebugPush(lastDebugQueries_, ClimbingDebugQuery{"", top.hit ? "top.hit" : "top.miss", overFront,
449 topEnd});
450 if (!top.hit || top.body != front.body || top.normalY < profile_.minTopNormalY)
451 return finalizeCandidates();
452
453 const float height = top.y - pose.feet.y;
454 if (height < 0.f || height > profile_.maxObstacleHeight + profile_.skin)
455 return finalizeCandidates();
456
457 const float baseDepth = std::sqrt((top.x - front.x) * (top.x - front.x) +
458 (top.z - front.z) * (top.z - front.z));
459 float measuredDepth = baseDepth;
460 float measuredCurvature = 0.f;
461 const float topSlope = std::acos(std::clamp(top.normalY, -1.f, 1.f));
462 float depthProbeDistance = 0.f;
463 bool needsDepthEvidence = false;
464 for (const ClimbingActionDefinition& action : profile_.actions) {
465 if (!isObstacleProbeKind(action.kind) || !isActionEnabledForPose(profile_, action, pose) ||
467 continue;
468 if (action.minDepth > baseDepth + epsilon || action.maxDepth < 999.f || action.maxCurvature < 999.f) {
469 needsDepthEvidence = true;
470 depthProbeDistance = std::max(depthProbeDistance, std::max(action.minDepth, action.maxDepth < 999.f
471 ? action.maxDepth + profile_.skin
472 : 0.f));
473 }
474 }
475 depthProbeDistance = std::min(profile_.maxProbeDistance, std::max(0.f, depthProbeDistance - baseDepth));
476 if (needsDepthEvidence && depthProbeDistance > epsilon) {
477 const std::uint32_t sampleCount = std::min<std::uint32_t>(4, profile_.probeSectors);
478 Vec3 previousNormal{top.normalX, top.normalY, top.normalZ};
479 const float stepDistance = depthProbeDistance / static_cast<float>(sampleCount);
480 for (std::uint32_t sample = 1; sample <= sampleCount; ++sample) {
481 if (!reserveQuery()) break;
482 const float offset = stepDistance * static_cast<float>(sample);
483 const Vec3 sampleStart{top.x + forward.x * offset, top.y + profile_.skin + 0.2f,
484 top.z + forward.z * offset};
485 const Vec3 sampleEnd{sampleStart.x, top.y - profile_.skin - 0.2f, sampleStart.z};
486 auto sampled = world.rayCastOwned(sampleStart.x, sampleStart.y, sampleStart.z, sampleEnd.x,
487 sampleEnd.y, sampleEnd.z, filter);
488 if (!sampled) return eve::Result<void>::failure(sampled.status());
489 const physics::RayHit3D hit = std::move(sampled).takeValue();
490 if (!hit.hit || hit.body != top.body || hit.normalY < profile_.minTopNormalY) break;
491 measuredDepth = baseDepth + offset;
492 const Vec3 normal{hit.normalX, hit.normalY, hit.normalZ};
493 const float dot = std::clamp(previousNormal.x * normal.x + previousNormal.y * normal.y +
494 previousNormal.z * normal.z,
495 -1.f, 1.f);
496 measuredCurvature = std::max(measuredCurvature, std::acos(dot) / std::max(stepDistance, epsilon));
497 previousNormal = normal;
498 }
499 }
500
501 for (const ClimbingActionDefinition& action : profile_.actions) {
502 if (!isActionEnabledForPose(profile_, action, pose)) {
503 rejectCandidate();
504 if (debugCapture_ == ClimbingDebugCapture::Enabled)
505 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "definition_policy_rejected"});
506 continue;
507 }
509 rejectCandidate();
510 if (debugCapture_ == ClimbingDebugCapture::Enabled)
511 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "probe_recipe_rejected"});
512 continue;
513 }
514 if (!isObstacleProbeKind(action.kind)) {
515 if (isRuntimeProbeKind(action.kind)) continue;
516 rejectCandidate();
517 if (debugCapture_ == ClimbingDebugCapture::Enabled)
518 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "anchor_graph_only"});
519 continue;
520 }
521 if (height + epsilon < action.minHeight || height - epsilon > action.maxHeight) {
522 rejectCandidate();
523 if (debugCapture_ == ClimbingDebugCapture::Enabled)
524 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "out_of_height_range"});
525 continue;
526 }
527 if (measuredDepth + epsilon < action.minDepth || measuredDepth - epsilon > action.maxDepth ||
528 topSlope - epsilon > action.maxSlopeRadians || measuredCurvature - epsilon > action.maxCurvature ||
529 !isSupportSelectorMatch(action, top.shapeTag, top.materialId)) {
530 rejectCandidate();
531 if (debugCapture_ == ClimbingDebugCapture::Enabled)
532 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "support_geometry_rejected"});
533 continue;
534 }
535 if (pose.speed + epsilon < action.minSpeed) {
536 rejectCandidate();
537 if (debugCapture_ == ClimbingDebugCapture::Enabled)
538 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "below_min_speed"});
539 continue;
540 }
541 const bool wantsHang = action.kind == ClimbingActionKind::LedgeGrab;
542 if ((wantsHang && (pose.grounded || pose.verticalSpeed > epsilon)) || (!wantsHang && !pose.grounded)) {
543 rejectCandidate();
544 if (debugCapture_ == ClimbingDebugCapture::Enabled)
545 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "source_mode_rejected"});
546 continue;
547 }
548
549 const Vec3 landing =
550 wantsHang
551 ? Vec3{front.x - forward.x * (profile_.capsuleRadius + profile_.skin + action.hangBodyOffset),
552 top.y - action.hangFeetBelowLedge,
553 front.z - forward.z * (profile_.capsuleRadius + profile_.skin + action.hangBodyOffset)}
554 : Vec3{top.x + forward.x * action.landingForward, top.y, top.z + forward.z * action.landingForward};
555 const float lowerY = landing.y + profile_.capsuleRadius + profile_.skin;
556 const float upperY = landing.y + profile_.capsuleHeight - profile_.capsuleRadius + profile_.skin;
557 if (!reserveQuery()) return finalizeCandidates();
558 auto overlap = world.queryCapsuleOwned(landing.x, lowerY, landing.z, landing.x, upperY, landing.z,
559 profile_.capsuleRadius, filter);
560 if (!overlap) return eve::Result<void>::failure(overlap.status());
561 if (debugCapture_ == ClimbingDebugCapture::Enabled)
562 boundedDebugPush(lastDebugQueries_,
564 overlap.value().bodyCount == 0 ? "landing.clear" : "landing.blocked",
565 landing, landing, profile_.capsuleRadius, profile_.capsuleHeight});
566 if (overlap.value().bodyCount != 0) {
567 rejectCandidate();
568 if (debugCapture_ == ClimbingDebugCapture::Enabled)
569 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "clearance_blocked"});
570 continue;
571 }
572
574 if (wantsHang) {
575 const Vec3 supportStart{landing.x, landing.y + profile_.capsuleHeight * 0.45f, landing.z};
576 const Vec3 supportEnd =
577 supportStart + forward * (profile_.capsuleRadius + profile_.skin + action.hangBodyOffset + 0.25f);
578 if (!reserveQuery()) return finalizeCandidates();
579 auto supportHit = world.rayCastOwned(supportStart.x, supportStart.y, supportStart.z, supportEnd.x,
580 supportEnd.y, supportEnd.z, filter);
581 if (!supportHit) return eve::Result<void>::failure(supportHit.status());
582 if (debugCapture_ == ClimbingDebugCapture::Enabled)
583 boundedDebugPush(lastDebugQueries_,
585 supportHit.value().hit ? "support.hit" : "support.miss",
586 supportStart, supportEnd});
587 support = supportHit.value().hit && supportHit.value().body == front.body ? HangSupport::Braced
589 }
590
591 const float distance = std::sqrt((front.x - pose.feet.x) * (front.x - pose.feet.x) +
592 (front.z - pose.feet.z) * (front.z - pose.feet.z));
593 if (distance + epsilon < action.minDistance || distance - epsilon > action.maxDistance ||
594 front.normalY + epsilon < action.minSurfaceNormalY) {
595 rejectCandidate();
596 if (debugCapture_ == ClimbingDebugCapture::Enabled)
597 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "geometry_policy_rejected"});
598 continue;
599 }
600 const Vec3 approachNormal = normalizedHorizontal({-front.normalX, 0.f, -front.normalZ});
601 const float heightCenter = (action.minHeight + action.maxHeight) * 0.5f;
602 const std::int64_t score = selectionCost(profile_, action, pose, approachNormal,
603 landing - pose.feet, std::fabs(height - heightCenter),
604 distance) +
605 (previousActionId_ == action.id ? action.repetitionPenalty : 0);
606 if (debugCapture_ == ClimbingDebugCapture::Enabled) {
607 const std::int64_t biasCost = static_cast<std::int64_t>(action.selectionBias) * 1000000ll;
608 const std::int64_t heightCost = quantizeMillimeters(height) * 1000ll;
609 const std::int64_t distanceCost = quantizeMillimeters(distance);
610 boundedDebugPush(lastEvidence_, ClimbingCandidateEvidence{action.id, "accepted", biasCost, heightCost,
611 distanceCost, score});
612 }
613 ClimbingCandidate candidate;
614 candidate.definitionGeneration = definitionGeneration_;
615 candidate.world = world.runtimeHandle();
616 candidate.obstacleBody = front.body;
617 candidate.obstacleShape = front.shape;
618 candidate.obstacleBodyId = front.bodyId;
619 candidate.obstacleShapeId = front.shapeId;
620 candidate.ignoredBodyId = pose.ignoredBodyId;
621 candidate.frontPoint = {front.x, front.y, front.z};
622 candidate.topPoint = {top.x, top.y, top.z};
623 candidate.landingFeet = landing;
624 candidate.surfaceNormal = {front.normalX, front.normalY, front.normalZ};
625 candidate.surfaceTangent = normalizedHorizontal({front.normalZ, 0.f, -front.normalX});
626 candidate.leftHandAnchor = candidate.topPoint - candidate.surfaceTangent * (action.handSpacing * 0.5f);
627 candidate.rightHandAnchor = candidate.topPoint + candidate.surfaceTangent * (action.handSpacing * 0.5f);
628 candidate.obstacleHeight = height;
629 candidate.obstacleDepth = measuredDepth;
630 candidate.gapDistance = distance;
631 candidate.clearanceHeight = profile_.capsuleHeight;
632 candidate.slopeRadians = topSlope;
633 candidate.curvature = measuredCurvature;
634 candidate.supportShapeTag = top.shapeTag;
635 candidate.supportMaterialId = top.materialId;
636 candidate.probeRecipe = action.probeRecipe;
637 candidate.score = score;
638 candidate.kind = action.kind;
639 candidate.support = support;
640 if (physics::Body3D* body = world.findBody(front.body)) {
641 auto localTop = body->worldToLocalPointOwned(top.x, top.y, top.z);
642 if (!localTop) return eve::Result<void>::failure(localTop.status());
643 auto localLanding = body->worldToLocalPointOwned(landing.x, landing.y, landing.z);
644 if (!localLanding) return eve::Result<void>::failure(localLanding.status());
645 candidate.bodyLocalTop = {localTop.value().x, localTop.value().y, localTop.value().z};
646 candidate.bodyLocalLanding = {localLanding.value().x, localLanding.value().y,
647 localLanding.value().z};
648 }
649 candidates.considerWithActionId(std::move(candidate), action.id);
650 }
651 return finalizeCandidates();
652}
653
655 if (phase_ != ClimbingPhase::Hanging || !execution_)
657 "drop requires a hanging execution", "runtime.phase",
658 {}, "climbing"));
659 if (tick <= execution_->lastTick)
661 "drop tick must be newer than the last execution tick",
662 "tick", {}, "climbing"));
663 auto eventCapacity = requireEventCapacity(1, tick);
664 if (!eventCapacity) return eventCapacity;
665 if (execution_->anchorGraph.isValid()) {
666 auto released = releaseAnchorReservation();
667 if (!released && released.error() && released.error()->code() != eve::DiagnosticCode::StaleHandle &&
668 released.error()->code() != eve::DiagnosticCode::NotFound)
669 return released;
670 }
671 enqueueEvent({ClimbingEventKind::Dropped, execution_->candidate.actionId, tick, execution_->executionId});
672 execution_->velocity = {0.f, -profile_.dropInitialSpeed, 0.f};
673 execution_->lastTick = tick;
674 execution_->candidate.support = HangSupport::None;
675 execution_->compactCollisionActive = false;
676 execution_->branchWindowOpen = false;
678 terminalCode_.clear();
680}
681
683 if (phase_ != ClimbingPhase::Hanging || !execution_)
685 "climb up requires a hanging execution",
686 "runtime.phase", {}, "climbing"));
687 if (tick <= execution_->lastTick)
690 "climb-up tick must be newer than the last execution tick", "tick", {}, "climbing"));
691 const auto found = std::find_if(profile_.actions.begin(), profile_.actions.end(), [&](const auto& action) {
692 return (action.kind == ClimbingActionKind::ClimbUp || action.kind == ClimbingActionKind::Mantle) &&
693 execution_->candidate.obstacleHeight + epsilon >= action.minHeight &&
694 execution_->candidate.obstacleHeight - epsilon <= action.maxHeight;
695 });
696 if (found == profile_.actions.end())
698 "no compatible climb-up action is registered",
699 "profile.actions", {}, "climbing"));
700 if (execution_->anchorGraph.isValid()) {
701 auto released = releaseAnchorReservation();
702 if (!released && released.error() && released.error()->code() != eve::DiagnosticCode::StaleHandle &&
703 released.error()->code() != eve::DiagnosticCode::NotFound)
704 return released;
705 }
706 execution_->candidate.actionId = found->id;
707 execution_->candidate.kind = ClimbingActionKind::ClimbUp;
708 execution_->candidate.definitionGeneration = definitionGeneration_;
709 execution_->definitionGeneration = definitionGeneration_;
710 execution_->action = *found;
711 execution_->startFeet = execution_->currentFeet;
712 execution_->candidate.landingFeet =
713 execution_->candidate.topPoint - execution_->candidate.surfaceNormal * found->landingForward;
714 execution_->elapsed = eve::Duration::zero();
715 execution_->duration = found->duration;
716 execution_->lastTick = tick;
717 execution_->accumulatedResidual = {};
718 execution_->leftContactEmitted = false;
719 execution_->rightContactEmitted = false;
720 execution_->landContactReleased = false;
721 execution_->compactCollisionActive = false;
722 execution_->branchWindowOpen = false;
724 terminalCode_.clear();
726}
727
729 if (!isActivePhase(phase_) || !execution_)
731 "no climbing execution is active", "runtime.phase", {},
732 "climbing"));
733 if (tick < execution_->lastTick)
735 "cancel tick must not precede the last execution tick",
736 "tick", {}, "climbing"));
737 const ClimbingActionDefinition& action = execution_->action;
738 const float t =
739 action.duration.nanoseconds() > 0
740 ? static_cast<float>(std::clamp(execution_->elapsed.seconds() / action.duration.seconds(), 0.0, 1.0))
741 : 0.f;
742 if (phase_ != ClimbingPhase::Hanging && phase_ != ClimbingPhase::Dropping &&
743 (t < action.cancelWindowStart || t > action.cancelWindowEnd))
746 "the active definition does not allow cancellation in this window", "action.cancelWindow", {}, "climbing"));
747 auto eventCapacity = requireEventCapacity(1, tick);
748 if (!eventCapacity) return eventCapacity;
749 if (execution_->anchorGraph.isValid()) {
750 auto released = releaseAnchorReservation();
751 if (!released && released.error() && released.error()->code() != eve::DiagnosticCode::StaleHandle &&
752 released.error()->code() != eve::DiagnosticCode::NotFound)
753 return released;
754 }
755 enqueueEvent({ClimbingEventKind::Cancelled, execution_->candidate.actionId, tick, execution_->executionId});
756 terminalCode_ = reason == ClimbingCancelReason::LinkStale ? "climbing.link.stale"
757 : reason == ClimbingCancelReason::AnchorStale ? "climbing.anchor.stale"
758 : reason == ClimbingCancelReason::MotionBlocked ? "climbing.motion.blocked"
759 : reason == ClimbingCancelReason::WarpBudgetExceeded ? "climbing.warp.budget_exceeded"
760 : "climbing.cancelled";
761 execution_.reset();
764}
765
768 snapshot.phase = phase_;
769 snapshot.candidates = lastCandidates_;
770 snapshot.queryCount = lastQueryCount_;
771 snapshot.terminalCode = terminalCode_;
772 snapshot.queries = lastDebugQueries_;
773 snapshot.evidence = lastEvidence_;
774 snapshot.motion = motionEvidence_;
775 if (execution_) {
776 snapshot.accumulatedWarpResidual = execution_->accumulatedResidual;
777 snapshot.executionId = execution_->executionId;
778 }
779 snapshot.broadPhaseQueryCount = lastCounters_.broadPhaseQueryCount;
780 snapshot.broadPhaseHitCount = lastCounters_.broadPhaseHitCount;
781 return snapshot;
782}
783
785 debugCapture_ = capture;
786 if (capture == ClimbingDebugCapture::Disabled) {
787 lastCandidates_.clear();
788 lastDebugQueries_.clear();
789 lastEvidence_.clear();
790 motionEvidence_.clear();
791 }
792}
793
795 std::uint32_t moverIterations,
796 std::uint64_t elapsedNanoseconds) noexcept {
797 lastQueryCount_ = queryCount;
798 lastCounters_ = {};
799 lastCounters_.workload = ClimbingWorkload::Ordinary;
800 lastCounters_.queryBudget = ClimbingQueryBudgets::Ordinary;
801 lastCounters_.queryCount = queryCount;
802 lastCounters_.moverIterations = moverIterations;
803 if (queryCount > lastCounters_.queryBudget)
804 lastCounters_.budgetState = ClimbingQueryBudgetState::Exceeded;
805 telemetry_.record({tick, elapsedNanoseconds, lastCounters_});
806}
807
808} // namespace eve::climbing
SQInteger top
double score
Definition Agent.cpp:50
EVEngine assertion entry point, backed by zeroerr.
#define EV_ASSERT(cond,...)
Assert an internal engine invariant (state that must always hold).
Definition Assert.h:37
std::string output
eve::EntitySpatialPose pose
Vec3 tangent
Definition CaveMesh.cpp:80
Versioned, lossless codecs for climbing definitions.
Deterministic climbing/parkour planning and capsule-constrained execution.
DiagnosticCode code
ShaderImageInput shape
glm::vec4 clip
std::uint32_t height
size_t offset
bool valid
float distance
Texture * normal
World3D * world
std::string action
Definition PlayHost.cpp:117
bool hit
float t
bool found
int reused
std::string filter
std::uint32_t count
Battle::Events events
SimulationTick tick
int limit
Definition TreeMesh.cpp:164
int sides
Definition TreeMesh.cpp:322
std::string body
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Definition AnimClip.h:145
Fixed-capacity owning candidate set used by the production probe hot path.
Definition Climbing.h:329
void considerWithActionId(ClimbingCandidate candidate, std::string_view actionId)
Consider a candidate while reusing slot-owned action-id storage on the production probe path.
Definition Climbing.cpp:29
void consider(ClimbingCandidate candidate)
Consider one candidate and retain it only when it belongs to the best fixed-capacity set.
Definition Climbing.cpp:23
const ClimbingCandidate * end() const noexcept
Returns the sentinel following the last candidate for synchronous read-only iteration.
Definition Climbing.h:366
void swap(ClimbingCandidateSet &other) noexcept
Exchange complete owning slot storage without allocating.
Definition Climbing.cpp:60
void sortAndLimit(std::size_t limit)
Sort by canonical deterministic selection key and clamp to a validated limit.
Definition Climbing.cpp:55
const ClimbingCandidate * begin() const noexcept
Returns the first candidate for synchronous read-only iteration.
Definition Climbing.h:360
const ClimbingCandidate & front() const noexcept
Front.
Definition Climbing.h:351
bool empty() const noexcept
Empty.
Definition Climbing.h:347
static constexpr std::size_t Capacity
Definition Climbing.h:331
void clear() noexcept
Remove all candidates while retaining each slot's owned string capacity.
Definition Climbing.h:334
ClimbingDebugSnapshot inspect() const
Captures the latest bounded owning diagnostics without querying Physics.
Definition Climbing.cpp:766
eve::Result< void > setProfile(ClimbingProfile profile)
Replaces the runtime profile after validating all dimensions and action definitions.
Definition Climbing.cpp:93
eve::Result< void > drop(eve::SimulationTick tick)
Enters an explicit physics-constrained drop from a hanging pose.
Definition Climbing.cpp:654
void recordOrdinaryTick(eve::SimulationTick tick, std::uint32_t queryCount=0, std::uint32_t moverIterations=0, std::uint64_t elapsedNanoseconds=0) noexcept
Record one ordinary locomotion tick and its bounded Physics mover work.
Definition Climbing.cpp:794
ClimbingLocomotionPolicy locomotionPolicy() const noexcept
Return an owning ordinary-locomotion policy snapshot safe across later profile reloads.
Definition Climbing.cpp:97
eve::Result< eve::Value > snapshot() const
Captures deterministic runtime state; debug candidates and final bone poses are excluded.
eve::Result< std::vector< ClimbingEvent > > drainEvents()
Atomically takes all events queued by simulation operations in stable production order.
Definition Climbing.cpp:86
eve::Result< ClimbingCandidateSet > probe(physics::World3D &world, const ClimbingPose &pose) const
Probes, validates, and deterministically sorts all matching candidates.
Definition Climbing.cpp:166
eve::Result< void > probeInto(physics::World3D &world, const ClimbingPose &pose, ClimbingCandidateSet &output, eve::SimulationTick tick=eve::SimulationTick::zero()) const
Probe into caller-retained fixed storage; successful calls atomically replace output.
Definition Climbing.cpp:174
eve::Result< void > validateAnimationBinding(std::string_view actionId, const animation::AnimClip &clip)
Validates and records an action's clip notify contract without retaining the clip.
Definition Climbing.cpp:145
eve::Result< void > cancel(ClimbingCancelReason reason, eve::SimulationTick tick)
Cancels an active execution with an injected tick and stable, strongly typed reason.
Definition Climbing.cpp:728
eve::Result< void > climbUp(eve::SimulationTick tick)
Branches from hanging into the best compatible mantle/climb-up definition.
Definition Climbing.cpp:682
eve::Result< void > upsertAction(ClimbingActionDefinition action)
Adds or replaces one action definition by stable id.
Definition Climbing.cpp:126
static constexpr std::size_t PendingEventCapacity
Hard bound for undelivered post-simulation events owned by one runtime.
Definition Climbing.h:660
void setDebugCapture(ClimbingDebugCapture capture) noexcept
Enables or disables bounded diagnostic capture; disabling immediately releases captured data.
Definition Climbing.cpp:784
3D rigid body (Box3D) in meter-space coordinates (+Y up by convention). Owned by a World3D; create pr...
Definition Body3D.h:24
Box3D rigid-body world. Script coordinates are meters (Box3D native), unlike 2D World which uses pixe...
Definition World3D.h:63
bool isActionEnabledForPose(const ClimbingProfile &profile, const ClimbingActionDefinition &action, const ClimbingPose &pose)
True when action enabled for pose.
std::int64_t selectionCost(const ClimbingProfile &profile, const ClimbingActionDefinition &action, const ClimbingPose &pose, Vec3 targetDirection, Vec3 targetDelta, float heightError, float distance, std::int64_t stableTieBreak=0)
Selection cost.
bool isCandidateLess(const ClimbingCandidate &lhs, const ClimbingCandidate &rhs)
True when candidate less.
bool isRuntimeProbeKind(ClimbingActionKind kind)
True when runtime probe kind.
const ClimbingActionDefinition * findAction(const ClimbingProfile &profile, std::string_view id)
Optional action. @borrowed From profile; lifetime ends when the caller-owned profile changes or is de...
std::int64_t quantizeMillimeters(float value)
Quantize millimeters.
Vec3 normalizedHorizontal(Vec3 value)
Normalized horizontal.
bool isProbeRecipeMatchingKind(const ClimbingActionDefinition &action)
True when probe recipe matching kind.
float lengthSquared(Vec3 value)
Length squared.
bool isSupportSelectorMatch(const ClimbingActionDefinition &action, int shapeTag, int materialId)
True when support selector match.
bool isFinite(float value)
True when finite.
void boundedDebugPush(std::vector< T > &values, T value)
Bounded debug push.
bool isObstacleProbeKind(ClimbingActionKind kind)
True when obstacle probe kind.
bool isActivePhase(ClimbingPhase phase)
True when active phase.
ClimbingCancelReason
Stable, strongly typed terminal cancellation reason.
Definition Climbing.h:140
ClimbingDebugCapture
Runtime policy controlling optional owning diagnostic capture.
Definition Climbing.h:600
eve::Result< void > validateClimbingActionDefinition(const ClimbingActionDefinition &action)
Validate every known field of one action definition.
HangSupport
Evidence describing whether a hanging pose has foot support.
Definition Climbing.h:137
eve::Result< void > validateClimbingProfileDefinition(const ClimbingProfileDefinition &profile)
Validate every known field and nested action of one profile definition.
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
Definition of one geometry-compatible climbing action.
Definition Climbing.h:160
Quantized selection cost and hard-decision evidence for one action definition.
Definition Climbing.h:613
Owning, replayable climbing candidate; contains no borrowed query pointers.
Definition Climbing.h:290
ClimbingProbeRecipe probeRecipe
Definition Climbing.h:317
std::uint64_t definitionGeneration
Definition generation used to produce this candidate.
Definition Climbing.h:293
physics::PhysicsWorldHandle world
Definition Climbing.h:294
physics::PhysicsBodyHandle obstacleBody
Definition Climbing.h:295
physics::PhysicsShapeHandle obstacleShape
Definition Climbing.h:296
Geometry-query evidence suitable for an engine debug-draw adapter.
Definition Climbing.h:603
Bounded owning debug snapshot, independent of Physics query caches.
Definition Climbing.h:633
Small owning projection of profile fields consumed by ordinary ground/air locomotion.
Definition Climbing.h:261
Input pose used for one deterministic candidate probe.
Definition Climbing.h:275
Runtime profile controlling capsule geometry and probe limits.
Definition Climbing.h:218
physics::QueryFilter3D queryFilter
Definition Climbing.h:254
std::vector< ClimbingActionDefinition > actions
Definition Climbing.h:255
static constexpr std::uint32_t CandidateProbe
static constexpr std::uint32_t Ordinary
Small owning world-space vector used at climbing module boundaries.
One generation-qualified shape returned by an owning broad-phase query.
Query filter applied atomically for one owning query operation.
Owning snapshot of the closest ray intersection.
PhysicsShapeHandle shape
PhysicsBodyHandle body