bevy_rapier3d/control/vehicle_controller.rs
1use crate::control::ControllerFilterPredicate;
2use crate::geometry::CollisionGroups;
3use crate::math::{Real, Vect};
4use crate::reflect::WheelTuningWrapper;
5use bevy::ecs::entity::EntityHashSet;
6use bevy::prelude::*;
7use rapier::control::DynamicRayCastVehicleController;
8use rapier::prelude::QueryFilterFlags;
9
10pub use rapier::control::WheelTuning;
11
12/// The state of a [`VehicleWheel`] computed by the plugin at the last vehicle update.
13///
14/// World-space quantities are expressed relative to the chassis pose before the last
15/// simulation step.
16#[derive(Copy, Clone, Debug, Default, PartialEq, Reflect)]
17#[reflect(Default, PartialEq, Debug)]
18pub struct VehicleWheelState {
19 /// The wheel's current rotation angle (in radians) around its axle.
20 ///
21 /// This increases when the vehicle rolls along the wheel's forward direction
22 /// (`contact_normal × axle`).
23 pub rotation: Real,
24 /// The current length of the wheel's suspension.
25 pub suspension_length: Real,
26 /// Is the wheel in contact with the ground?
27 pub is_in_contact: bool,
28 /// The world-space point hit by the wheel's ray-cast.
29 pub contact_point: Vect,
30 /// The world-space contact normal between the wheel and the ground.
31 pub contact_normal: Vect,
32 /// The world-space starting point of the wheel's ray-cast.
33 pub hard_point: Vect,
34 /// The world-space center of the wheel.
35 pub center: Vect,
36 /// The world-space direction of the wheel's suspension.
37 pub suspension_direction: Vect,
38 /// The world-space direction of the wheel's axle, including steering.
39 pub axle_direction: Vect,
40 /// The collider entity hit by the wheel's ray-cast, if any.
41 pub ground_entity: Option<Entity>,
42 /// The force applied by the suspension.
43 pub suspension_force: Real,
44 /// The forward impulse applied by the wheel on the chassis.
45 pub forward_impulse: Real,
46 /// The side impulse applied by the wheel on the chassis.
47 pub side_impulse: Real,
48}
49
50/// A wheel of a [`RayCastVehicleController`].
51///
52/// The description fields (connection point, directions, lengths, tuning) and the controls
53/// (engine force, brake, steering) can be modified at any time. The [`Self::state`] is
54/// written by the plugin at each vehicle update.
55#[derive(Copy, Clone, Debug, PartialEq, Reflect)]
56#[reflect(Default, PartialEq, Debug)]
57pub struct VehicleWheel {
58 /// The position of the wheel's suspension attachment, in the chassis' local space.
59 pub chassis_connection_point: Vect,
60 /// The direction of the wheel's suspension, in the chassis' local space.
61 ///
62 /// The ray-cast detecting the ground follows this direction.
63 pub direction: Vect,
64 /// The wheel's axle axis, in the chassis' local space.
65 pub axle: Vect,
66 /// The rest length of the wheel's suspension spring.
67 pub suspension_rest_length: Real,
68 /// The wheel's radius.
69 pub radius: Real,
70 /// Parameters affecting the physical behavior of the wheel.
71 #[reflect(remote = WheelTuningWrapper)]
72 pub tuning: WheelTuning,
73 /// The forward force applied by this wheel on the chassis.
74 pub engine_force: Real,
75 /// The maximum braking impulse applied by this wheel to slow down the vehicle.
76 pub brake: Real,
77 /// The steering angle (in radians) of this wheel, around the suspension direction.
78 pub steering: Real,
79 /// The state of the wheel, written back by the plugin.
80 pub state: VehicleWheelState,
81}
82
83impl Default for VehicleWheel {
84 fn default() -> Self {
85 Self {
86 chassis_connection_point: Vect::ZERO,
87 direction: -Vect::Y,
88 axle: Vect::Z,
89 suspension_rest_length: 0.5,
90 radius: 0.5,
91 tuning: WheelTuning::default(),
92 engine_force: 0.0,
93 brake: 0.0,
94 steering: 0.0,
95 state: VehicleWheelState::default(),
96 }
97 }
98}
99
100impl VehicleWheel {
101 /// Creates a wheel with the given geometry, the given tuning, and no engine force,
102 /// brake, or steering.
103 pub fn new(
104 chassis_connection_point: Vect,
105 direction: Vect,
106 axle: Vect,
107 suspension_rest_length: Real,
108 radius: Real,
109 tuning: WheelTuning,
110 ) -> Self {
111 Self {
112 chassis_connection_point,
113 direction,
114 axle,
115 suspension_rest_length,
116 radius,
117 tuning,
118 ..Default::default()
119 }
120 }
121
122 /// The wheel's transform relative to the chassis, for rendering.
123 ///
124 /// The translation is the wheel center at the current suspension length, and the rotation
125 /// combines steering (around the suspension direction) and spin (around the axle).
126 pub fn local_transform(&self) -> Transform {
127 let translation =
128 self.chassis_connection_point + self.direction * self.state.suspension_length;
129 let steering = Quat::from_axis_angle(-self.direction.normalize_or_zero(), self.steering);
130 // Rolling forward along `normal × axle` is a negative rotation around the axle.
131 let spin = Quat::from_axis_angle(self.axle.normalize_or_zero(), -self.state.rotation);
132 Transform::from_translation(translation).with_rotation(steering * spin)
133 }
134
135 pub(crate) fn sync_to_raw(&self, raw: &mut rapier::control::Wheel) {
136 raw.chassis_connection_point_cs = self.chassis_connection_point;
137 raw.direction_cs = self.direction;
138 raw.axle_cs = self.axle;
139 raw.suspension_rest_length = self.suspension_rest_length;
140 raw.radius = self.radius;
141 raw.suspension_stiffness = self.tuning.suspension_stiffness;
142 raw.damping_compression = self.tuning.suspension_compression;
143 raw.damping_relaxation = self.tuning.suspension_damping;
144 raw.max_suspension_travel = self.tuning.max_suspension_travel;
145 raw.side_friction_stiffness = self.tuning.side_friction_stiffness;
146 raw.friction_slip = self.tuning.friction_slip;
147 raw.max_suspension_force = self.tuning.max_suspension_force;
148 raw.engine_force = self.engine_force;
149 raw.brake = self.brake;
150 raw.steering = self.steering;
151 }
152}
153
154/// A vehicle controller simulating wheels with ray-casts, attached to the chassis rigid-body.
155///
156/// Before each simulation step, the plugin casts one ray per wheel along its suspension
157/// direction, then applies suspension, engine, braking and friction impulses to the chassis,
158/// which must be a dynamic rigid-body on the same entity. Colliders attached to the chassis
159/// rigid-body are always ignored by the ray-casts. The results are written back into each
160/// wheel's [`VehicleWheel::state`] and into [`Self::current_vehicle_speed`].
161///
162/// The impulses are computed once per plugin update using the last simulation timestep, so
163/// this is best used with a fixed timestep.
164#[derive(Clone, Debug, Component, Reflect)]
165#[reflect(Component, Default, Debug)]
166pub struct RayCastVehicleController {
167 /// The wheels of this vehicle.
168 ///
169 /// Wheels can be added, removed or modified at any time.
170 pub wheels: Vec<VehicleWheel>,
171 /// The chassis' local up axis (`0 = x, 1 = y, 2 = z`).
172 pub index_up_axis: usize,
173 /// The chassis' local forward axis (`0 = x, 1 = y, 2 = z`).
174 ///
175 /// This is only used to determine the sign of [`Self::current_vehicle_speed`].
176 pub index_forward_axis: usize,
177 /// Flags for excluding some categories of colliders from the wheels' ray-casts.
178 #[reflect(ignore)]
179 pub filter_flags: QueryFilterFlags,
180 /// Groups for excluding some colliders from the wheels' ray-casts.
181 pub filter_groups: Option<CollisionGroups>,
182 /// Colliders (identified by their entity) ignored by the wheels' ray-casts.
183 pub exclude_colliders: EntityHashSet,
184 /// Rigid-bodies (identified by their entity) whose colliders are all ignored by the
185 /// wheels' ray-casts.
186 pub exclude_rigid_bodies: EntityHashSet,
187 /// If set, any collider for which this predicate returns `false` is ignored by the wheels'
188 /// ray-casts. The colliders with a [`ControllerIgnored`](crate::control::ControllerIgnored)
189 /// component (or attached to a rigid-body with one) are always ignored.
190 #[reflect(ignore)]
191 pub filter_predicate: Option<ControllerFilterPredicate>,
192 /// The current forward speed of the vehicle, written back by the plugin.
193 ///
194 /// It is negative if the vehicle moves backward along its forward axis.
195 pub current_vehicle_speed: Real,
196 /// The underlying Rapier controller, created and synchronized automatically by the plugin.
197 ///
198 /// Its wheels are overwritten from [`Self::wheels`] before each update.
199 #[reflect(ignore)]
200 pub raw: Option<DynamicRayCastVehicleController>,
201}
202
203impl Default for RayCastVehicleController {
204 fn default() -> Self {
205 Self {
206 wheels: vec![],
207 index_up_axis: 1,
208 index_forward_axis: 0,
209 filter_flags: QueryFilterFlags::EXCLUDE_SENSORS,
210 filter_groups: None,
211 exclude_colliders: EntityHashSet::default(),
212 exclude_rigid_bodies: EntityHashSet::default(),
213 filter_predicate: None,
214 current_vehicle_speed: 0.0,
215 raw: None,
216 }
217 }
218}
219
220impl RayCastVehicleController {
221 /// Creates a vehicle controller with the given wheels.
222 pub fn new(wheels: Vec<VehicleWheel>) -> Self {
223 Self {
224 wheels,
225 ..Default::default()
226 }
227 }
228
229 /// Adds a wheel to this vehicle.
230 pub fn add_wheel(&mut self, wheel: VehicleWheel) -> &mut Self {
231 self.wheels.push(wheel);
232 self
233 }
234
235 /// Is any wheel of this vehicle in contact with the ground?
236 pub fn any_wheel_in_contact(&self) -> bool {
237 self.wheels.iter().any(|w| w.state.is_in_contact)
238 }
239}