← 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

Leg drive

The walker is a wheeled vehicle with invisible wheels. The leg drive throws away what the engine and gearbox would do and writes the velocity directly – every frame, on the physics owner only.

In plain wordsInstead of letting the engine and wheels push the vehicle, a script sets its speed and turn rate every frame. That gives exact speeds and smooth acceleration. A second part keeps the body from rocking on uneven ground.

Why SetVelocity instead of forces

Wheel forces on a 50 t body react slowly, slide on slopes and depend on gear ratios. Writing the horizontal velocity directly gives an exact top speed, a designed acceleration curve and no sliding when standing. Vertical velocity is left to physics so the body still falls, climbs and lands.

Example code – free to use
override void EOnFrame(IEntity owner, float dt)
{
	if (m_Rpl && !m_Rpl.IsOwner())
		return;				// only the machine that simulates the body

	Physics phys = owner.GetPhysics();
	if (!phys || !phys.IsDynamic() || IsAirborne())
		return;				// in the air: hands off (remember the real speed)

	vector fwd = owner.GetTransformAxis(2);
	fwd[1] = 0;
	fwd.Normalize();
	vector right = Vector(fwd[2], 0, -fwd[0]);

	// --- target speed from input, capped by the payload limit
	float vmax = Math.Min(m_fMaxSpeedKmh, GetPayloadSpeedLimitKmh()) / 3.6;
	float target = m_fThrottle * vmax * GetLegFactor();		// 0 when parked or a leg is broken

	// --- ramp: 50 t take a moment to get going
	float rate = m_fDecel;
	if (Math.AbsFloat(target) > Math.AbsFloat(m_fSpeed))
		rate = m_fAccel;
	m_fSpeed += Math.Clamp(target - m_fSpeed, -rate * dt, rate * dt);

	// --- obstacle: never push harder than half of what really happens (no jitter at walls)
	vector v = phys.GetVelocity();
	float real = vector.Dot(v, fwd);
	if (Math.AbsFloat(real) < Math.AbsFloat(m_fSpeed) * 0.5)
		m_fSpeed = real + (m_fSpeed - real) * 0.5;

	vector vNew = fwd * m_fSpeed + right * m_fStrafe;
	vNew[1] = v[1];			// keep gravity / climbing
	phys.SetVelocity(vNew);

	TurnTowardHeading(owner, phys, dt);
}

Turning toward a heading

The player drives with the mouse: the walker turns toward where the camera looks. A dead zone keeps small glances from turning the body, a gain turns the heading error into a yaw rate. Players can tune both (per-player settings).

Example code – free to use
protected void TurnTowardHeading(IEntity owner, Physics phys, float dt)
{
	float bodyYaw = owner.GetYawPitchRoll()[0];
	float error = MyMath.WrapDeg(m_fHeadingWanted - bodyYaw);

	float over = Math.Max(Math.AbsFloat(error) - m_fDeadZoneDeg, 0);
	if (error < 0)
		over = -over;

	float maxRate = Math.Lerp(m_fTurnRateSpot, m_fTurnRateWalk, Math.Clamp(Math.AbsFloat(m_fSpeed) / 2.0, 0, 1));
	float wantDeg = Math.Clamp(over * m_fGain, -maxRate, maxRate);

	// smooth start / stop of the turn
	float k = Math.Clamp(dt / m_fTurnResponse, 0, 1);
	m_fYaw += (wantDeg * Math.DEG2RAD * m_fYawSign - m_fYaw) * k;

	vector w = phys.GetAngularVelocity();
	w[1] = m_fYaw;
	ApplyHullGyro(owner, w, dt);		// pitch / roll, see below
	phys.SetAngularVelocity(w);
}
PitfallThe sign between angular velocity and yaw angle depends on conventions you will get wrong at least once. M.A.W.S. measures it at runtime: if the body turns away from the target for 20 frames, m_fYawSign flips.

Hull gyro

With free pitch and roll the body rocks on its invisible suspension over every bump. The gyro pulls pitch/roll rate toward the rate that would align the hull with the averaged terrain slope under it. Slopes stay slopes, only the rocking disappears.

Example code – free to use
protected void ApplyHullGyro(IEntity owner, inout vector w, float dt)
{
	BaseWorld world = owner.GetWorld();
	vector p = owner.GetOrigin();
	float a = 9.0;		// half size of the sampled area (m) - not too small, see below

	float x0 = world.GetSurfaceY(p[0] - a, p[2]);
	float x1 = world.GetSurfaceY(p[0] + a, p[2]);
	float z0 = world.GetSurfaceY(p[0], p[2] - a);
	float z1 = world.GetSurfaceY(p[0], p[2] + a);
	vector ground = Vector(x0 - x1, 2 * a, z0 - z1).Normalized();

	vector up = owner.GetTransformAxis(1);
	// cross(up, ground): rotation axis * sin(angle), horizontal part only
	vector err = Vector(up[1] * ground[2] - up[2] * ground[1], 0, up[0] * ground[1] - up[1] * ground[0]);
	vector target = err * m_fGyroFollow;		// 1/s

	float k = Math.Clamp(dt / m_fGyroTime, 0, 1);	// 0.15 s
	w[0] = w[0] + (target[0] - w[0]) * k;
	w[2] = w[2] + (target[2] - w[2]) * k;
}
PitfallDo not follow the terrain under the feet too closely. Sampled over ±5 m and followed directly, the hull pitched and rolled up to 12°/s on uneven ground – every crew camera on it shook. Sample a larger area (±9 m, two rings averaged) and low-pass the slope (about 0.6 s): the hull follows the shape of the hill, not every bump.

Above about 20° tilt an additional upright assist overrides pitch/roll with a stronger righting rate, so a walker that brakes hard while turning on a slope does not tip over.

Park lock

PitfallDo not gate the drive on an animation value alone. If the rig tick stalls on a server, a parked walker could be driven while every client shows it crouched. Gate on the replicated park wish as well.
Example code – free to use
bool parked = rig.IsParkWanted() || rig.GetParkBlend() > 0.05;
if (parked)
	m_fThrottle = 0;		// no walking, no strafing, no turning
PitfallCheck in the log that the handbrake really engages. A state flag that starts as "brake already on" means the code never actually pulls it – the invisible wheels roll and a parked walker slowly slides and turns.

Park anchor

Setting the velocity to zero once per frame is not enough. Whatever pushes the body – an editor-placed vehicle that sits a little in the ground and is pushed out again, wheel contacts on a slope – acts in every physics substep. A parked walker crept away at about 0.06 m/s and turned 1.4°/s. The fix is an anchor: remember position and heading when parking, then correct the accumulated error every frame on the physics owner.

Example code – free to use
// physics owner, every frame
if (parked && Math.AbsFloat(m_fSpeed) < 0.1)
{
	vector here = owner.GetOrigin();
	float yaw = MyMath.Yaw(owner);
	float yawErr = MyMath.WrapDeg(m_fAnchorYaw - yaw);

	// first time, or pushed away by a blast / a ramming vehicle: accept the new spot
	if (!m_bAnchor || vector.DistanceXZ(here, m_vAnchor) > 3 || Math.AbsFloat(yawErr) > 20)
	{
		m_bAnchor = true;
		m_vAnchor = here;
		m_fAnchorYaw = yaw;
		yawErr = 0;
	}

	vector back = (m_vAnchor - here) * 2.0;		// 2 m/s per metre of error
	back[1] = 0;
	if (back.Length() > 1)
		back = back.Normalized();

	vector v = physics.GetVelocity();
	physics.SetVelocity(Vector(back[0], v[1], back[2]));	// keep gravity

	vector w = physics.GetAngularVelocity();
	w[1] = Math.Clamp(yawErr * 2.0, -10, 10) * Math.DEG2RAD;
	physics.SetAngularVelocity(w);
}
else
{
	m_bAnchor = false;
}
NoteClients that do not own the physics still simulate a dynamic proxy. While parked, zero its horizontal velocity and yaw rate as well – otherwise it drifts between two network corrections.
PitfallRe-anchor on a jump within one frame (more than about 0.5 m or 2°). Creep is around a millimetre per frame, a Game Master moving or rotating the walker is not – without this the anchor pulls it back.
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