Car controller for Bevy and Avian3d 0.6.1
3

Configure Feed

Select the types of activity you want to include in your feed.

add optional not working sphere casts

Matthew Blair (May 20, 2026, 11:46 AM +1000) 3451191b 328951d1

+410 -113
+323 -104
examples/two.rs
··· 4 4 use bevy_hotpatching_experiments::prelude::*; 5 5 6 6 use avian3d::prelude::{ 7 - Collider, Forces, RayHitData, ReadRigidBodyForces, RigidBody, RigidBodyQueryReadOnly, 8 - SpatialQuery, SpatialQueryFilter, WriteRigidBodyForces, 7 + Collider, ComputedMass, Forces, PhysicsStepSystems, RayHitData, ReadRigidBodyForces, RigidBody, 8 + RigidBodyQueryReadOnly, ShapeCastConfig, ShapeCaster, ShapeHits, SpatialQuery, 9 + SpatialQueryFilter, WriteRigidBodyForces, 9 10 }; 10 11 use bevy::{ 11 - asset::RenderAssetUsages, 12 - color::palettes::css::*, 12 + color::palettes::{css::*, tailwind::CYAN_500}, 13 13 image::{ImageAddressMode, ImageLoaderSettings, ImageSampler, ImageSamplerDescriptor}, 14 14 input::common_conditions::input_just_released, 15 15 light::NotShadowCaster, 16 16 math::Affine2, 17 - mesh::{Extrudable, Indices, PerimeterSegment, PrimitiveTopology, VertexAttributeValues}, 18 17 prelude::*, 19 18 }; 20 19 21 20 use avian_car::{ 22 21 AvianCarPlugin, Car, CarCamera, CarVisual, 23 - cars::car_two_bundle, 22 + cars::{SHAPE_CAST_WHEEL_SPHERE_RADIUS, Wheels, camera_bundle, car_two_bundle}, 24 23 common_example::{PROTOTYPE_TEXTURE, PROTOTYPE_TEXTURE2, setup_common}, 25 24 controls::{Flip, Steer, Throttle}, 26 25 }; ··· 28 27 use rand::{Rng, SeedableRng}; 29 28 use rand_chacha::ChaCha8Rng; 30 29 30 + const REST_LENGTH: f32 = 0.85; 31 + const STRENGTH: f32 = 5.0; 32 + const DAMPING: f32 = 1.0; 33 + 34 + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] 35 + #[states(scoped_entities)] 36 + enum GameState { 37 + #[default] 38 + A, 39 + B, 40 + } 41 + 42 + #[cfg_attr(feature = "hot", hot)] 31 43 fn main() { 32 44 let mut app = App::new(); 33 45 34 46 app.add_plugins(DefaultPlugins) 35 47 .add_plugins(AvianCarPlugin) 48 + .init_state::<GameState>() 36 49 .init_gizmo_group::<CarGizmos>() 37 50 .init_resource::<CarInfo>() 51 + .add_systems(OnEnter(GameState::A), new_game) 38 52 .add_systems( 39 - Startup, 53 + OnEnter(GameState::B), 40 54 (setup_common, setup_car, setup_two, setup_instructions), 41 55 ) 42 56 .add_systems( 43 57 Update, 44 - (|mut player_visibility: Single<&mut Visibility, With<CarVisual>>| { 45 - player_visibility.toggle_inherited_hidden(); 46 - }) 47 - .run_if(input_just_released(KeyCode::KeyC)), 58 + ( 59 + (|mut player_visibility: Single<&mut Visibility, With<CarVisual>>| { 60 + player_visibility.toggle_inherited_hidden(); 61 + }) 62 + .run_if(input_just_released(KeyCode::Digit1)) 63 + .run_if(in_state(GameState::B)), 64 + (|mut car_info: ResMut<CarInfo>| { 65 + car_info.use_sphere_cast = !car_info.use_sphere_cast; 66 + }) 67 + .run_if(input_just_released(KeyCode::Digit2)) 68 + .run_if(in_state(GameState::B)), 69 + ), 70 + ) 71 + .add_systems(Update, handle_escape_reload.run_if(in_state(GameState::B))) 72 + .add_systems( 73 + FixedUpdate, 74 + (update_debug_text, print_hits).run_if(in_state(GameState::B)), 48 75 ) 49 - .add_systems(FixedUpdate, update_debug_text) 50 76 .add_systems( 51 77 FixedUpdate, 52 - (player_off_the_ground, player_move, car_gizmo_render).chain(), 78 + ( 79 + car_update_ground, 80 + player_move, 81 + player_move_force, 82 + car_torque_alignment, 83 + car_gizmo_render, 84 + ) 85 + .chain() 86 + .before(PhysicsStepSystems::Solver) 87 + .run_if(in_state(GameState::B)), 53 88 ) 54 - .add_systems(Update, toggle_system) 55 89 .add_observer(flip_car); 56 90 57 91 #[cfg(feature = "hot")] ··· 66 100 pub struct CarInfo { 67 101 ground: Vec3, 68 102 is_grounded: bool, 103 + use_sphere_cast: bool, 69 104 } 70 105 71 - const WHEEL_OFFSETS: [Vec3; 4] = [ 72 - Vec3::new(0.7, -0.3, 1.2), // front-right 73 - Vec3::new(-0.7, -0.3, 1.2), // front-left 74 - Vec3::new(0.7, -0.3, -1.2), // rear-right 75 - Vec3::new(-0.7, -0.3, -1.2), // rear-left 76 - ]; 77 - 78 106 #[derive(Default, Reflect, GizmoConfigGroup)] 79 107 pub struct CarGizmos; 80 108 109 + fn new_game(current_state: Res<State<GameState>>, mut next_state: ResMut<NextState<GameState>>) { 110 + if *current_state.get() == GameState::A { 111 + next_state.set(GameState::B); 112 + } 113 + } 114 + 115 + fn handle_escape_reload( 116 + input: Res<ButtonInput<KeyCode>>, 117 + current_state: Res<State<GameState>>, 118 + mut next_state: ResMut<NextState<GameState>>, 119 + ) { 120 + if input.just_pressed(KeyCode::Escape) { 121 + if *current_state.get() == GameState::B { 122 + next_state.set(GameState::A); 123 + } 124 + } 125 + } 126 + 81 127 fn _debug_texture_samplers(materials: Res<Assets<StandardMaterial>>, images: Res<Assets<Image>>) { 82 128 for (_id, material) in materials.iter() { 83 129 if let Some(texture_handle) = &material.base_color_texture { ··· 91 137 } 92 138 } 93 139 140 + #[cfg_attr(feature = "hot", hot)] 94 141 fn setup_car( 95 142 mut commands: Commands, 96 143 mut meshes: ResMut<Assets<Mesh>>, 97 144 mut materials: ResMut<Assets<StandardMaterial>>, 98 145 ) { 99 - commands.spawn(car_two_bundle(&mut meshes, &mut materials)); 146 + commands.spawn((DespawnOnExit(GameState::B), camera_bundle())); 147 + commands.spawn(( 148 + DespawnOnExit(GameState::B), 149 + car_two_bundle(&mut meshes, &mut materials), 150 + )); 100 151 } 101 152 102 153 fn rand_color(rng: &mut ChaCha8Rng) -> Color { ··· 107 158 ) 108 159 } 109 160 110 - #[cfg(feature = "hot")] 111 - #[hot] 161 + #[cfg_attr(feature = "hot", hot)] 112 162 fn setup_two( 113 163 mut commands: Commands, 114 164 mut meshes: ResMut<Assets<Mesh>>, 115 165 mut materials: ResMut<Assets<StandardMaterial>>, 116 166 asset_server: Res<AssetServer>, 167 + mut config_store: ResMut<GizmoConfigStore>, 117 168 ) { 118 169 let mut rng = ChaCha8Rng::seed_from_u64(42); 119 170 120 - commands.spawn(debug_text_bundle()); 171 + let (config, _) = config_store.config_mut::<CarGizmos>(); 172 + config.depth_bias = -1.0; 173 + 174 + commands.spawn((DespawnOnExit(GameState::B), debug_text_bundle())); 121 175 122 176 // cube big flat 123 177 commands.spawn(( 178 + DespawnOnExit(GameState::B), 124 179 Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 125 180 MeshMaterial3d(materials.add(StandardMaterial { 126 181 base_color: rand_color(&mut rng), ··· 141 196 unlit: true, 142 197 ..default() 143 198 })), 144 - Transform::from_xyz(-10.0, 1.0, 0.0).with_scale(Vec3::new(10.0, 2.0, 10.0)), 199 + Transform::from_xyz(-16.0, 1.0, 0.0).with_scale(Vec3::new(10.0, 2.0, 10.0)), 145 200 NotShadowCaster, 146 201 Collider::cuboid(1.0, 1.0, 1.0), 147 202 RigidBody::Static, ··· 150 205 // cube bumps 151 206 for i in 0..5 { 152 207 commands.spawn(( 208 + DespawnOnExit(GameState::B), 153 209 Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 154 210 MeshMaterial3d(materials.add(StandardMaterial { 155 211 base_color: rand_color(&mut rng), ··· 174 230 unlit: true, 175 231 ..default() 176 232 })), 177 - Transform::from_xyz(0.0, 0.7, 5.0 + i as f32 + 0.1).with_scale(Vec3::new( 233 + Transform::from_xyz(0.0, 0.0, 5.0 + i as f32 + 0.1).with_scale(Vec3::new( 178 234 3.0, 179 - 1.0 + (i as f32 * 0.1), 235 + 0.2 + (i as f32 * 0.15), 180 236 0.5, 181 237 )), 182 238 NotShadowCaster, ··· 187 243 188 244 let vertices = vec![ 189 245 Vec2::new(0.0, 0.0), // Right angle corner at origin 190 - Vec2::new(2.0, 0.0), // Base corner 191 - Vec2::new(0.0, 1.5), // Top corner 246 + Vec2::new(3.0, 0.0), // Base corner 247 + Vec2::new(0.0, 1.0), // Top corner 192 248 ]; 193 249 194 250 // Create a convex polygon from the vertices 195 251 let triangle = ConvexPolygon::new(vertices).expect("Triangle is convex"); 196 - let extrusion = Extrusion::new(triangle, 2.5); 252 + let extrusion = Extrusion::new(triangle, 0.5); 197 253 let ramp_collider = Collider::convex_decomposition_from_mesh(&extrusion.mesh().build()) 198 254 .expect("unable to build ramp collider"); 199 255 200 256 commands.spawn(( 257 + DespawnOnExit(GameState::B), 201 258 Mesh3d(meshes.add(extrusion)), 202 259 MeshMaterial3d(materials.add(StandardMaterial { 203 260 base_color: rand_color(&mut rng), ··· 206 263 ..default() 207 264 })), 208 265 Transform::default() 209 - .with_scale(Vec3::new(1.4, 1.0, 5.0)) 210 - .with_translation(Vec3::ZERO.with_x(-4.0)), 266 + .with_scale(Vec3::new(2.4, 2.0, 10.0)) 267 + .with_translation(Vec3::ZERO.with_x(-8.0)), 211 268 ramp_collider, 212 269 RigidBody::Static, 213 270 )); 214 271 } 215 272 216 273 fn setup_instructions(mut commands: Commands) { 217 - commands.spawn((Text::new("Press Spacebar to Toggle Atmospheric Fog.\nPress S to Toggle Directional Light Fog Influence."), 274 + commands.spawn(( 275 + Text::new( 276 + "Press F to flip the car\nPress 1 to hide car visuals\nPress 2 to use sphere casts", 277 + ), 278 + TextFont { 279 + font_size: 12.0, 280 + ..default() 281 + }, 218 282 Node { 219 283 position_type: PositionType::Absolute, 220 284 bottom: px(12), 221 285 left: px(12), 222 286 ..default() 223 - }) 224 - ); 225 - } 226 - 227 - fn toggle_system(keycode: Res<ButtonInput<KeyCode>>, mut fog: Single<&mut DistanceFog>) { 228 - if keycode.just_pressed(KeyCode::Space) { 229 - let a = fog.color.alpha(); 230 - fog.color.set_alpha(1.0 - a); 231 - } 232 - 233 - if keycode.just_pressed(KeyCode::KeyS) { 234 - let a = fog.directional_light_color.alpha(); 235 - fog.directional_light_color.set_alpha(0.5 - a); 236 - } 287 + }, 288 + )); 237 289 } 238 290 239 291 fn flip_car(_event: On<Fire<Flip>>, mut transform: Single<&mut Transform, With<Car>>) { ··· 242 294 } 243 295 244 296 ///Move the player (CarCamera) around the scene 297 + #[cfg_attr(feature = "hot", hot)] 245 298 pub fn player_move( 246 299 movement: Query<&Action<Steer>>, 247 300 throttle: Query<&Action<Throttle>>, 248 301 mut camera_transform: Single<&mut Transform, With<CarCamera>>, 249 - car: Single<Forces, With<Car>>, 302 + car: Single<(Forces, &ComputedMass), With<Car>>, 250 303 mut car_gizmos: Gizmos<CarGizmos>, 304 + time: Res<Time>, 251 305 ) { 252 306 let steering = movement 253 307 .iter() ··· 256 310 .unwrap_or(Vec2::splat(0.0)); 257 311 let throttle = throttle.iter().next().map(|t| **t).unwrap_or(0.0); 258 312 259 - let mut car = car.into_inner(); 313 + let (mut car, mass) = car.into_inner(); 260 314 261 - const SPEED: f32 = 12.0; 315 + const MAX_SPEED: f32 = 12.0; 316 + const MAX_FORWARD_FORCE: f32 = 500.0; // Maximum engine power 317 + const MAX_LATERAL_GRIP: f32 = 15.0; // Lower = driftier, Higher = turns on rails 262 318 // move and steer the player car 263 319 let rotation = Quat::from_rotation_y(-steering.x * 0.05); 264 320 camera_transform.rotate_around(Vec3::ZERO, rotation); 265 - let forward = (camera_transform.rotation * Vec3::NEG_Z).with_y(0.0); 266 - car_gizmos.ray(car.position().0, forward, WHITE); 267 - car.apply_force(forward * SPEED * throttle); 321 + 322 + let forward = (camera_transform.rotation * Vec3::NEG_Z) 323 + .with_y(0.0) 324 + .normalize(); 325 + let right = (camera_transform.rotation * Vec3::NEG_X) 326 + .with_y(0.0) 327 + .normalize(); 328 + 329 + camera_transform.translation = car.position().0 - (forward * 7.0) + (Vec3::Y * 3.0); 330 + 331 + let current_velocity = car.linear_velocity(); 332 + let dt = time.delta_secs(); 333 + let car_mass = mass.value() * 0.5; 334 + if dt > 0.0 { 335 + // ----------------------------------------------------------------- 336 + // LONGITUDINAL LOOP (Forward Acceleration / Braking) 337 + // ----------------------------------------------------------------- 338 + let current_forward_speed = current_velocity.dot(forward); 339 + let desired_forward_speed = throttle * MAX_SPEED; 340 + 341 + // Calculate required force: F = m * a where a = (v_target - v_current) / dt 342 + let required_forward_accel = (desired_forward_speed - current_forward_speed) / dt; 343 + let raw_forward_force = required_forward_accel * car_mass; 344 + 345 + // Clamp the engine force scalar, then apply it along the forward vector 346 + let clamped_forward_force = raw_forward_force.clamp(-MAX_FORWARD_FORCE, MAX_FORWARD_FORCE); 347 + let final_forward_force_vec = forward * clamped_forward_force; 348 + 349 + // ----------------------------------------------------------------- 350 + // LATERAL LOOP (Artificial Traction / Sideways Friction) 351 + // ----------------------------------------------------------------- 352 + let current_sideways_speed = current_velocity.dot(right); 353 + let desired_sideways_speed = 0.0; // We want 0 lateral slide for perfect traction 354 + 355 + // Calculate required counter-force to kill sideways velocity 356 + let required_lateral_accel = (desired_sideways_speed - current_sideways_speed) / dt; 357 + let raw_lateral_force = required_lateral_accel * car_mass; 358 + 359 + // Clamp by your grip threshold. If the slide force exceeds this, the car drifts! 360 + let clamped_lateral_force = raw_lateral_force.clamp(-MAX_LATERAL_GRIP, MAX_LATERAL_GRIP); 361 + let final_lateral_force_vec = right * clamped_lateral_force; 362 + 363 + // ----------------------------------------------------------------- 364 + // COMBINE AND APPLY FORCES 365 + // ----------------------------------------------------------------- 366 + let total_force = final_forward_force_vec + final_lateral_force_vec; 367 + car.apply_force(total_force); 368 + } 369 + 370 + // Render some velocity gizmos 371 + car_gizmos.ray(car.position().0, forward * MAX_SPEED * throttle, WHITE); 372 + car_gizmos.ray( 373 + car.position().0, 374 + Vec3::ZERO.with_z(forward.z) * MAX_SPEED * throttle, 375 + BLUE, 376 + ); 377 + car_gizmos.ray( 378 + car.position().0, 379 + Vec3::ZERO.with_x(forward.x) * MAX_SPEED * throttle, 380 + RED, 381 + ); 382 + } 383 + 384 + /// Calculates the signed angle (in radians) from `a` to `b` around a reference `normal`. 385 + /// Returns a value between -PI and +PI. 386 + fn signed_angle_to(a: Vec3, b: Vec3, normal: Vec3) -> f32 { 387 + let n = normal.normalize(); 388 + let cross = a.cross(b); 389 + let sign = cross.dot(n); 390 + let cos = a.dot(b); 391 + f32::atan2(sign, cos) 392 + } 393 + 394 + fn signed_angle_y(a: Vec3, b: Vec3) -> f32 { 395 + signed_angle_to(a, b, Vec3::Y) 268 396 } 269 397 270 - pub fn car_gizmo_render( 271 - player_transform: Single<&mut Transform, With<Car>>, 272 - car_info: Res<CarInfo>, 273 - mut car_gizmos: Gizmos<CarGizmos>, 398 + #[cfg_attr(feature = "hot", hot)] 399 + pub fn car_torque_alignment( 400 + camera_query: Single<&Transform, With<CarCamera>>, 401 + mut car: Single<Forces, With<Car>>, 274 402 ) { 275 - let ground = Isometry3d::from_translation(car_info.ground); 276 - trace!("ground: {ground:?}, info: {car_info:?}"); 277 - car_gizmos 278 - .circle( 279 - ground * Isometry3d::from_rotation(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)), 280 - 1.1, 281 - NAVY, 282 - ) 283 - .resolution(64); 284 - car_gizmos.axes(**player_transform, 2.0); 403 + const ALIGNMENT_TORQUE_STRENGTH: f32 = 8.0; 404 + const ALIGNMENT_TORQUE_MAX: f32 = 40.0; 405 + 406 + let camera_transform = *camera_query; 407 + let car_rotation = car.rotation(); 408 + 409 + let camera_forward = (camera_transform.rotation * Vec3::NEG_Z) 410 + .with_y(0.0) 411 + .normalize_or_zero(); 412 + let car_forward = (car_rotation * Vec3::NEG_Z).with_y(0.0).normalize_or_zero(); 413 + 414 + let angle_offset = signed_angle_y(car_forward, camera_forward); 415 + 416 + let turn_force = angle_offset * ALIGNMENT_TORQUE_STRENGTH; // use angular damping avian component 417 + 418 + let turn_force_mag = turn_force.clamp(-ALIGNMENT_TORQUE_MAX, ALIGNMENT_TORQUE_MAX); 419 + 420 + // --- DIAGNOSTIC --- 421 + // info!("angle_offset: {angle_offset:?}, turn force mag: {turn_force_mag:?}"); 422 + 423 + let torque_force_mag = Vec3::Y * turn_force_mag; 424 + 425 + car.apply_torque(Vec3::Y * torque_force_mag); 285 426 } 286 427 287 - #[cfg(feature = "hot")] 288 - #[hot] 289 - pub fn player_off_the_ground( 290 - player: Single<&mut Transform, With<Car>>, 428 + #[cfg_attr(feature = "hot", hot)] 429 + pub fn player_move_force( 291 430 spatial_query: SpatialQuery, 292 431 mut car_info: ResMut<CarInfo>, 293 432 mut car_gizmos: Gizmos<CarGizmos>, 294 433 forces_q: Single<Forces, With<Car>>, 434 + mut wheels_q: Query<(&mut Transform, &Wheels, &ShapeHits), With<Wheels>>, 295 435 ) { 296 - const REST_LENGTH: f32 = 0.85; 297 - const STRENGTH: f32 = 5.0; 298 - const DAMPING: f32 = 1.0; 299 - 300 - let world_pos = player.translation; 301 436 let mut forces = forces_q.into_inner(); 302 - let ray_dir = player.rotation * Dir3::NEG_Y; 437 + let world_pos = forces.position().0; 438 + let player_rotation = forces.rotation().0; 439 + let ray_dir = player_rotation * Dir3::NEG_Y; 303 440 let mut hits = 0; 304 441 305 - for offset in WHEEL_OFFSETS { 306 - let world_offset = player.rotation * offset; 442 + for (mut wheel_transform, wheel, shape_hits) in wheels_q.iter_mut() { 443 + let offset = wheel.offset(); 444 + let world_offset = player_rotation * offset; 307 445 let ray_origin = world_pos + world_offset; 308 446 309 - if let Some(hit) = get_terrain_height(&spatial_query, ray_origin, ray_dir) { 447 + // shape_caster.origin = ray_origin; 448 + 449 + if let Some(hit) = get_terrain_height( 450 + &spatial_query, 451 + ray_origin, 452 + ray_dir, 453 + match car_info.use_sphere_cast { 454 + true => Some(shape_hits), 455 + false => None, 456 + }, 457 + ) { 310 458 if hit.distance < REST_LENGTH { 311 459 hits += 1; 312 460 } ··· 317 465 318 466 let spring_d = REST_LENGTH - hit.distance; 319 467 320 - // only push up, never pull down — remove this clamp if you want a 2-sided spring 321 - // if spring_d < 0.0 { 322 - // continue; 323 - // } 468 + // only push up, never pull down — remove this clamp for a 2-sided spring 469 + if spring_d < 0.0 { 470 + continue; 471 + } 324 472 325 473 let force_magnitude = (spring_d * STRENGTH) - (rel_vel * DAMPING); 326 474 327 475 if hit.distance < REST_LENGTH || hits != 0 { 328 476 forces.apply_force_at_point(Vec3::Y * force_magnitude, ray_origin); 329 477 } 478 + wheel_transform.translation.y = -(hit.distance.min(REST_LENGTH)); 330 479 331 480 car_gizmos.ray(ray_origin, Dir3::NEG_Y * hit.distance, ORANGE_RED); 332 - car_gizmos 333 - .circle( 334 - Isometry3d::from_translation(hit_point) 335 - * Isometry3d::from_rotation(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)), 336 - 0.1, 337 - RED, 338 - ) 339 - .resolution(64); 481 + if car_info.use_sphere_cast { 482 + car_gizmos 483 + .sphere(Isometry3d::from_translation(hit_point), 0.1, RED) 484 + .resolution(64); 485 + } else { 486 + car_gizmos 487 + .circle( 488 + Isometry3d::from_translation(hit_point) 489 + * Isometry3d::from_rotation(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)), 490 + 0.1, 491 + RED, 492 + ) 493 + .resolution(64); 494 + } 340 495 } else { 341 496 car_gizmos.ray(ray_origin, ray_dir * REST_LENGTH, AQUAMARINE); 342 497 } ··· 350 505 spatial_query: &SpatialQuery<'_, '_>, 351 506 ray_origin: Vec3, 352 507 ray_dir: Dir3, 508 + shape_hits: Option<&ShapeHits>, 353 509 ) -> Option<RayHitData> { 354 510 const RAYCAST_Z_DISTANCE: f32 = 10.0; 355 511 356 - let filter = SpatialQueryFilter::DEFAULT; //SpatialQueryFilter::from_mask(FILTER_MASK_GROUND_PLACEMENT); 357 - spatial_query.cast_ray(ray_origin, ray_dir, RAYCAST_Z_DISTANCE, true, &filter) 512 + let filter = SpatialQueryFilter::DEFAULT; 513 + if let Some(hits) = shape_hits { 514 + return hits.iter_sorted().next().map(|hit| RayHitData { 515 + entity: hit.entity, 516 + distance: hit.distance, 517 + normal: hit.normal1, 518 + }); 519 + } else { 520 + spatial_query.cast_ray(ray_origin, ray_dir, RAYCAST_Z_DISTANCE, true, &filter) 521 + } 522 + } 523 + 524 + pub fn car_update_ground( 525 + spatial_query: SpatialQuery, 526 + mut car_info: ResMut<CarInfo>, 527 + forces_q: Single<Forces, With<Car>>, 528 + ) { 529 + let ray_origin = forces_q.position().0; 530 + if let Some(hit) = get_terrain_height(&spatial_query, ray_origin, Dir3::NEG_Y, None) { 531 + let hit_point = ray_origin + Dir3::NEG_Y * hit.distance; 532 + car_info.ground = hit_point; 533 + } 534 + } 535 + 536 + pub fn car_gizmo_render( 537 + player_transform: Single<&mut Transform, With<Car>>, 538 + car_info: Res<CarInfo>, 539 + mut car_gizmos: Gizmos<CarGizmos>, 540 + ) { 541 + let player = Isometry3d::from_translation(player_transform.translation); 542 + let ground = Isometry3d::from_translation(car_info.ground); 543 + 544 + trace!("ground: {ground:?}, info: {car_info:?}"); 545 + 546 + car_gizmos 547 + .circle( 548 + ground * Isometry3d::from_rotation(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) * player, 549 + 1.1, 550 + NAVY, 551 + ) 552 + .resolution(64); 553 + car_gizmos.axes(**player_transform, 1.0); 358 554 } 359 555 360 556 #[derive(Debug, Clone, Copy, Component)] 361 557 pub struct DebugText; 362 558 559 + #[cfg_attr(feature = "hot", hot)] 363 560 pub fn debug_text_bundle() -> impl Bundle { 364 561 let mut cc = LIGHT_GRAY; 365 - cc.set_alpha(0.7); 562 + cc.set_alpha(0.4); 366 563 ( 367 564 Node { 368 565 position_type: PositionType::Absolute, 369 566 top: px(20.0), 370 567 left: px(20.0), 371 - 568 + padding: UiRect::all(px(6.0)), 569 + min_width: percent(20.0), 570 + max_width: percent(20.0), 372 571 ..default() 373 572 }, 374 573 BackgroundColor(cc.into()), 375 - Text::new("Gathering reflection data..."), 376 - TextFont { 377 - font_size: 20.0, 378 - ..default() 379 - }, 380 - TextColor(Color::WHITE), 381 - DebugText, 574 + children![( 575 + Text::new("Gathering reflection data..."), 576 + TextFont { 577 + font_size: 10.0, 578 + ..default() 579 + }, 580 + TextColor(Color::WHITE), 581 + DebugText, 582 + )], 382 583 ) 383 584 } 384 585 586 + #[cfg_attr(feature = "hot", hot)] 385 587 pub fn update_debug_text( 386 588 mut query: Query<&mut Text, With<DebugText>>, 387 589 car_physics: Single<RigidBodyQueryReadOnly, With<Car>>, ··· 392 594 **text = format!("Angular Velocity: {:?}", vel); 393 595 } 394 596 } 597 + 598 + fn print_hits( 599 + query: Query<(&ShapeCaster, &ShapeHits), With<Wheels>>, 600 + mut car_gizmos: Gizmos<CarGizmos>, 601 + ) { 602 + for (_shape_caster, hits) in query.iter() { 603 + for hit in hits.iter() { 604 + // car_gizmos.arrow(transform.translation, hit.point1, ORANGE); 605 + // car_gizmos.sphere(Isometry3d::from_translation(hit.point1), 0.2, PURPLE); 606 + car_gizmos.sphere( 607 + Isometry3d::from_translation(hit.point2), 608 + SHAPE_CAST_WHEEL_SPHERE_RADIUS, 609 + CYAN_500, 610 + ); 611 + } 612 + } 613 + }
+86 -8
src/cars.rs
··· 1 - use avian3d::prelude::{AngularDamping, Collider, LinearDamping, LinearVelocity, RigidBody}; 2 - use bevy::{color::palettes::css::RED, prelude::*}; 1 + use std::f32::consts::FRAC_PI_2; 2 + 3 + use avian3d::prelude::{AngularDamping, Collider, LinearDamping, RigidBody, ShapeCaster, SweptCcd}; 4 + use bevy::{ 5 + color::palettes::css::{BLACK, RED}, 6 + prelude::*, 7 + }; 8 + 9 + const WHEEL_RADIUS: f32 = 0.3; 10 + const WHEEL_HEIGHT: f32 = 0.22; 11 + pub const SHAPE_CAST_WHEEL_SPHERE_RADIUS: f32 = 0.1; 12 + 13 + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, Component)] 14 + pub enum Wheels { 15 + #[default] 16 + FL, 17 + FR, 18 + BL, 19 + BR, 20 + } 21 + 22 + impl Wheels { 23 + pub fn offset(self) -> Vec3 { 24 + const WHEEL_X: f32 = 0.6; 25 + const WHEEL_Z: f32 = 0.8; 26 + match self { 27 + Wheels::FL => Vec3::new(-WHEEL_X, -0.3, CAR_LENGTH - WHEEL_Z), 28 + Wheels::FR => Vec3::new(WHEEL_X, -0.3, CAR_LENGTH - WHEEL_Z), 29 + Wheels::BL => Vec3::new(-WHEEL_X, -0.3, -CAR_LENGTH + WHEEL_Z), 30 + Wheels::BR => Vec3::new(WHEEL_X, -0.3, -CAR_LENGTH + WHEEL_Z), 31 + } 32 + } 33 + } 34 + 35 + #[cfg(feature = "hot")] 36 + use bevy_hotpatching_experiments::prelude::*; 3 37 4 38 use crate::{ 5 39 Car, CarCamera, CarVisual, ··· 35 69 car_physics_bundle(), 36 70 children![ 37 71 car_visual_bundle(meshes, materials), // requires own tranform to offset 38 - camera_bundle(), // requires own transform to point at car 39 72 ], 40 73 ) 41 74 } 42 75 76 + #[cfg_attr(feature = "hot", hot)] 43 77 fn car_physics_bundle() -> impl Bundle { 44 78 const R: f32 = 0.2; 45 79 ( 46 80 Collider::round_cuboid(CAR_WIDTH - R, CAR_HEIGHT - R, CAR_LENGTH - R, R), 47 81 RigidBody::Dynamic, 48 - LinearVelocity::ZERO, 49 - AngularDamping(5.0), // tune up if still tumbling 50 - LinearDamping(0.5), 82 + AngularDamping(5.7), // TODO: use const or calculate 2 * sqrt(SPRING_FORCE) 83 + SweptCcd::LINEAR, 84 + // tune up if still tumbling 85 + LinearDamping(0.6), 51 86 ) 52 87 } 53 88 89 + #[cfg_attr(feature = "hot", hot)] 54 90 fn car_visual_bundle( 55 91 meshes: &mut ResMut<Assets<Mesh>>, 56 92 materials: &mut ResMut<Assets<StandardMaterial>>, 57 93 ) -> impl Bundle { 94 + let wheel_mesh = meshes.add(Cylinder::new(WHEEL_RADIUS, WHEEL_HEIGHT)); 95 + let wheel_material = materials.add(StandardMaterial { 96 + base_color: BLACK.into(), 97 + ..Default::default() 98 + }); 58 99 ( 59 100 CarVisual, 60 101 Mesh3d(meshes.add(Cuboid::new(CAR_WIDTH, CAR_HEIGHT, CAR_LENGTH))), ··· 62 103 base_color: RED.into(), 63 104 ..Default::default() 64 105 })), 106 + children![ 107 + (( 108 + Mesh3d(wheel_mesh.clone()), 109 + MeshMaterial3d(wheel_material.clone()), 110 + make_wheel(Wheels::FL) 111 + )), 112 + (( 113 + Mesh3d(wheel_mesh.clone()), 114 + MeshMaterial3d(wheel_material.clone()), 115 + make_wheel(Wheels::FR) 116 + )), 117 + (( 118 + Mesh3d(wheel_mesh.clone()), 119 + MeshMaterial3d(wheel_material.clone()), 120 + make_wheel(Wheels::BL) 121 + )), 122 + (( 123 + Mesh3d(wheel_mesh), 124 + MeshMaterial3d(wheel_material), 125 + make_wheel(Wheels::BR) 126 + )), 127 + ], 65 128 ) 66 129 } 67 130 68 - fn camera_bundle() -> impl Bundle { 131 + fn make_wheel(wheel: Wheels) -> impl Bundle { 132 + ( 133 + ShapeCaster::new( 134 + Collider::sphere(SHAPE_CAST_WHEEL_SPHERE_RADIUS), 135 + Vec3::ZERO, 136 + Quat::default(), 137 + Dir3::NEG_Y, 138 + ), 139 + wheel, 140 + Transform::from_translation(wheel.offset()) 141 + .with_rotation(Quat::from_axis_angle(Vec3::Z, FRAC_PI_2)), 142 + ) 143 + } 144 + 145 + #[cfg_attr(feature = "hot", hot)] 146 + pub fn camera_bundle() -> impl Bundle { 69 147 ( 70 148 Camera3d::default(), 71 - Transform::from_xyz(0.0, 3.4, -7.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), 149 + Transform::from_xyz(0.0, 7.4, -12.0).looking_at(Vec3::new(0.0, 0.0, 6.0), Vec3::Y), 72 150 DistanceFog { 73 151 color: Color::srgba(0.35, 0.48, 0.66, 1.0), 74 152 directional_light_color: Color::srgba(1.0, 0.95, 0.85, 0.5),
+1 -1
src/common_example.rs
··· 82 82 unlit: true, 83 83 ..default() 84 84 })), 85 - Transform::from_xyz(0.0, 1.0, 0.0), 85 + Transform::from_xyz(0.0, 0.0, 0.0), 86 86 NotShadowCaster, 87 87 RigidBody::Static, 88 88 Collider::cuboid(200.0, 0.1, 200.0),