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.
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.
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).
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);
}
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.
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;
}
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
bool parked = rig.IsParkWanted() || rig.GetParkBlend() > 0.05;
if (parked)
m_fThrottle = 0; // no walking, no strafing, no turning
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.
// 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;
}