use bevy::{ color::palettes::css::{BLUE, GREEN}, render::mesh::{SphereKind, SphereMeshBuilder}, }; use bevy_rapier3d::rapier::prelude::CollisionEventFlags; use games::*; /// Example showing using Rapier3d to do area intersection /// /// Have you ever wanted to detect if a character is within some bounds? /// This shows how to do that. fn main() { App::new() .add_plugins(BaseGamePlugin) .init_resource::() .add_systems( Startup, ( setup_physics_scene, setup_ui, position_camera.after(setup_camera), ), ) .add_systems( Update, ( control_ball, process_events, sync_resource_to_ui::.run_if(resource_changed::), ), ) .run(); } /// Which area the sphere is in/touching #[derive(Component, Debug, Clone)] enum AreaMarker { Green, Blue, } /// Setup a basic scene with: /// * Floor /// * A light for dramatic shadows /// * A sphere that can move /// * Two cubes that designate area intersection fn setup_physics_scene( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { // Create the ground. // Make this a physical looking object commands.spawn(( Collider::cuboid(100.0, 0.1, 100.0), Transform::from_xyz(0.0, -1.0, 0.0), Pickable::IGNORE, Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(50.0)))), MeshMaterial3d(materials.add(StandardMaterial { base_color: RED.into(), ..Default::default() })), )); commands.spawn(( SpotLight { color: WHITE.into(), intensity: 10_000_000.0, shadows_enabled: true, ..default() }, Transform::from_xyz(0.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), )); // Create the bouncing ball. commands.spawn(( RigidBody::Dynamic, GravityScale(1.0), Collider::ball(0.5), ColliderMassProperties::Mass(2.0), Restitution::coefficient(0.7), ExternalImpulse::default(), Transform::from_xyz(0.0, 4.0, 0.0), ActiveEvents::all(), Name::new("Ball"), Mesh3d(meshes.add(SphereMeshBuilder::new( 0.5, SphereKind::Uv { sectors: 10, stacks: 10, }, ))), MeshMaterial3d(materials.add(StandardMaterial { base_color: WHITE.into(), ..Default::default() })), )); // Create a box that the ball can pass into/through commands.spawn(( Sensor, Collider::cuboid(1.0, 1.0, 1.0), Transform::from_xyz(0.0, 0.0, 2.0), Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), Name::new("Green Area"), MeshMaterial3d(materials.add(StandardMaterial { base_color: GREEN.with_alpha(0.5).into(), alpha_mode: AlphaMode::Blend, ..Default::default() })), AreaMarker::Green, )); // Create a box that the ball can pass into/through commands.spawn(( Sensor, Collider::cuboid(1.0, 1.0, 1.0), Transform::from_xyz(0.0, 0.0, -2.0), Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), Name::new("Blue Area"), MeshMaterial3d(materials.add(StandardMaterial { base_color: BLUE.with_alpha(0.5).into(), alpha_mode: AlphaMode::Blend, ..Default::default() })), AreaMarker::Blue, )); } /// Setup the UI for this example that indicates if the game "sees" the sphere /// is inside the cube fn setup_ui(mut commands: Commands) { commands.spawn(( Node { align_self: AlignSelf::Start, justify_self: JustifySelf::Center, ..default() }, Text("Placeholder".into()), SyncResource::::default(), )); } /// Position the world camera to get a better view fn position_camera(mut query: Query<&mut Transform, (With, With)>) { query.iter_mut().for_each(|mut t| { *t = Transform::from_xyz(10.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y); }) } /// Control the ball based on arrow keys fn control_ball(keys: Res>, mut ball: Single<&mut ExternalImpulse>) { if keys.pressed(KeyCode::ArrowUp) { ball.torque_impulse = Vec3::new(0.0, 0.0, 0.1); } if keys.pressed(KeyCode::ArrowDown) { ball.torque_impulse = Vec3::new(0.0, 0.0, -0.1); } if keys.pressed(KeyCode::ArrowLeft) { ball.torque_impulse = Vec3::new(0.1, 0.0, 0.0); } if keys.pressed(KeyCode::ArrowRight) { ball.torque_impulse = Vec3::new(-0.1, 0.0, 0.0); } } /// Maps CollisionEvent to a game-specific construct (did X enter/exit an area) #[derive(Debug)] enum WithinBounds { Enter(Entity, Entity), Exit(Entity, Entity), } /// Resource tracking what box the sphere is in #[derive(Resource, Debug, Default, Clone)] struct InsideArea(Option); impl Display for InsideArea { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self.0 { Some(AreaMarker::Blue) => write!(f, "Ball IS touching the BLUE box"), Some(AreaMarker::Green) => write!(f, "Ball IS touching the GREEN box"), None => write!(f, "Ball is NOT touching ANY box"), } } } /// Read collision events and procss them into resource updates to be displayed to the user fn process_events( mut events: EventReader, areas: Query<&AreaMarker>, mut inside: ResMut, ) { events .read() .filter_map(|collision_event| match collision_event { CollisionEvent::Started(a, b, flags) => { (*flags == CollisionEventFlags::SENSOR).then_some(WithinBounds::Enter(*a, *b)) } CollisionEvent::Stopped(a, b, flags) => { (*flags == CollisionEventFlags::SENSOR).then_some(WithinBounds::Exit(*a, *b)) } }) .for_each(|within_bounds| { match within_bounds { WithinBounds::Enter(a, b) => { if let Ok(x) = areas.get(a) { inside.0 = Some(x.clone()); } else if let Ok(x) = areas.get(b) { inside.0 = Some(x.clone()); } } WithinBounds::Exit(a, b) => { if areas.contains(a) || areas.contains(b) { inside.0 = None; } } } }) }