MOVEMENT.md — how characters move, and why¶
kke::Locomotion (engine/include/kke/Locomotion.h) is the movement layer
between input actions and RigidWorld's character controller. Its rules
come from PointDown's controller, "mechanics museum" and parkour videos
(https://www.youtube.com/@PointDown). They're Godot videos, but the
principles are engine-independent. This file keeps the notes and says
where each principle lives in the code.
Tested in tests/test_locomotion.cpp (12 tests, including the whole
kke_demo parkour lane at 60 and at 15 fps). You can play it in kke_demo:
walk to the lane at x = 20, or run KKE_DEMO_AUTOPILOT=1 ./kke_demo to watch
it run the lane by itself. The Synty scenes (SCENES.md) are real-art trails:
SceneTrails.* runs them headless when the packs are present.
The frame¶
PointDown's controller core (Advanced controller core, E3BxMgmP4m0) splits a character into input → model → presentation:
- Input:
InputModule/InputMapgather actions (move,sprint,walk,crouch,jump). The game never reads keys. - Area awareness runs next, before any state logic, and is the only
code that asks the world questions (
Locomotion::probe). - Translation, per state: the current state turns actions into a move.
jumpon the ground becomes a vault if a thin, hip-high obstacle is ahead, a climb if there's a top with room, and a jump otherwise (Designing AAA parkour system, veOpS2S8Lco: "input actions are not move names"). - Update: the state moves the capsule.
- Presentation: the Animator picks poses from the state. Animations are pose providers: they never decide where the capsule goes (Root motion once and for all, _PHRm2EqfX0).
Area awareness: dynamic, no markup¶
PointDown compares static awareness (trigger volumes placed by a designer)
with dynamic awareness (ray casts that read the level). Dynamic costs more
code but lets a level designer drop in any mesh (Designing AAA parkour
system; Ledge actions, IMSFMmekFxg). We use dynamic awareness because
the Synty levels have hundreds of props nobody will mark up. probe()
costs about 15–30 ray casts and 2–4 capsule tests, and runs only on the
frame "go up" is pressed, or every frame while in the air.
- Face: rays forward at knee, hip and chest height, from the middle and both shoulders (a centre ray slips through the seam between two fence panels). Anything lower is a stair the controller steps up by itself.
- Top: one ray down, just past the face, from above the highest top this sensor can reach, and a second one further in when the rim is bevelled (rocks). A ray that starts inside geometry means the obstacle is too tall.
- Depth: rays down across the top until it drops away. Thin is a fence (vault); a platform is a climb.
- Room: capsule tests for a tucked body over the top, and for a standing body at the landing or on the top.
- Sensors per state (PointDown's "ray slice" resources): walking looks 0.7 m ahead and reaches 1.9 m, sprinting looks 1.6 m ahead and reaches 2.2 m, and the air sensor reaches 2.1 m. A 2.1 m ledge is only climbable from a sprint.
Floor: three tiers, not two¶
From Designing AAA parkour system: on the floor; up to 25 cm above the floor, which still counts as walking (snap down, no fall animation on a kerb or a stair edge going down); and in the air. That removes the fall/land stutter on small drops. Coyote time (0.12 s) and a jump buffer (0.15 s, PointDown's "queued input") come from the same idea: don't make the player hit a one-frame window.
Drawing it: between physics steps¶
The capsule moves at the physics rate (60 Hz). Drawn at its raw position
on a faster screen it stands still one frame and jumps the next, and a
camera that follows it smoothly (the spring arm's lag) shows that as a
shaking body, worst while the camera turns. Games draw the character, and
point the camera, at RigidWorld::characterDrawPosition(id,
app.fixedAlpha()): the feet between the last two physics steps. Gameplay
(Locomotion, hits, the network) keeps using characterPosition. The
showcase, the starter game, the cookbook, Climb Race, the party game,
tennis, the duel, the goblin horde and the pet do this.
Turning (Smoother turn movement, MM1, ysKxT3q4tA8)¶
- Speed and direction are separate. Input rotates the direction at a limited rate (600°/s running, 320°/s sprinting) instead of replacing the velocity. The head doesn't jump 40 cm in one frame when you mash A/D.
- Sharp turns slow you down to 60% while the turn lasts ("creatures that value their lives drop speed in turns").
- A 180° reversal keeps its side. Near 180° the sign of the angle flips on float noise, and the turn would change sides every frame; the last turn direction wins inside ±12° of 180°.
- The animation follows the measured speed: the blend space gets how far the feet actually moved, not the requested speed. Legs slow down in turns and stop against walls. It is measured over the time physics simulated since the last frame (physics steps at 60 Hz, frames don't), so a frame between two physics steps doesn't read as standing still (BUG-054).
Jumping and the air (Jump & Fall control, MM2, vPFAh2T8Ipg)¶
- Air control is a small acceleration (5 m/s²), added to the flight and capped at the take-off speed (at least 1.6 m/s for standing jumps). It corrects a jump; it doesn't fly the character.
- Facing is stored separately from velocity. When a wall deflects the flight, the body still faces where you meant to go.
- Take-off is a glue window. The last grounded frame may re-aim the run at the input, so a jump straight after a turn goes where you asked.
- A jump always happens. The press is kept until a physics step takes
it (
RigidWorld::CharacterInput::jump): on a screen faster than the 60 Hz physics most frames have no step, and the next frame's input used to wipe the jump while the jump animation still played. A coyote jump (or one from the kerb tier) jumps even when the capsule isn't touching the ground that step (jumpInAir): Locomotion has already decided. - No bunny hopping. Jumping again within 0.3 s of landing a jump goes
up at 82% of the last one's speed (67% of the height), down to 60% of a
full jump (
repeatJumpWindow,repeatJumpFactor,repeatJumpMin); a moment on the ground gives a full jump again. Mashing the button can't keep the character floating, and the jump animation plays only when a jump really starts (Locomotion::jumped()). Tests:Locomotion.JumpSurvivesFramesWithoutAPhysicsStep,CoyoteJumpGoesUp,RepeatJumpsGoLessHigh. - Ledge grab in the air: rising slowly or falling, steering into a wall whose top is within reach turns into a climb when "go up" is held, or when the top is less than 1.5 m above the feet. Otherwise it hangs.
Ledge hang and shimmy (Ledge actions, IMSFMmekFxg)¶
- Grab: in the air, steering into a wall whose top is 1.5-2.35 m above the feet (and "go up" not held) hangs from it. That includes edges the probe can't stand on, like a thin wall's top. The capsule is pulled in over 0.15 s to 2.05 m below the top, square to the wall.
- Shimmy: the sideways part of the input moves along the edge at 1.1 m/s. Every step re-finds the wall (at chest height) and the top (just in from the face). Where either is gone, the top steps by more than 15 cm, or the capsule wouldn't fit, the character stops: it never shimmies off the end or through a side wall. The wall normal is re-read each step, so gently curved walls work.
- Corners: where the edge ends, an outside corner (the wall turns away) or an inside corner (a wall ahead) with a top at the same height is followed: the character moves around it over 0.3 s and hangs from the new face. The held input keeps going the same way along the ledge, even though it no longer points along the new wall. A top that steps up or down is a real end.
- Jump back: "go up" while pushing away from the wall jumps off it (3.5 m/s out, 80% of a jump up), facing away, with no re-grab for 0.4 s.
- Climb up: "go up" runs the normal checked climb from the hang (so a thin wall with no room on top can be hung from but not climbed).
- Let go: crouch. A small push off the wall, no coyote jump, and no re-grab for 0.4 s.
- Animation: no hang clip in the UAL sets, so kke_demo uses the fall
pose slowed down with hand IK on the edge; a pack with
Hang_Idleis picked up by name.KKE_DEMO_HANG=1 ./kke_demoruns a jump, hang, shimmy around the wall's end and a jump back at the lane's 3 m wall by itself. - Tests:
Locomotion.JumpAtAHighWallHangsFromTheTop,ShimmyAlongTheEdgeStopsWhereItEnds,ClimbUpFromAHang,CrouchLetsGoAndDoesNotRegrab,HangsFromAThinWallItCannotStandOn,ShimmyAlongTheEdgeAndAroundTheCorner,ShimmyStopsWhereTheTopStepsUp,ShimmyIntoAnInsideCorner,JumpBackOffAHang.
Ledge leaps (Ledge actions, IMSFMmekFxg)¶
- Sideways: hanging, "go up" with the stick pushed along the wall leaps to the next edge that way. The search walks along the wall past the end of the current edge (the part shimmying covers is skipped) and takes the nearest edge within 2.2 m that is up to 1.3 m higher or 1 m lower. It lands a body width in from that edge's end, so the hands aren't on a corner.
- Up: with the stick into the wall (or no stick), "go up" climbs if there is room on top. If there isn't (a thin wall, a sill), it leaps up to an edge above on the same wall, up to 1.3 m higher.
- The target is found and checked before the leap, including room for the body there and at the middle of the arc. The body moves kinematically for 0.45 s: evenly along the ground, on a parabola in height that peaks about 0.3 m above the higher end (PointDown's ledge-leap parabola). It hangs on arrival. Hand IK reaches for the new edge in the last 40%.
- No target (nothing in reach, no room) means no leap. A sideways "go up" then climbs if it can, as before.
- Tests:
Locomotion.LeapsSidewaysAcrossAGapToAHigherEdge,LeapsUpToAnEdgeAbove.
Wall climb (run up a wall)¶
- Start: on the ground, running into a wall at 2.5 m/s or more, "go up" with the wall at most 1.2 m past the capsule, square to it (within about 45 degrees), and its top too high to climb straight (above the sprint sensor's 2.2 m) but no higher than 4.3 m above the feet. There must be a hangable edge at the top and room for the body all the way up.
- The move: a Leap (
wallClimbing()is true) that goes to the wall's foot in the first third and rises the whole way, slowing as it goes (the run-up's momentum running out), 0.6 s to the hang position. It hangs on arrival, and "go up" climbs on from there. - Animation in kke_demo: the start of UAL2's
ClimbUp_2m(the spring and the reach), with foot IK planting two steps on the wall, left then right, each held where it first touched while the body rises past it. - Standing still, "go up" at the same wall is a plain jump.
- Tests:
Locomotion.RunningAtATallWallRunsUpItAndHangs,StandingAtATallWallIsNoWallClimb.
Hang vault (over a thin wall from a hang)¶
- Hanging from a wall too thin to stand on (the probe finds no room on
top), "go up" looks across the top for where it drops away (at most
1.1 m), for a floor on the far side (at most 4 m below the top), and
for room for the tucked body over the top and the standing body at the
landing. Then it vaults over: a Vault (
hangVaulting()is true). - Path: pull up at the wall until the feet are level with the top (the first 45% of 1 s), then across with the feet never below the top until past its far side, then down to the floor, falling faster as it goes. It ends on the ground with a little of the run (40% of run speed).
- An edge above to leap up to comes first (the trick course's beam over its thin wall); the hang vault is for a thin wall with nothing above.
- Animation in kke_demo:
ClimbUp_2m's pull-up, then the second half ofSafetyVault; hand IK keeps the hands on the top until the body is over. - Test:
Locomotion.VaultsOverAThinWallFromAHang.
Wall run¶
- Start: in the air, going at least 4 m/s, still holding the run, with a vertical wall at the hips and head within 0.45 m of the capsule's side, and running along it rather than into it (the wall is within 60 degrees of parallel). At least 0.35 m off the ground, so a wall beside a kerb doesn't count.
- On the wall: the body moves kinematically along the wall at the entry speed, with the wall's normal re-read each step (gently curved walls work). Height follows a lighter gravity (7 m/s² instead of 9.8), starting with 70% of the take-off's vertical speed, so a run arcs up about a metre and comes down. It lasts at most 1.1 s.
- Ends: "go up" is a wall jump (4.5 m/s off the wall, 85% of a jump up, 80% of the run kept). Letting go of the stick or crouching drops off. Where the wall ends, the runner flies on with the momentum. Something in the way stops the run. Landing on the floor keeps the run going on the ground. The same wall can't catch you again for 0.35 s, but another wall can, so wall-to-wall chains work.
- Animation: UAL2's
WallRun_L_Loop/WallRun_R_Loopby wall side (Locomotion::wallRunSide()). The camera's shoulder moves to the open side during the run, because over the wall shoulder the spring arm would pull in behind the head. - Tests:
Locomotion.JumpingAlongAWallRunsOnIt,WallJumpKicksOffTheWall,SlowJumpNextToAWallIsNoWallRun. - kke_demo: the trick course at x = 26 has a 12 m wall to run along, a
row of thin pillars with 1 m gaps and rising tops for leaps, and a thin
wall under a beam for the leap up.
KKE_DEMO_TRICKS=1 ./kke_demoplays through all of them.
Vault and climb (Parkour ep3, rhzwhJPb-jQ; Ledge actions)¶
- The capsule becomes kinematic for the move (
RigidWorld:: setCharacterKinematic). It follows a path that was checked for room before the move started, so there's no fight with the solver halfway over. - Correction window (first 0.2 s): the body turns square to the obstacle, whatever angle you came in at.
- Vault path: PointDown's ledge-leap parabola (
leapParabola). It is solved through the start and the landing, with a peak that clears both edges of the top. Horizontal speed stays constant, so a speed vault keeps its momentum (90% of the entry speed). - Climb path: up the wall to the edge (hands on top), then over it onto the top.
- At the end the momentum goes back to the controller, and the character is on the floor that the probe found (no "stand on thin air" snap).
Free climbing (kke::ClimbWall, kke::Climber; games/climb_race)¶
Ledge hang is for edges you run into; free climbing is a whole rock face where you choose every hold.
- The wall (
ClimbWall::generate(seed)) is a height field over x, y: bands of lean (an easy start, a forced overhang in the middle, a slab at the top), fbm relief and buttresses. Ledges are boxes sticking out of the rock. Holds (jug, crimp, sloper, edge) are placed along a drawn line that is always climbable, then filled in around it. Same seed, same mountain. - Reach is
ClimbWall::reachDistance: sideways and up count fully, in and out of the rock a quarter. Routes, the climber and the bot all use it. - The climber (
kke::Climber) is pure logic, with no physics. The capsule is kinematic atfeet()while climbing and goes back tokke::Locomotionon a fall or at the top. The triggers move the hands (1.55 m reach), the bumpers step the feet onto footholds, and jump held then let go lunges the body up to 1.35 m (grab in time or fall). The body hangs under the hands and stays out of the rock. Stamina is the one resource: it comes back only with both feet on, and the arms alone cost double. - The body is IK only: the hang clip slowed down, with two-bone IK
putting each hand and foot exactly where
Climbersays. - Tests:
tests/test_climb_wall.cppcovers 25 seeds climbable, reaches, lunges, feet, loose holds, stamina, no clipping and the bot to the top.
Animations¶
The Universal Animation Library "Standard" set in assets/animations/ has
no vault or climb clips (its 43 clips are locomotion, jump, crouch,
combat, sitting, swimming and interaction). Volume 2 (UAL2.fbx, CC0,
same skeleton, git-ignored like the first) has them: put it next to
UAL1_Standard.fbx and kke_demo plays
SafetyVaultfor vaults (one hand on the top, legs swung to the side),ClimbUp_1mfor climbs up walls under 1.6 m,ClimbUp_2mabove that.
Those clips are made in place: the pelvis rises up to 0.8 m and comes back
down at the end. Locomotion already moves the capsule up and over, so
AnimationSet::removeLift takes the vertical travel out of them (the
forward and sideways body motion stays). And they aren't played by the
clock: Animator::setProgress poses them at Locomotion's progress through
the move, so a vault that takes 0.4 s or 0.8 s (it depends on the entry
speed) still puts the hand down on the edge. Other players see the same
clips: the network state carries the obstacle height during the move.
Without volume 2 the stand-ins are used: the tucked jump pose for the vault, the take-off reach and a crouch step for the climb. There is no hang or shimmy clip in either volume (the hang uses the slowed fall pose). kke_demo makes the shimmy with IK: hand over hand along the edge, each hand planted while the body moves past it (60% of a 0.36 m stride), then lifted a few centimetres and moved on, the leading hand first; the feet step along the wall below in between.
After the Animator, two small modifiers run in a fixed order
(engine/include/kke/AnimRig.h, PointDown's SkeletonModifier3D idea):
- Feet on the ground. Each foot keeps its animated lift above the ground under it, the hips drop when one foot has to go lower than the capsule's floor (stairs, slopes, rock), and analytic two-bone IK bends the legs. Each foot also tilts to lie along the ground under it (the ray's normal, at most 30 degrees), keeping the animated foot angle on top of that. Smoothed over frames, and off in the air.
- Hands on the edge. During the first part of a vault or climb, the hands go to the top edge the probe found, shoulder-width apart, with the elbows bending out and back. It stands in for the hand plant the stand-in clips don't have.
Any Synty character can wear the UAL clips. Bones pair up by name
(UAL and Synty both follow the Unreal mannequin, give or take case and
Synty's indexFinger/finger), and each paired bone copies the source's
rotation change from its rest pose, turned by the difference in facing
(UAL faces -Z, Synty +Z); the pelvis travel scales with leg length. The
match logs which bones stayed at rest (Synty's eyes, eyebrows and toes).
Importing animated 3D characters (a0_JVEY7sbY) puts it as a contract: a
clip only means something for the skeleton it was made for, so a
mismatch should be visible, not silent. Try it in kke_demo's Character
panel, or KKE_CHARACTER=SK_Character_Father_01 ./kke_demo.
2D blend spaces (ENG-G19) blend clips placed over two parameters,
usually the velocity in the character's own frame (x right, y forward,
m/s): Animator::addBlendState2D with a BlendSpace2D, then
setParameter2 every frame. Every clip gets a weight by gradient band
interpolation (Johansen 2009, the method behind Unity's freeform blend
trees) instead of a triangulated grid (Unreal), so any layout works and
the weights change smoothly; in the default polar mode direction is an
angle and speed a length, so a full-speed diagonal blends forward and
right only, never idle. All weighted clips run at one shared phase, so
feet don't slide. goblin_horde's heroes strafe with one: walking round a
circle the weights change by at most 0.006 per half degree, where the
old left/right/back thresholds jumped from one clip to the next; it
costs 3.6 µs an update for a 65-bone hero against 2.3 µs for a 1D space.
Root motion is available as data: AnimationSet::extractRootMotion
moves a bone's horizontal travel out of the clips into a track, the clip
plays in place, and Animator::rootMotion() reports the travel each
update for the game to apply. Locomotion clips don't use it (the
controller moves the capsule, and the blend space follows the measured
speed); it's for authored moves such as a real vault clip.
Animation events (ENG-F28, Unity's animation events, Unreal's
notifies): a name at a point in a clip, reported on the update that
passes it, for footstep sounds, the frame a sword hits or a ball leaves
the hand. Animator::addEvent(state, fraction, name) places one at a
0..1 fraction of a state's cycle, so it also works on blend spaces (a
step at 0.25 and 0.75 lands with the feet at any speed);
Animator::events() lists what the last update() passed, current state
only (one fading out says nothing), once per pass (a loop every cycle),
events at 0 on the first update after play(). For plain clips on an
instance, ModelModule::addClipEvent(model, clip, seconds, name) and
clipEvents() do the same in seconds; Lua has them as models.event
and the AnimEvent hook (SCRIPTING.md). The window maths
(kke/AnimEvents.h) is shared and unit-tested.
local hero = models.load("Hero")
models.event(hero, "Walk_Loop", 0.3, "step")
models.event(hero, "Walk_Loop", 0.8, "step")
hook.Add("AnimEvent", "hero.steps", function(e)
if e.name == "step" then audio.impact(heroPos, "Stone", 0.3) end
end)
Retargeting between rest poses¶
kke::retargetAnimations and kke::PoseRetarget copy each bone's turn
away from its rest pose. Two skeletons modelled in different rest poses
(an A-pose character playing T-pose clips) would keep that difference in
every frame, so rest matching (on by default, ENG-G19) first turns each
arm and leg bone of the target to point the way the source's points at
rest. Hands turn with the forearm, feet stay flat on the floor and the
spine keeps its curve: those differences are the body's shape, not its
pose. Pass matchRest = false to keep the old copy.
Measured with UAL's walk on one character from each of 26 installed
Synty packs (AnimRig.RestMatchingOnInstalledCharacters): the worst limb
pointed 5 to 6.2 degrees away from the clip without matching (the
werewolf's arms rest 30 degrees off UAL's), 0.0 with it, and no foot
moved. PoseRetarget::restDifferenceDegrees() says how much was matched.
Not done yet (next)¶
- Animation layering (upper body over locomotion; Animation layering pipelines, Fsa2wxyQvzM, blocked below).
Transcripts¶
Fetched 2026-09-26 as auto-captions. YouTube blocks this server after about a dozen requests, so these videos didn't come through: God Tier 3D Character Controller (qIf5YQ8qJng), Detect climbable ledges (yxWxHfjNpa4), Animation layering pipelines (Fsa2wxyQvzM), Post-start melee attack redirection (WGZ-QG-0cpw), Use professional AAA practices (FgO5edghqRE). Importing animated 3D characters (a0_JVEY7sbY) came through on a retry. Their principles overlap with the videos above; retrying from another network would complete the set.