From f12c5358a14a63044ef67e5b59502212f033d3c6 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Wed, 25 Jun 2025 12:20:09 -0700 Subject: [PATCH] Basic area intersection done(?) --- examples/sensors.rs | 235 +++++++++++++++++++++++++++++--------------- src/lib.rs | 1 + 2 files changed, 159 insertions(+), 77 deletions(-) diff --git a/examples/sensors.rs b/examples/sensors.rs index d31356c..ad1802b 100644 --- a/examples/sensors.rs +++ b/examples/sensors.rs @@ -1,98 +1,151 @@ -use bevy::{color::palettes::css::{BLUE, GREEN}, render::mesh::{SphereKind, SphereMeshBuilder}}; +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) - .add_systems(Startup, ( - setup_physics_scene, - position_camera.after(setup_camera) - ).chain()) - .add_systems(Update, (control_ball, read_events)) + .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> + 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, -2.0, 0.0), - Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(50.0)))), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: RED.into(), - ..Default::default() - })), - )); + commands.spawn(( + Collider::cuboid(100.0, 0.1, 100.0), + Transform::from_xyz(0.0, -1.0, 0.0), + 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() }, + 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), - Marker, - ActiveEvents::all(), - Mesh3d(meshes.add(SphereMeshBuilder::new( - 0.5, - SphereKind::Uv { - sectors: 10, - stacks: 10, - }, - ))), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: BLUE.into(), - ..Default::default() - })), - )); + 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(), + 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 - // TODO: Transparent box to visualize area - commands - .spawn(( - Sensor, - Marker, - Collider::cuboid(1.0, 1.0, 1.0), - Transform::from_xyz(0.0, 0.0, 0.0), - Mesh3d(meshes.add(Cuboid::new(2.0, 2.0, 2.0))), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: GREEN.with_alpha(0.5).into(), - alpha_mode: AlphaMode::Blend, - ..Default::default() - })), - )); + 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))), + 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))), + 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<(Entity, &mut Transform), (With, With)>, - mut commands: Commands, -) { - query.iter_mut().for_each(|(e, mut t)| { - *t = Transform::from_xyz(10.0, 10.0, 10.0) - .looking_at(Vec3::ZERO, Vec3::Y); +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); }) } -fn control_ball( - keys: Res>, - mut ball: Single<&mut ExternalImpulse>, -) { +/// 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); } @@ -110,29 +163,57 @@ fn control_ball( } } +/// Maps CollisionEvent to a game-specific construct (did X enter/exit an area) #[derive(Debug)] enum WithinBounds { Enter(Entity, Entity), Exit(Entity, Entity), } -#[derive(Component, Debug)] -struct Marker; +/// Resource tracking what box the sphere is in +#[derive(Resource, Debug, Default, Clone)] +struct InsideArea(Option); -fn read_events( +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, - mut commands: Commands, + areas: Query<&AreaMarker>, + mut inside: ResMut, ) { - events.read().filter_map(|collision_event| { - match collision_event { + 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| { - info!("Event: {:?}", within_bounds); - }) + }) + .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; + } + } + } + }) } diff --git a/src/lib.rs b/src/lib.rs index 31e831c..e76cca8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![allow(ambiguous_glob_reexports)] +#![allow(clippy::type_complexity)] mod base_game; mod debug;