Skip to main content

bevy_rapier3d/plugin/systems/
vehicle_controller.rs

1use 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
15/// Rebuilds the Rapier vehicle if its wheel count no longer matches the component's wheels.
16fn 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
83/// System responsible for updating the [`RayCastVehicleController`]s before the simulation
84/// step, and writing back their wheels' state.
85pub 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            // The wheels' rays start inside the chassis, so it must never be hit.
148            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        // Let the vehicle settle on its suspension.
224        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        // Accelerate with the front wheels.
249        {
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}