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.
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 WheelTuning 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 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.
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)
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 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.
)