Skip to content

Animation

Every player pawn that wears a humanoid .dh-avatar is animated by the engine’s built-in locomotion. It uses no clips. A pose is made every frame from what the pawn is doing:

MotionWhat drives it
IdleTime: a slow breath in the chest and a weight shift in the hips.
Walk and runVelocity against the body’s facing (the 2D blend) and speed (idle → walk → run).
CrouchThe eye height. The hips sink and sit back over the planted feet, the legs bend to reach them, and the trunk leans forward. It layers over everything else, so a crouched pawn walks crouched.
JumpVertical speed. The feet tuck at take-off, stay tucked through the rise, reach down while falling, and the hips absorb the landing in proportion to the impact. A crouch held in the air pulls the legs up instead of sinking the hips, as a Source crouch-jump does, and no foot target is ever allowed above, or closer than a minimum fold to, its hip. In a sustained fall the arms flail: a windmill in antiphase on a clock of its own, with the shoulders lifting, the elbows working and the feet kicking a little, easing in after a short delay so an ordinary jump never reaches it.
Noclip floatTime and flight speed. The legs drift slowly and out of step, as if treading water, the arms drift, and the body leans into the flight.

States cross-fade. Nothing pops between standing, moving, jumping and flying.

A foot is planted, not swung. The stride phase advances by the distance the body covers divided by the stride length, on the pawn’s stride clock (below). A planted foot is carried back under the body by exactly what the body did that frame, so it holds its place in the world. A swinging foot eases to the next plant, half a stance ahead along the direction of travel. The leg reaches its foot with a two-bone solve, and the hips drop just enough that no leg is pulled straight. Stepping sideways spreads the feet so they pass without crossing. A pawn that stops finishes only the step already in the air: that foot lands within client.anim.locomotionSettleSeconds, at its rest rather than half a stance ahead, and a planted foot is never lifted to start another. A planted foot never slides: one left farther than client.anim.locomotionSettleTolerance from its rest takes a short settle step home instead (locomotionSettleStepSeconds, lifted locomotionSettleStepLift), after the foot in the air has landed and one foot at a time, so the feet are never both in the air. The step plants once, so it plays one footstep through the same path (a pawn with no legs to draw takes none). The toe-off and heel-strike roll scales with the gait, so a foot at idle stays level.

The stride lives in a GaitClock, not in the pose. It owns the phase, the cadence and stride it advances by, how a start sets off and a stop settles, and the moment each foot lifts and plants. The pose reads it and draws the feet it says; it keeps no phase of its own. The footsteps play on the same plants (see Audio), so the foot you see land and the step you hear are one event.

Every pawn has a clock, whatever it wears. A humanoid avatar, an avatar with no legs to animate, the hull box and the stand-in shown while an avatar loads all tick the same clock at the same cadence for the same speed, so all of them are heard walking. A legged rig adds one thing, and it does not move the beat: it lifts a foot early when the foot has been carried as far behind as it reaches. A pawn with no legged rig reaches with client.anim.gaitStandInLegLength, which bounds the stance and nothing else.

The clock ticks whether or not the body is drawn, so your own footsteps play in first person with the body hidden, and whether or not client.anim.locomotionEnabled is on.

The direction of travel, measured against the body’s facing, blends four anchors: forward, left, back and right. The weights are linear in angle, so a diagonal is its own mix of its two neighbors. Backward has its own stride and lean; it is not a mirrored forward. Speed blends idle, walk and run, and the cadence rises through client.anim.locomotionSprintSpeed.

The pose order is stride clock → animation → springs → eyes → draw, and every step goes through one shared per-pawn path in ClientEntityPoses (AdvanceGait, then AdvanceAnimation). The desktop client, the iOS shell and every other host get the same picture with no code of their own.

  • Your own pawn moves by prediction: velocity, grounded, landed, noclip and the crouch, stated by the shared local-body submission.
  • Everyone else moves by where they are drawn from frame to frame and by their replicated Grounded, Noclip and Flying flags. Nothing new crosses the wire.
  • The crouch comes from the eye height for both: how far it sits between the standing eye and the crouched one. The server already smooths the eye through the transition, so the body sinks exactly as fast as the camera does. A state with no eye height reads as standing.
  • Editor bone edits layer on top of the animation. Each delta acts on the bone’s animated transform exactly as it acts on the rest pose.

An avatar whose rig maps no hips and no two whole legs (upper leg, lower leg and foot on each side) stands in its bind pose, as before. Its stride clock still ticks, so it still has footsteps.

Every number is a local preference under client.anim.locomotion* and applies on the next frame. The units are m/s, strides per second, degrees, seconds, and leg lengths for every distance on the body, so one setting reads the same on a small avatar and a tall one.

PreferenceDefaultMeaning
client.anim.locomotionEnabledtrueOff leaves every pawn in its bind pose. The stride clock and the footsteps keep running.
client.anim.gaitStandInLegLength0.85 mLeg length, at scale one, the stride clock reaches with for a pawn with no legged avatar.
client.anim.locomotionWalkSpeed / RunSpeed / SprintSpeed1.6 / 5.08 / 10.16 m/sWhere the walk and the run are complete, and where the cadence tops out.
client.anim.locomotionStartCadence / WalkCadence / RunCadence / SprintCadence0.6 / 0.9 / 1.4 / 1.8Strides per second at those speeds.
client.anim.locomotionWalkStance / RunStance0.62 / 0.36Share of a stride each foot is planted.
client.anim.locomotionStanceReach0.65Farthest a planted foot travels under the body.
client.anim.locomotionSideReach0.5Share of that reach when stepping sideways.
client.anim.locomotionWalkLift / RunLift0.12 / 0.42How high a swinging foot lifts.
client.anim.locomotionFootRoll25°Toe-off and heel-strike pitch at a full gait; none at idle.
client.anim.locomotionStepClearance0.08Gap the feet keep stepping sideways.
client.anim.locomotionSettleSeconds / SettleTolerance0.2 s / 0.2How long a stop takes to land the foot in the air, and how far from rest (leg lengths) a planted foot may stand before it steps home.
client.anim.locomotionSettleStepSeconds / SettleStepLift0.16 s / 0.06How long the settle step home takes, and how high it lifts the foot (leg lengths).
client.anim.locomotionSideStride / BackStride0.5 / 0.8Stride length sideways and backward, as a share of forward.
client.anim.locomotionForwardLean / BackLean / SideLean10° / 5° / 5°Lean into the run at the run speed.
client.anim.locomotionWalkBob / RunBob / RunCrouch0.02 / 0.035 / 0.04Hip rise over a walking leg, dip over a running one, and a lower run.
client.anim.locomotionPelvisTwist / ChestCounter12° / 1.3Pelvis yaw per leg length of foot split, and how much of it the chest turns back.
client.anim.locomotionArmSwing / WalkElbow / RunElbow / ArmOut38° / 18° / 85° / 8°Arm swing per leg length of split, elbow bends, and arms out from the sides.
client.anim.locomotionHeadSteady0.6Share of the trunk’s pitch the head turns back.
client.anim.locomotionIdleKnee0.015Hips sit lower at rest so the knees are soft.
client.anim.locomotionCrouchHipDrop / CrouchHipBack / CrouchLean0.7 / 0.3 / 35°How far the hips sink and sit back at a full crouch, and the trunk’s lean over them.
client.anim.locomotionCrouchAirTuck0.25How much higher a full crouch pulls the feet in the air (the hips sink only on the ground, fading out with the ground weight).
client.anim.locomotionMinFootBelowHip / MinLegFold0.15 / 0.3The least a foot target may sit under its hip and keep from it, so a tucked knee never folds behind the body.
client.anim.locomotionBreathPeriod / BreathAngle4 s / 1.5°The breath.
client.anim.locomotionSwayPeriod / SwayAmount7 s / 0.012The weight shift.
client.anim.locomotionVelocitySmoothing / FadeSeconds0.16 s / 0.18 sHow quickly the gait follows the velocity, and the ground/air/flight cross-fade.
client.anim.locomotionGaitEase1How softly idle, walk and run hand over: 0 is linear in speed, 1 a smoothstep.
client.anim.locomotionJumpSpeed5 m/sVertical speed that counts as a full rise or fall.
client.anim.locomotionTakeoffTuck / TuckSeconds / ApexTuck / FallTuck0.38 / 0.12 s / 0.22 / 0.04The feet’s tuck through the jump.
client.anim.locomotionFlailFallSpeed / FlailDelay / FlailRamp / FlailRelease3 m/s / 0.3 s / 0.25 s / 0.3 sThe free-fall flail: the downward speed that counts as falling, how long a fall lasts before the flail starts, how long it takes to ease in, and how long to ease out once the fall ends (slow, because the landing cross-fade is already taking the arms away). An ordinary jump’s descent never reaches it.
client.anim.locomotionFlailFrequency / FlailArmSwing / FlailArmRaise / FlailElbow / FlailLegKick1.5 Hz / 40° / 25° / 30° / 0.08The flail’s windmill rate on its own clock (never the stride’s), the arms’ swing, the shoulders’ lift, the elbows’ extra bend and the feet’s fore-aft kick against the arms.
client.anim.locomotionLandDrop / LandMaxDrop / LandRecover0.035 / 0.22 / 0.35 sHip drop per m/s of impact, its limit, and the recovery.
client.anim.locomotionFloatPeriod / FloatKick / FloatLegPhase / FloatTuck2.6 s / 0.14 / 0.37 / 0.22The floating legs’ drift.
client.anim.locomotionFloatLean / FloatLeanSpeed / FloatTrail35° / 12 m/s / 0.2Lean into the flight, and the feet trailing.
client.anim.locomotionFloatArmOut / FloatArmDrift28° / 10°The floating arms.
DigitalHeaven.Engine.Host --render <dir> --renderAnim --avatar <barcode> [--set client.render.msaa=1]

This walks one pawn through the same PlayerPawnVisual a live client draws with, at a fixed 60 Hz step. Its springs are on. The capture follows the pawn with its own camera and writes a strip of PNGs plus one CSV per strip: <avatar>_anim_<strip>_<nn>.png and <avatar>_anim_<strip>.csv.

  • idle: eight frames over one breath.
  • walk_<direction> and run_<direction> for all eight directions, plus sprint_forward: six frames across one stride, chosen by the stride’s own phase.
  • blend_run_000 to blend_run_090: the run at 15° steps from forward to left, all at the same phase. They show the 2D blend is continuous.
  • startStop: standing, a run, a stop and the settling step.
  • jump and jumpRun: take-off, tuck, rise, apex, fall, landing and recovery.
  • crouchJump: the standing jump with the crouch held.
  • crouch and crouch_walk: standing, sinking into a crouch and holding it; then one crouched stride at the walk speed.
  • floatStill and floatMove: noclip hovering, and flying forward up to the noclip speed.

The CSV records every step: the phase, the stride in meters, the speed, all blend weights, each foot’s height, whether it is planted, and the foot skate, which is how far a planted foot has slid since it was put down, in millimeters. The log states the worst skate per strip.

With MSAA off, two runs are byte-identical, the same pin verify-render-determinism.bat uses for its spawn half. The design is in Engine/design-notes/locomotion.md.

DigitalHeaven.Engine.Host --render <dir> --map core:maps/flat-map --renderAnim --animVideo --avatar <barcode>
[--animClip idle,walk,...] [--animFps 60] [--width 1280 --height 720] [--set client.render.msaa=1]
bun Engine/scripts/encode-anim-video.ts <dir>/animVideo [outDir]

--animVideo renders every frame of a set of clips instead of the strips, at a fixed rate: --animFps, 60 unless given, which is also the step the pawn is advanced by. It is the same pawn the strips walk, through the same PlayerPawnVisual, with its springs, and with its eyes, which are offered the camera as a face to look at. The camera trails the pawn from a fixed side, so a turn reads as a turn. Each clip lands in <dir>/animVideo/<avatar>/<clip>/ as 00000.png onward, with trace.csv (the strip CSV, one row per frame) and clip.json (the rate, the frame count and what the clip shows).

  • idle: six seconds standing.
  • walk: a walk around a circle while facing one way, so all eight directions and the blends between them show.
  • strafe: facing the camera, a walk left, right, back and forward again.
  • run: a run, a 180° turn, a sprint back, a turn and a stop, seen from the side.
  • startStop: standing, a run and a stop, from the side.
  • jump: a standing jump, then a jump out of a run, from the side.
  • noclip: a rise off the floor, a hover, a flight forward, a turn back and a hover.
  • showcase: all of the above back to back as one take.

--animClip picks clips by name; a run renders about eight frames a second at 1280x720, so the showcase is one run and the rest another. core:maps/flat-map keeps walls and props out of a moving camera’s way. With MSAA off, two runs write the same bytes.

encode-anim-video.ts turns every clip folder under the path it is given into an H.264 MP4 at the rate in its clip.json, yuv420p at CRF 16, so it plays anywhere, a phone included. The engine never runs ffmpeg; the script does, from PATH or $FFMPEG.