I spend enough time on real trails that the off-road games always bother me in the same place: the physics. So my current side project is an off-road simulation in Unity with fully custom vehicle physics. No built-in WheelCollider, no shortcuts. Raycast suspension where every parameter is mine to tune: spring rate, damping, ride height, travel, grip. The long-term goal is proper mud and terrain deformation, but the vehicle foundation has to be right first.
The architecture that didn’t work
The first version was the obvious one and it was wrong. Each wheel had its own script reading input and applying drive forces, which meant four scripts fighting over one chassis. Debugging was miserable, because no single place owned the truth about what the vehicle was doing.
The rewrite moved everything into one controller. It reads input once, owns steering and drive, and loops through wheels that only report suspension state and contact:
public class UniversalCar : MonoBehaviour
{
[SerializeField] Wheel[] wheels;
[SerializeField] float springRate, damping, restLength, wheelRadius;
Rigidbody rb;
void FixedUpdate()
{
float throttle = Input.GetAxis("Vertical"); // read ONCE
float steer = Input.GetAxis("Horizontal");
foreach (var w in wheels)
{
if (Physics.SphereCast(w.transform.position, wheelRadius,
-transform.up, out var hit,
restLength, groundMask))
{
float compression = restLength - hit.distance;
float springVel = (compression - w.prevCompression)
/ Time.fixedDeltaTime;
float force = compression * springRate
+ springVel * damping;
rb.AddForceAtPosition(transform.up * force,
w.transform.position);
w.prevCompression = compression;
w.grounded = true;
}
else { w.grounded = false; w.prevCompression = 0f; }
}
}
}
The wheels store prevCompression so damping works from real spring velocity instead of guesses, and forces land at each wheel’s position on the chassis body, which is what makes weight transfer emerge for free instead of being faked. The lesson carries outside game dev: a vehicle is one physical system, and the moment you let the parts make their own decisions you get four opinions instead of one truth. Same reason a car has one ECU and not one per cylinder.
Best bug so far
On the first run of the new architecture, the truck launched into orbit the instant I hit play. I chased scripts, spring constants, force limits, and layer masks before finding the indicator that mattered: raising the chassis spawn height stopped the launch. The suspension was starting fully compressed, so frame one contained four springs at maximum stored energy with nowhere to go but up. The fix is one line of humility, spawn the chassis so the wheels start near rest length, plus clamping the spring force so a bad frame can’t become an orbital insertion:
float force = Mathf.Clamp(compression * springRate + springVel * damping,
0f, maxSpringForce); // springs push, never pull
Every discipline has its version of this: the fault appears at startup because the initial conditions were wrong, not because the running system is broken.
It drives, it’s stable, and there’s a long list ahead: tire friction, braking, slip, weight transfer tuning, then the terrain work. This one connects the two halves of the garage directly, because twenty years of real axle swaps and trail miles is exactly the reference data a simulation needs.
