← Home SEEBIE90 DOCS
Download PDF
No copies or uploads of M.A.W.S. assetsExample code is free to useNo printed reproduction or modification of this documentationSuggestions: Discord to the authorLicense

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.

In plain wordsEach machine looks at how fast the walker moves and decides where each foot should stand. A foot on the ground stays exactly where it is; a lifted foot swings to the next spot. Simple trigonometry then bends the knee so the leg reaches the foot.

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.

Example code – free to use
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.

Example code – free to use
// 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

Example code – free to use
//! 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.

Example code – free to use
//! 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.

Example code – free to use
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();
}
PitfallNever read 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.

Example code – free to use
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;
NoteParking blends a crouch posture in (SmoothCD toward 1). Limp lowering is faded out while crouched – otherwise a walker with two broken legs folds its legs sideways.
Example code on this site is free to use in your own projects (credit appreciated). The M.A.W.S. mods, their code and assets are covered by the M.A.W.S. License – no copies or uploads. This documentation may not be reproduced in print or modified. Suggestions: Discord. Arma Reforger / Enfusion are trademarks of Bohemia Interactive.
Impressum · Privacy Policy