obOB STUDIO
← Back to blog
Physics

Drift Physics: Making Cars Feel Right Without a Physics Engine

5 min readolivers-racers

I spent two weeks with RigidBody3D before I threw it out. Not because it can't simulate a car. It absolutely can. But because it simulates a car too well. I don't want tire slip curves and suspension springs. I want you to hold SPACE, kick the tail out, and slide around a corner grinning. That's a CharacterBody3D job.

The constants that matter

Here are the numbers that define how every car in the game feels:

extends CharacterBody3D

const GRAVITY          := 30.0
const DRAG_QUAD        := 0.010   # quadratic drag
const DRAG_LIN         := 0.22    # rolling resistance
const BRAKE_POWER      := 36.0    # decel when braking from forward speed
const STEER_RATE       := 1.9     # base yaw rate at full lock (rad/s)
const STEER_SMOOTH     := 8.0     # how fast the wheel turns toward the input
const HIGH_SPEED_TAME  := 0.52    # steering reduction approaching top speed
const HANDBRAKE_GRIP   := 0.22    # grip multiplier while the handbrake is on
const HANDBRAKE_YAW    := 1.35    # extra steering kick while drifting
const DRIFT_SIDE_SPD   := 4.2     # sideways m/s that counts as a drift

That HANDBRAKE_GRIP is the magic number. Normal grip bleeds sideways velocity fast. Your car tracks where it's pointed. Hold the handbrake and that grip drops to 22% of its normal value. Suddenly the back end keeps sliding. That's drifting.

The grip model

The actual grip logic is one line of math buried in the _drive() function:

l2 *= exp(-grip_eff * delta)
flat = fwd * f2 + right * l2

l2 is the car's lateral speed. How fast it's moving sideways. grip_eff is the effective grip value, which accounts for the car's base grip stat, whether you're on road or grass, whether the surface is slippery (snow has slick = 0.82), and whether the handbrake is on:

var grip_eff := _grip * slick * (surface if off else 1.0)
if handbrake:
    grip_eff *= HANDBRAKE_GRIP
if not grounded:
    grip_eff *= 0.06   # airborne: almost no lateral bleed

The exponential decay is key. It means high grip doesn't snap the car straight. It eases it. The tail swings and then catches. At low grip (handbrake held), the decay is slow enough that you maintain a long, sweeping slide. And in the air? Grip drops to 6%. You barely realign at all, which is why jumps feel floaty and momentum-driven.

Speed-sensitive steering

Nobody wants full steering lock at 200 km/h. That's how you get barrel rolls. So the steering authority scales with speed in both directions:

var authority := clampf(absf(f_spd) / STEER_REF_SPEED, 0.0, 1.0)
var tame := 1.0 - HIGH_SPEED_TAME * clampf(absf(f_spd) / _top, 0.0, 1.0)
var yaw := steer * STEER_RATE * _steer_mult * authority * tame

At low speed, authority is low. You can't spin in place. At high speed, tame kicks in and chops up to 52% off your turning rate. The sweet spot is mid-range, where the car is responsive but controllable. And when the handbrake is on, HANDBRAKE_YAW = 1.35 cranks the steering rate up so you can actually aim through a drift.

Six cars, six personalities

Every car reads its stats from a catalog at equip time:

func apply_car() -> void:
    var c: Dictionary = CarCatalog.get_car(GameState.equipped)
    _power = float(c.power)
    _top = float(c.top_speed)
    _grip = float(c.grip)
    _steer_mult = float(c.steer)
    _offroad_stat = float(c.offroad)

The go-kart has low top speed but sharp steering and decent grip. The hypercar is blisteringly fast but starts drifting if you even look at a corner wrong. The Rally Lynx has a high _offroad_stat, so it barely slows on grass while other cars bog down to 55% top speed. Vortex GT? Beautiful on asphalt. Touch grass and it's like driving on ice.

The beauty of the exponential grip model is that these stat differences compound. A high-grip car on a slippery surface drifts differently from a low-grip car on tarmac. Not just "more" or "less" but in genuinely distinct ways that you learn to feel.

The dead end: RigidBody

Godot's physics engine applies forces, torques, and impulses, then resolves collisions. But I need frame-perfect control over the velocity vector. With CharacterBody3D, I set velocity directly and call move_and_slide(). With RigidBody, I'd have to fight the solver. Applying counter-forces to stop unwanted rotation, manually clamping angular velocity, and praying the integrator doesn't introduce weird oscillations at high speed.

Could I have made it work? Sure. But arcade driving isn't about simulation accuracy. It's about feel. And feel means I need to touch every number, every frame, with zero surprises from a physics solver I don't own.

CharacterBody3D gave me that control. And the code ended up simpler, too.

ob

Written by Oliver

I build browser games and simulations on my own, everything here runs in a tab, with no installer and no account. The biggest is Oliver's Racers: procedural circuits in Godot 4, online multiplayer relayed by a Raspberry Pi in my room, and an Android build. Almost nothing here is imported artwork; the cars, trees and grandstands are built out of boxes and cylinders in code at load time.

More about me · See the projects