bevy_rapier3d/plugin/systems/
vehicle_controller.rs1use crate::control::controller_filter::ControllerExclusions;
2use crate::control::ControllerIgnored;
3use crate::control::{RayCastVehicleController, VehicleWheel, VehicleWheelState};
4use crate::dynamics::RapierRigidBodyHandle;
5use crate::plugin::context::{
6 RapierContextColliders, RapierContextEntityLink, RapierContextSimulation, RapierRigidBodySet,
7};
8use crate::plugin::RapierConfiguration;
9use bevy::ecs::entity::EntityHashSet;
10use bevy::prelude::*;
11use rapier::control::{DynamicRayCastVehicleController, WheelTuning};
12use rapier::geometry::{Collider as RapierCollider, ColliderHandle, ColliderSet};
13use rapier::pipeline::QueryFilter;
14
15fn sync_raw_vehicle(
17 vehicle: &mut RayCastVehicleController,
18 chassis: rapier::dynamics::RigidBodyHandle,
19) {
20 let wheels_match = vehicle
21 .raw
22 .as_ref()
23 .is_some_and(|raw| raw.wheels().len() == vehicle.wheels.len());
24
25 if !wheels_match {
26 let mut raw = DynamicRayCastVehicleController::new(chassis);
27 for wheel in &vehicle.wheels {
28 let raw_wheel = raw.add_wheel(
29 wheel.chassis_connection_point,
30 wheel.direction,
31 wheel.axle,
32 wheel.suspension_rest_length,
33 wheel.radius,
34 &WheelTuning::default(),
35 );
36 raw_wheel.rotation = wheel.state.rotation;
37 }
38 vehicle.raw = Some(raw);
39 }
40
41 let RayCastVehicleController {
42 wheels,
43 index_up_axis,
44 index_forward_axis,
45 raw,
46 ..
47 } = vehicle;
48 let raw = raw.as_mut().expect("the raw vehicle was just created");
49 raw.chassis = chassis;
50 raw.index_up_axis = *index_up_axis;
51 raw.index_forward_axis = *index_forward_axis;
52
53 for (wheel, raw_wheel) in wheels.iter().zip(raw.wheels_mut()) {
54 wheel.sync_to_raw(raw_wheel);
55 }
56}
57
58fn read_wheel_state(
59 wheel: &mut VehicleWheel,
60 raw: &rapier::control::Wheel,
61 colliders: &ColliderSet,
62) {
63 let info = raw.raycast_info();
64 wheel.state = VehicleWheelState {
65 rotation: raw.rotation,
66 suspension_length: info.suspension_length,
67 is_in_contact: info.is_in_contact,
68 contact_point: info.contact_point_ws,
69 contact_normal: info.contact_normal_ws,
70 hard_point: info.hard_point_ws,
71 center: raw.center(),
72 suspension_direction: raw.suspension(),
73 axle_direction: raw.axle(),
74 ground_entity: info
75 .ground_object
76 .and_then(|h| RapierContextColliders::collider_entity_with_set(colliders, h)),
77 suspension_force: raw.wheel_suspension_force,
78 forward_impulse: raw.forward_impulse,
79 side_impulse: raw.side_impulse,
80 };
81}
82
83pub fn update_vehicle_controllers(
86 mut context_access: Query<(
87 &RapierContextSimulation,
88 &mut RapierContextColliders,
89 &mut RapierRigidBodySet,
90 &RapierConfiguration,
91 )>,
92 mut vehicles: Query<(
93 &RapierContextEntityLink,
94 &RapierRigidBodyHandle,
95 &mut RayCastVehicleController,
96 )>,
97 ignored: Query<Entity, With<ControllerIgnored>>,
98) {
99 let ignored: EntityHashSet = ignored.iter().collect();
100 for (link, handle, mut vehicle) in vehicles.iter_mut() {
101 let Ok((context, mut context_colliders, mut rigidbody_set, config)) =
102 context_access.get_mut(link.0)
103 else {
104 continue;
105 };
106 if !config.physics_pipeline_active {
107 continue;
108 }
109 let context_colliders = &mut *context_colliders;
110 let rigidbody_set = &mut *rigidbody_set;
111 if !rigidbody_set
112 .bodies
113 .get(handle.0)
114 .is_some_and(|body| body.is_enabled())
115 {
116 continue;
117 }
118
119 let vehicle = &mut *vehicle;
120 sync_raw_vehicle(vehicle, handle.0);
121
122 let RayCastVehicleController {
123 wheels,
124 filter_flags,
125 filter_groups,
126 exclude_colliders,
127 exclude_rigid_bodies,
128 filter_predicate,
129 current_vehicle_speed,
130 raw,
131 ..
132 } = vehicle;
133 let raw = raw.as_mut().expect("the raw vehicle was just synchronized");
134
135 let exclusions = ControllerExclusions::new(
136 exclude_colliders,
137 exclude_rigid_bodies,
138 &ignored,
139 filter_predicate.as_ref(),
140 rigidbody_set,
141 );
142 let predicate = |_: ColliderHandle, collider: &RapierCollider| exclusions.test(collider);
143 let filter = QueryFilter {
144 flags: *filter_flags,
145 groups: filter_groups.map(|g| g.into()),
146 exclude_collider: None,
147 exclude_rigid_body: Some(handle.0),
149 predicate: (!exclusions.is_empty())
150 .then_some(&predicate as &dyn Fn(ColliderHandle, &RapierCollider) -> bool),
151 };
152 let query_pipeline = context.broad_phase.as_query_pipeline_mut(
153 context.narrow_phase.query_dispatcher(),
154 &mut rigidbody_set.bodies,
155 &mut context_colliders.colliders,
156 filter,
157 );
158 raw.update_vehicle(context.integration_parameters.dt, query_pipeline);
159
160 *current_vehicle_speed = raw.current_vehicle_speed;
161 for (wheel, raw_wheel) in wheels.iter_mut().zip(raw.wheels()) {
162 read_wheel_state(wheel, raw_wheel, &context_colliders.colliders);
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::geometry::Collider;
171 use crate::plugin::{NoUserData, RapierPhysicsPlugin};
172 use crate::prelude::{RigidBody, Velocity};
173 use bevy::time::{TimePlugin, TimeUpdateStrategy};
174
175 fn vehicle_wheels(hw: f32, hh: f32) -> Vec<VehicleWheel> {
176 let tuning = WheelTuning {
177 suspension_stiffness: 100.0,
178 suspension_damping: 10.0,
179 ..WheelTuning::default()
180 };
181 [
182 Vec3::new(hw * 1.5, -hh, hw),
183 Vec3::new(hw * 1.5, -hh, -hw),
184 Vec3::new(-hw * 1.5, -hh, hw),
185 Vec3::new(-hw * 1.5, -hh, -hw),
186 ]
187 .into_iter()
188 .map(|pos| VehicleWheel::new(pos, -Vec3::Y, Vec3::Z, hh, hh / 4.0, tuning))
189 .collect()
190 }
191
192 #[test]
193 fn vehicle_rests_on_wheels_and_moves_forward() {
194 let mut app = App::new();
195 app.add_plugins((
196 TransformPlugin,
197 TimePlugin,
198 RapierPhysicsPlugin::<NoUserData>::default(),
199 ))
200 .insert_resource(TimeUpdateStrategy::ManualDuration(
201 std::time::Duration::from_secs_f32(1.0 / 60.0),
202 ));
203 app.finish();
204
205 app.world_mut().spawn((
206 Transform::from_xyz(0.0, -0.1, 0.0),
207 Collider::cuboid(50.0, 0.1, 50.0),
208 ));
209
210 let (hw, hh) = (0.3, 0.15);
211 let chassis = app
212 .world_mut()
213 .spawn((
214 Transform::from_xyz(0.0, 0.5, 0.0),
215 RigidBody::Dynamic,
216 Collider::cuboid(hw * 2.0, hh, hw),
217 crate::geometry::ColliderMassProperties::Density(100.0),
218 Velocity::zero(),
219 RayCastVehicleController::new(vehicle_wheels(hw, hh)),
220 ))
221 .id();
222
223 for _ in 0..120 {
225 app.update();
226 }
227
228 let vehicle = app
229 .world()
230 .get::<RayCastVehicleController>(chassis)
231 .unwrap();
232 assert!(
233 vehicle.wheels.iter().all(|w| w.state.is_in_contact),
234 "All the wheels should touch the ground: {:?}",
235 vehicle.wheels
236 );
237 assert!(vehicle
238 .wheels
239 .iter()
240 .all(|w| w.state.ground_entity.is_some()));
241 let rest_height = app.world().get::<Transform>(chassis).unwrap().translation.y;
242 assert!(
243 rest_height > hh + 0.05,
244 "The chassis should be held above the ground by the suspension"
245 );
246 let start_x = app.world().get::<Transform>(chassis).unwrap().translation.x;
247
248 {
250 let mut vehicle = app
251 .world_mut()
252 .get_mut::<RayCastVehicleController>(chassis)
253 .unwrap();
254 vehicle.wheels[0].engine_force = 30.0;
255 vehicle.wheels[1].engine_force = 30.0;
256 }
257 for _ in 0..60 {
258 app.update();
259 }
260
261 let end_x = app.world().get::<Transform>(chassis).unwrap().translation.x;
262 let vehicle = app
263 .world()
264 .get::<RayCastVehicleController>(chassis)
265 .unwrap();
266 assert!(
267 end_x > start_x + 0.1,
268 "The vehicle should move forward: {start_x} -> {end_x}"
269 );
270 assert!(vehicle.current_vehicle_speed > 0.0);
271 assert!(vehicle.wheels[0].state.rotation > 0.0);
272 assert!(vehicle.any_wheel_in_contact());
273 }
274}