Movement
Procedural leg rig
The legs are not animated clips. Every machine computes where the feet stand, when they swing and how the joints bend – from nothing more than the motion of the vehicle root.
Measure, do not replicate
The rig reads speed and yaw rate from how the root moved since the last frame. On machines that only see the interpolated transform, network corrections move a parked walker by centimetres – enough to start a step. Measure over a short window and use small dead zones there.
vector delta = rootPos - m_vLastRootPos;
float measured = Vector(delta[0], 0, delta[2]).Length() / measDt;
if (IsProxy()) // not server, not owner: smooth over 0.2 s, ignore tiny corrections
measured = SmoothedOverWindow(measured, 0.2);
if (measured < 0.3)
measured = 0;
Gait phase
A single phase value 0..1 runs the whole gait. The right leg is half a cycle behind the left one. Each leg swings for the first 40 % of its cycle – below 50 % there is always a moment with both feet on the ground.
// stride grows with speed, cadence follows from it
float StrideAt(float v)
{
float s = m_fStrideLength * Math.Pow(Math.Max(v, 0.01) / m_fStrideRefSpeed, m_fStrideGrowth);
return Math.Clamp(s, m_fStrideMin, m_fStrideMax);
}
void UpdatePhase(float speed, float dt)
{
float cadence = Math.Clamp(speed / StrideAt(speed), m_fMinCadence, m_fMaxCadence); // cycles per second
m_fPhase = Math.Frac(m_fPhase + cadence * dt);
}
float LegPhase(bool left)
{
if (left)
return m_fPhase;
return Math.Frac(m_fPhase + 0.5);
}
bool IsSwinging(bool left)
{
return LegPhase(left) < m_fSwingFraction; // 0.4
}
Stance and swing
- Stance: the foot is fixed in world space. It stays exactly where it landed while the body walks over it.
- Swing target (Raibert style): the neutral foot spot, moved ahead by velocity × τ and rotated by yaw rate × τ, where τ = remaining swing time + half the stance. Then traced to the ground.
- Swing arc:
4s(1-s) · stepHeighton top of the eased line between lift-off and target, never below the terrain. - Rescue step: if a planted foot would overstretch the leg, it steps out of phase.
//! Ground under a foot target. Own walker parts are skipped by retrying just below the hit.
vector TraceGround(BaseWorld world, IEntity owner, vector p)
{
vector from = p + Vector(0, 5, 0);
vector to = p - Vector(0, 10, 0);
for (int i = 0; i < 3; i++)
{
autoptr TraceParam tp = new TraceParam();
tp.Start = from;
tp.End = to;
tp.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
tp.LayerMask = EPhysicsLayerDefs.Projectile;
tp.Exclude = owner;
float f = world.TraceMove(tp, null);
if (f >= 1.0)
break;
vector hit = from + (to - from) * f;
if (tp.TraceEnt && tp.TraceEnt.GetRootParent() == owner)
{
from = hit - Vector(0, 0.05, 0); // our own leg: look below it
continue;
}
return hit;
}
return Vector(p[0], world.GetSurfaceY(p[0], p[2]), p[2]);
}
Three segments, solved as two
The walker leg has thigh, shin and metatarsus (digitigrade, like a bird). Keeping thigh and metatarsus parallel turns it into a pantograph – and the problem collapses to a classic two-bone solve with the law of cosines.
//! Two-bone IK in the leg plane. Returns the knee bend angle (rad) for a hip-to-ankle distance d.
//! a, b = effective segment lengths (pantograph reduced).
static float KneeAngle(float a, float b, float d)
{
d = Math.Clamp(d, Math.AbsFloat(a - b) + 0.001, a + b - 0.001);
float c = (a * a + b * b - d * d) / (2.0 * a * b);
return Math.Acos(Math.Clamp(c, -1.0, 1.0));
}
//! Angle between the hip-to-ankle line and the first segment.
static float HipOffset(float a, float b, float d)
{
float c = (a * a + d * d - b * b) / (2.0 * a * d);
return Math.Acos(Math.Clamp(c, -1.0, 1.0));
}
Moving kinematic parts
Each leg segment is its own child entity. The rig computes a target transform in the root frame, converts it into the frame of the real parent and writes it.
void WritePart(IEntity part, IEntity parent, vector targetInRoot[4], vector parentInRoot[4])
{
vector local[4];
Math3D.MatrixInvMultiply4(parentInRoot, targetInRoot, local);
part.SetLocalTransform(local);
part.Update();
}
GetLocalTransform of freshly attached children – it returns garbage in the first frame. Collect the parts with a short CallLater after spawn and compute their rest pose from your own data.Pelvis
The pelvis carries the life of the walk: bob (twice per cycle), sway and roll (once per cycle), forward lean from speed and acceleration, a small height adaption when a planted leg would overstretch, and a spring for impacts. The stabilizer later removes exactly the fast part of this from the torso.
float ang = m_fPhase * Math.PI2;
float bob = m_fBobAmp * m_fGaitAmp * Math.Cos(2 * ang);
float sway = m_fSwayAmp * m_fGaitAmp * Math.Cos(ang);
float roll = m_fRollAmp * m_fGaitAmp * Math.Cos(ang);
pelvis = posture(parkBlend) * level(slope) * Euler(twist, lean, roll);
pelvis[3] = pelvis[3] + Vector(sway, bob - m_fHeightAdapt - limp, 0) + m_vImpactSpring;
SmoothCD toward 1). Limp lowering is faded out while crouched – otherwise a walker with two broken legs folds its legs sideways.