[Feature] Motion matching - #12
Merged
Merged
Conversation
added 3 commits
July 25, 2026 23:27
Animates characters with no animation state machine: gameplay states a goal (desired velocity/facing, typically the steering output) and the controller periodically searches a motion database for the frame that best matches the current pose and the predicted future trajectory, then jumps there through an inertialized transition. Root motion moves the entity; clips are the vocabulary, the search is the state machine. - MotionDatabase: clips resampled at a fixed rate (default 30 Hz) into 27-dim character-space feature vectors — foot positions/velocities, hip velocity, future root trajectory positions and facings at 0.33/0.66/1.0 s (loop-wrapped with the clip's per-loop root displacement). Per-dimension normalization, per-group weights. Built lazily at runtime from the entity's loaded clips; a persisted format is deferred until Learned Motion Matching needs it. - Brute-force nearest-neighbour search with early-out, seeded with the currently playing frame's cost discounted 10% — hysteresis so only meaningfully better frames cause a jump. Winners near the natural playback position are skipped entirely. - Query velocities are world-space finite differences rotated into the character frame, matching how the database measures them from clip root motion (a character-relative difference would zero out travel and bias every query toward idle). - Trajectory prediction: first-order lag of the simulated velocity toward the goal (predictionHalflife), integrated analytically. - beginAnimationTransition generalized to arbitrary target times so jumps land mid-clip through the standard transition path, with root motion re-baselined. - Public API: setMotionMatching(entityId:descriptor:), setMotionMatchingEnabled, isMotionMatchingEnabled, setMotionMatchingGoal(entityId:desiredVelocity:desiredFacing:). Docs: docs/API/UsingMotionMatching.md
Hierarchical assets loaded via setEntityMeshAsync carry their AnimationComponent on a skinned scenegraph child while the game holds and steers the asset root. Root motion applied its transform deltas to the component's entity, and motion matching read heading and world position from it — so the child drifted inside the asset along its never-rotated local frame while the root the game turned stayed put: characters marched off in a fixed world direction ignoring their goal, and the goal-to-character-space conversion churned the clip selection. Both systems now anchor to the entity the public API was called on (setRootMotionEnabled / setMotionMatching), which is the gameplay handle by the engine's own hierarchical-resolution convention. Flat entities behave exactly as before (anchor == component entity). Regression test drives a parent root with the components on a child.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
M3 of the animation roadmap (
docs/Architecture/animationPoseLayer.md): characters animate with no animation state machine. Gameplay states a goal — desired velocity and facing, typically straight fromSteeringSystem— and every search interval (default 0.1 s) the controller finds the database frame best matching the current pose and the predicted future trajectory, then jumps there through the existing inertialized-transition machinery. Root motion moves the entity: clips are the vocabulary, the search is the state machine.Design points
MotionDatabase— clips resampled at 30 Hz into 27-dim character-space feature vectors: foot positions/velocities, hip velocity, and future root trajectory positions + facings at 0.33/0.66/1.0 s, loop-wrapped with the clip's per-loop root displacement so trajectories never snap at clip ends. Per-dimension std-dev normalization, per-group weights (MotionMatchingWeights). Built lazily at runtime from the entity's loaded clips — no new file format or exporter changes; a persisted database is deferred until Learned Motion Matching needs one.predictionHalflife), integrated in closed form; facing eases toward the desired facing (or the velocity direction).beginAnimationTransitiongeneralized to arbitrary target times, so jumps land mid-clip through the standard transition path with root motion re-baselined.Stacked on #10 (
feature/animation_foot_ik) — merge #10 into develop first via its own PR page, then this retargets/merges cleanly.Testing
AnimationMotionMatchingTests(10 tests, all passing):All six animation suites (sampler, policy, inertialization, root motion, foot IK, motion matching) pass together.