Skip to main content

vehicle_controller

Simulating a car with rigid-bodies and joints, for example using one rigid-body per wheel attached to the chassis by a joint, is possible but can be difficult to control for games requiring non-realistic vehicles. This is why Rapier provides a vehicle controller (the current implementation was ported from the btRaycastVehicle of Bullet). The vehicle is a single rigid-body modeling its chassis, and its wheels are only represented by ray-casts pushing that body along a spring-like suspension.

info

The vehicle controller is only available in 3D, i.e., with bevy_rapier3d.

info

The vehicle controller is only available in 3D, i.e., with the r3 functions.

Setup​

The chassis is created like any other dynamic rigid-body, and the wheels are added to the controller afterwards. Each wheel is given its position on the chassis, the direction of its suspension (the direction of its ray-cast), its axle, the rest length of its suspension, and its radius. The WheelTuningR3WheelTuning shared by the wheels is what makes the vehicle feel heavy or light by controlling the elastic properties (stiffness and damping) of the suspension, as well as the grip of the wheels:

// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let (chassis_handle, _) = world.insert(
RigidBodyBuilder::dynamic().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(hw * 2.0, hh, hw).density(100.0),
);

// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};

let mut vehicle = DynamicRayCastVehicleController::new(chassis_handle);
let wheel_positions = [
Vector::new(hw * 1.5, -hh, hw),
Vector::new(hw * 1.5, -hh, -hw),
Vector::new(-hw * 1.5, -hh, hw),
Vector::new(-hw * 1.5, -hh, -hw),
];

for position in wheel_positions {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, -Vector::Y, Vector::Z, hh, hh / 4.0, &tuning);
}
// The chassis is an ordinary dynamic rigid-body.
let hw = 0.3;
let hh = 0.15;
let chassis = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic().setTranslation(0.0, 1.0, 0.0),
);
world.createCollider(RAPIER.ColliderDesc.cuboid(hw * 2.0, hh, hw).setDensity(100.0), chassis);

let vehicle = world.createVehicleController(chassis);
let wheelPositions = [
{ x: hw * 1.5, y: -hh, z: hw }, { x: hw * 1.5, y: -hh, z: -hw },
{ x: -hw * 1.5, y: -hh, z: hw }, { x: -hw * 1.5, y: -hh, z: -hw },
];

for (let position of wheelPositions) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.addWheel(position, { x: 0.0, y: -1.0, z: 0.0 }, { x: 0.0, y: 0.0, z: 1.0 }, hh, hh / 4.0);
}

// The tuning of each wheel: its suspension and its grip.
for (let i = 0; i < vehicle.numWheels(); ++i) {
vehicle.setWheelSuspensionStiffness(i, 100.0);
vehicle.setWheelSuspensionCompression(i, 10.0);
vehicle.setWheelSuspensionRelaxation(i, 10.0);
}

The vehicle controller is the RayCastVehicleController component, added to the entity of the chassis' dynamic rigid-body. Its wheels are VehicleWheel values stored in its wheels field, and can be added, removed, or modified at any time:

// The tuning shared by the wheels: the suspension and the grip.
let tuning = WheelTuning {
suspension_stiffness: 100.0,
suspension_damping: 10.0,
..WheelTuning::default()
};

let hw = 0.3;
let hh = 0.15;
let wheel_positions = [
Vec3::new(hw * 1.5, -hh, hw),
Vec3::new(hw * 1.5, -hh, -hw),
Vec3::new(-hw * 1.5, -hh, hw),
Vec3::new(-hw * 1.5, -hh, -hw),
];
let wheels = wheel_positions
.into_iter()
.map(|position| {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
VehicleWheel::new(position, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning)
})
.collect();

// The chassis is an ordinary dynamic rigid-body, with the vehicle controller attached to it.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::cuboid(hw * 2.0, hh, hw),
ColliderMassProperties::Density(100.0),
RayCastVehicleController::new(wheels),
));

The vehicle controller is the R3DynamicRayCastVehicleController object, created by r3NewDynamicRayCastVehicleController from the handle of the chassis' dynamic rigid-body. It remembers the world of that rigid-body, so it must be freed with r3FreeDynamicRayCastVehicleController before that world is freed. The wheels are added with r3DynamicRayCastVehicleController_AddWheel, which returns the index of the new wheel (starting from zero, in insertion order), and the R3WheelTuning given to each wheel is initialized with r3DefaultWheelTuning:

// The chassis is an ordinary dynamic rigid-body.
const R3Real hw = 0.3;
const R3Real hh = 0.15;
R3RigidBodyDesc chassis_body = r3DynamicRigidBodyDesc();
chassis_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle chassis_handle = r3InsertRigidBody(world, &chassis_body);
R3ColliderDesc chassis_collider = r3CuboidColliderDesc(r3Vector(hw * 2.0, hh, hw));
chassis_collider.density = 100.0;
r3InsertCollider(chassis_handle, &chassis_collider);

// The tuning shared by the wheels: the suspension and the grip.
R3WheelTuning tuning = r3DefaultWheelTuning();
tuning.suspension_stiffness = 100.0;
tuning.suspension_damping = 10.0;

// The controller must be freed (with r3FreeDynamicRayCastVehicleController) before its world.
R3DynamicRayCastVehicleController *vehicle = r3NewDynamicRayCastVehicleController(chassis_handle);
const R3Vector wheel_positions[4] = {
{hw * 1.5, -hh, hw},
{hw * 1.5, -hh, -hw},
{-hw * 1.5, -hh, hw},
{-hw * 1.5, -hh, -hw},
};

for (size_t i = 0; i < 4; i++) {
// The position of the wheel, the direction its suspension pushes along, its axle, the
// rest length of its suspension, and its radius; all in the local frame of the chassis.
r3DynamicRayCastVehicleController_AddWheel(vehicle, wheel_positions[i], r3Vector(0.0, -1.0, 0.0),
r3Vector(0.0, 0.0, 1.0), hh, hh / 4.0, &tuning);
}

By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points upward. Other axes can be selected with r3DynamicRayCastVehicleController_SetAxes.

The vehicle controller is the DynamicRayCastVehicleController class, created from the handle of the chassis' dynamic rigid-body. The wheels are added with its add_wheel method, which returns the index of the new wheel (starting from zero, in insertion order). The WheelTuning given to each wheel is created with keyword arguments overriding its default values (as given by WheelTuning.default()):

# The chassis is an ordinary dynamic rigid-body.
hw = 0.3
hh = 0.15
chassis_handle = world.add_body(
rp.RigidBody.dynamic(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(hw * 2.0, hh, hw).density(100.0)],
)

# The tuning shared by the wheels: the suspension and the grip. The
# parameters that aren't given keep their default values.
tuning = rp.WheelTuning(suspension_stiffness=100.0, suspension_damping=10.0)

vehicle = rp.DynamicRayCastVehicleController(chassis_handle)
wheel_positions = [
(hw * 1.5, -hh, hw),
(hw * 1.5, -hh, -hw),
(-hw * 1.5, -hh, hw),
(-hw * 1.5, -hh, -hw),
]

for position in wheel_positions:
# The position of the wheel, the direction its suspension pushes along, its axle, the
# rest length of its suspension, and its radius; all in the local frame of the chassis.
vehicle.add_wheel(position, (0.0, -1.0, 0.0), (0.0, 0.0, 1.0), hh, hh / 4.0, tuning)

By default, the vehicle moves forward along the local x axis of the chassis, and its local y axis points upward. Other axes can be selected with the index_forward_axis and index_up_axis properties of the controller (0, 1, and 2 standing for the x, y, and z axes).

Driving the vehicle​

A vehicle is driven by giving each of its wheels an engine force, a brake force, and a steering angle. The controller is then updated before each timestep, which is when the ray-casts are made and when the resulting suspension and friction forces are applied to the chassis. It is strongly recommended to exclude the chassis from the ray-casts, otherwise the ray might hit it and be misinterpreted as being the floor.

for _ in 0..200 {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
let wheels = vehicle.wheels_mut();
wheels[0].engine_force = 30.0;
wheels[0].steering = 0.2;
wheels[1].engine_force = 30.0;
wheels[1].steering = 0.2;

// The wheels are ray-casted against the scene: the chassis itself, as well as every other
// dynamic body, is generally excluded from these ray-casts.
let queries = world.broad_phase.as_query_pipeline_mut(
world.narrow_phase.query_dispatcher(),
&mut world.bodies,
&mut world.colliders,
QueryFilter::exclude_dynamic().exclude_rigid_body(chassis_handle),
);
vehicle.update_vehicle(world.integration_parameters.dt, queries);

world.step();
}

println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for (let k = 0; k < 200; ++k) {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
vehicle.setWheelEngineForce(0, 30.0);
vehicle.setWheelSteering(0, 0.2);
vehicle.setWheelEngineForce(1, 30.0);
vehicle.setWheelSteering(1, 0.2);

// The wheels are ray-casted against the scene: the dynamic bodies, including the chassis
// itself, are generally excluded from these ray-casts.
vehicle.updateVehicle(world.integrationParameters.dt, RAPIER.QueryFilterFlags.EXCLUDE_DYNAMIC);
world.step();
}

console.log("Vehicle speed:", vehicle.currentVehicleSpeed());

The controller is updated automatically by the plugin before each simulation step, and the colliders attached to the chassis are always excluded from the ray-casts. Other colliders can be excluded with the filter_flags, filter_groups, exclude_colliders, exclude_rigid_bodies, and filter_predicate fields of the RayCastVehicleController (similar to the ones of the character controller), or by inserting the ControllerIgnored component on them (or on their rigid-body). So driving the vehicle is just a matter of modifying the engine force, brake, and steering of its wheels:

fn drive_vehicle(mut vehicles: Query<&mut RayCastVehicleController>) {
for mut vehicle in vehicles.iter_mut() {
// The vehicle is driven by setting the engine force, the brake, and the steering angle of
// its wheels. Here the two front wheels are the driving and steering ones.
for wheel in &mut vehicle.wheels[0..2] {
wheel.engine_force = 30.0;
wheel.steering = 0.2;
}

// The colliders of the chassis are always ignored by the ray-casts of the wheels. Other
// colliders, here the ones attached to dynamic rigid-bodies, can be ignored too.
vehicle.filter_flags =
QueryFilterFlags::EXCLUDE_SENSORS | QueryFilterFlags::EXCLUDE_DYNAMIC;

// The results of the last update are written back into the component.
println!("Vehicle speed: {}", vehicle.current_vehicle_speed);
for wheel in &vehicle.wheels {
println!(
"Wheel in contact: {}, suspension length: {}",
wheel.state.is_in_contact, wheel.state.suspension_length
);
}
}
}
warning

The forces applied by the vehicle controller are computed once per update of the plugin, using the length of the last simulation timestep. So it is best used with a fixed timestep.

The engine force, brake, and steering angle of a wheel are set with r3DynamicRayCastVehicleController_SetWheelControls (from the index of the wheel), and the controller is updated with r3DynamicRayCastVehicleController_UpdateVehicle before each r3Step. The colliders attached to the chassis are always excluded from the ray-casts, and the other obstacles can be filtered with the R3QueryOptions given to the update (see the scene query filters). Note that its predicate callback, if any, is called once for every collider of the world before the update, while the world is locked for writing: it may only use the Read functions of its R3ReadContext.

// The wheels are ray-casted against the scene: the chassis itself is always excluded, and
// every other dynamic body is generally excluded from these ray-casts too.
R3QueryOptions options = r3DefaultQueryOptions();
options.filter.flags = R3_QUERY_EXCLUDE_DYNAMIC;

for (int i = 0; i < 200; i++) {
// The vehicle is driven by setting the steering angle, the engine force, and the brake of
// its wheels. Here the two front wheels (indices 0 and 1) are the driving and steering ones.
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 0, 0.2, 30.0, 0.0);
r3DynamicRayCastVehicleController_SetWheelControls(vehicle, 1, 0.2, 30.0, 0.0);

r3DynamicRayCastVehicleController_UpdateVehicle(vehicle, r3TimeStep(world), &options);

r3Step(world, NULL, NULL);
}

printf("Vehicle speed: %f\n", (double)r3DynamicRayCastVehicleController_CurrentVehicleSpeed(vehicle));

The engine force, brake, and steering angle of a wheel are set with the apply_engine_force, set_brake, and set_steering methods of the controller (from the index of the wheel), and the controller is updated with its update_vehicle method before each PhysicsWorld.step. The colliders attached to the chassis are always excluded from the ray-casts, and the other obstacles can be filtered with the optional QueryFilter given to update_vehicle (see the scene query filters). The predicate of that filter, if any, is called once for every collider before the update, while the sets are locked: it must not modify them.

for _ in range(200):
# The vehicle is driven by setting the engine force, the brake, and the steering angle of
# its wheels (from their indices). Here the two front wheels are the driving and steering ones.
vehicle.apply_engine_force(0, 30.0)
vehicle.set_steering(0, 0.2)
vehicle.apply_engine_force(1, 30.0)
vehicle.set_steering(1, 0.2)

# The wheels are ray-casted against the scene: the chassis itself is always excluded from
# these ray-casts, and every other dynamic body is generally excluded too.
vehicle.update_vehicle(
world.integration_parameters.dt,
world.rigid_bodies,
world.colliders,
world.query_pipeline,
rp.QueryFilter.exclude_dynamic(),
)

world.step()

print("Vehicle speed:", vehicle.current_vehicle_speed)
note

The state of each wheel after an update (whether it touches the floor, the compression of its suspension, its rotation angle, etc.) is readable from the controller. This is what the rendering of the wheels can be based on since there is no actual per-wheel rigid-bodies to read their state from.

The state of each wheel is written back by the plugin into its VehicleWheel::state field (a VehicleWheelState), and the speed of the vehicle into RayCastVehicleController::current_vehicle_speed. The VehicleWheel::local_transform method computes the transform of a wheel relative to the chassis (including its suspension length, steering, and rotation), which can be given directly to the child entity rendering that wheel.

The state of the wheels is copied into a buffer of R3WheelState by r3DynamicRayCastVehicleController_Wheels: the world-space center of each wheel, the world-space directions of its suspension and axle, its rotation angle, the length and force of its suspension, and its contact with the ground (whether it touches it, the collider touched, and the world-space contact point and normal). The speed of the vehicle along its forward axis is given by r3DynamicRayCastVehicleController_CurrentVehicleSpeed.

// The wheels are given in the order they were added to the controller.
R3WheelState wheels[4];
size_t num_wheels = r3DynamicRayCastVehicleController_Wheels(vehicle, wheels, 4);
for (size_t i = 0; i < num_wheels; i++) {
// The world-space center of the wheel, its current suspension length, rotation angle, etc.
printf("Wheel %zu: center (%f, %f, %f), suspension length %f, rotation %f, in contact: %u\n", i,
(double)wheels[i].center.x, (double)wheels[i].center.y, (double)wheels[i].center.z,
(double)wheels[i].suspension_length, (double)wheels[i].rotation,
(unsigned)wheels[i].is_in_contact);
}

The state of the wheels is given by the wheels method of the controller (or by its wheel method for a single wheel) as a list of Wheel objects. These are copies: modifying them doesn’t affect the vehicle. Each wheel gives its world-space center, the world-space directions of its suspension and axle, its rotation angle, the force of its suspension (wheel_suspension_force), and its raycast_info (a RayCastInfo): the length of its suspension (suspension_length) and its contact with the ground (is_in_contact, the collider touched ground_object, and the world-space contact point contact_point_ws and normal contact_normal_ws). The speed of the vehicle along its forward axis is given by the current_vehicle_speed property of the controller (in meters per second, or with the current_speed_km_hour method in kilometers per hour).

# The wheels are copies of the state of the wheels after the last update.
for wheel in vehicle.wheels():
contact = wheel.raycast_info
print(
"Wheel center:", wheel.center, # World-space center of the wheel.
"axle:", wheel.axle, # World-space direction of its axle.
"rotation:", wheel.rotation, # Rotation angle around its axle.
"suspension length:", contact.suspension_length,
"touches the ground:", contact.is_in_contact,
"ground collider:", contact.ground_object, # None if it doesn’t touch the ground.
)