diff --git a/src/game/level.rs b/src/game/level.rs index 1a53792..db6e32e 100644 --- a/src/game/level.rs +++ b/src/game/level.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use avian3d::prelude::*; -use super::target_wall::TargetWallPlugin; +mod target_wall; pub struct LevelPlugin; @@ -10,7 +10,7 @@ impl Plugin for LevelPlugin { fn build(&self, app: &mut App) { - app.add_plugins(TargetWallPlugin); + app.add_plugins(target_wall::TargetWallPlugin); app.add_systems(Startup, setup_level); } } diff --git a/src/game/level/target_wall.rs b/src/game/level/target_wall.rs new file mode 100644 index 0000000..ecd31a7 --- /dev/null +++ b/src/game/level/target_wall.rs @@ -0,0 +1,107 @@ + +use bevy::prelude::*; +use avian3d::prelude::*; + +use crate::physics::Bullet; +use crate::util::physics::CollisionQuery; + +const WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1); +const WALL_FACE_Z: f32 = -25.0; +const WALL_COLOR: Color = Color::srgb(0.000, 0.412, 0.580); + +const TARGET_COUNT: usize = 10; +const TARGET_RADIUS: f32 = 1.0; +const TARGET_HEIGHT: f32 = 0.1; +const TARGET_COLOR: Color = Color::srgb(0.7, 0.1, 0.1); + +pub struct TargetWallPlugin; + +impl Plugin for TargetWallPlugin +{ + fn build(&self, app: &mut App) + { + app.add_systems(Startup, setup_target_wall); + app.add_systems(PostProcessCollisions, on_targets_shot.run_if(on_event::)); + } +} + +#[derive(Component)] +struct Target; + +impl Target +{ + fn random_transform() -> Transform + { + let radius = rand::random::() * 0.4 + 0.3; + let x = rand::random::() * (WALL_SIZE.x - radius) - WALL_SIZE.x * 0.5; + let y = rand::random::() * (WALL_SIZE.y - radius); + + Transform::IDENTITY + .with_translation(Vec3::new(x, y, WALL_FACE_Z + TARGET_HEIGHT * 0.5)) + .with_rotation(Quat::from_rotation_x(std::f32::consts::PI * 0.5)) + .with_scale(Vec3::new(radius, 1.0, radius)) + } +} + +fn setup_target_wall( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Mesh3d(meshes.add(Cuboid::from_corners( + Vec3::ZERO, WALL_SIZE + ))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: WALL_COLOR, + ..default() + })), + Transform::from_xyz(0.0, WALL_SIZE.y * 0.5, WALL_FACE_Z - WALL_SIZE.z * 0.5), + Collider::cuboid(WALL_SIZE.x, WALL_SIZE.y, WALL_SIZE.z * 0.5), + RigidBody::Static, + )); + + let target_mesh = meshes.add(Cylinder { + radius: TARGET_RADIUS, + half_height: TARGET_HEIGHT * 0.5, + }); + let target_material = materials.add(StandardMaterial { + base_color: TARGET_COLOR, + ..default() + }); + let target_collider = Collider::cylinder( + TARGET_RADIUS, TARGET_HEIGHT + ); + + let targets: Box<[_;TARGET_COUNT]> = Box::new(std::array::from_fn(|_| ( + Target, + Mesh3d(target_mesh.clone()), + MeshMaterial3d(target_material.clone()), + Target::random_transform(), + target_collider.clone(), + RigidBody::Static, + ))); + commands.spawn_batch(targets.into_iter()); +} + +fn on_targets_shot( + mut collision_events: EventReader, + mut bullet_query: Query>, + mut target_query: Query<&mut Transform, With>, +) { + for collision in collision_events.read() + { + // resolve the query data from the collision, if the collision is + // between a bullet and a target + // + let Ok(_) = collision.query_unique(&mut bullet_query) else { + continue; + }; + let Ok(mut target_transform) = collision.query_unique(&mut target_query) else { + continue; + }; + + // move the target + *target_transform = Target::random_transform(); + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 682f1a7..23ba98d 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,8 +1,8 @@ use bevy::prelude::*; +mod player; mod level; -mod target_wall; pub struct GamePlugin; @@ -10,6 +10,7 @@ impl Plugin for GamePlugin { fn build(&self, app: &mut App) { + app.add_plugins(player::PlayerPlugin); app.add_plugins(level::LevelPlugin); } } diff --git a/src/player.rs b/src/game/player.rs similarity index 76% rename from src/player.rs rename to src/game/player.rs index 942a2d8..d1b4396 100644 --- a/src/player.rs +++ b/src/game/player.rs @@ -1,11 +1,10 @@ use bevy::prelude::*; -use bevy::window::{CursorGrabMode, PrimaryWindow}; use bevy::input::{mouse::MouseButtonInput, ButtonState}; use avian3d::prelude::*; -use crate::camera::CameraController; use crate::physics::BulletFiredEvent; +use crate::ui::camera::CameraController; pub struct PlayerPlugin; @@ -23,10 +22,7 @@ pub struct Player; fn setup_player( mut commands: Commands, - window: Query<&Window, With>, ) { - let window = window.single(); - commands .spawn(( Player, @@ -38,7 +34,6 @@ fn setup_player( )) .with_child(( CameraController { - active: window.cursor_options.grab_mode == CursorGrabMode::Locked, sensitivity: Vec2::new(0.0007, 0.0007), }, Camera3d::default(), @@ -48,15 +43,11 @@ fn setup_player( } fn do_shoot_on_left_click( - query: Query<(&GlobalTransform, &CameraController)>, + query: Single<&GlobalTransform, With>, mut mouse_events: EventReader, mut bullet_events: EventWriter, ) { - let (transform, controller) = query.single(); - - if ! controller.active { - return; - } + let transform = query.into_inner(); for _ in mouse_events.read() .filter(|event| event.button == MouseButton::Left) @@ -68,7 +59,6 @@ fn do_shoot_on_left_click( radius: 0.008, density: 11.0, velocity: 910.0, - gyro: f32::to_radians(360.0 * 4.0), }); } } diff --git a/src/game/target_wall.rs b/src/game/target_wall.rs deleted file mode 100644 index bf4fc54..0000000 --- a/src/game/target_wall.rs +++ /dev/null @@ -1,125 +0,0 @@ - -use std::collections::HashSet; - -use bevy::prelude::*; -use avian3d::prelude::*; - -use crate::util; -use crate::physics::Bullet; - -const TARGET_WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1); -const TARGET_WALL_DISTANCE: f32 = 25.0; -const TARGET_WALL_COLOR: Color = Color::srgb(0.000, 0.412, 0.580); - -const TARGET_COUNT: usize = 10; -const TARGET_RADIUS: f32 = 1.0; -const TARGET_HEIGHT: f32 = 0.1; -const TARGET_COLOR: Color = Color::srgb(0.7, 0.1, 0.1); - -pub struct TargetWallPlugin; - -impl Plugin for TargetWallPlugin -{ - fn build(&self, app: &mut App) - { - app.add_systems(Startup, setup_target_wall); - app.add_systems(PostProcessCollisions, on_targets_shot.run_if(on_event::)); - } -} - -#[derive(Component)] -struct Target; - -impl Target -{ - fn random_transform() -> Transform - { - let x = rand::random::() * TARGET_WALL_SIZE.x - TARGET_WALL_SIZE.x * 0.5; - let y = rand::random::() * TARGET_WALL_SIZE.y; - let scale = rand::random::() * 0.4 + 0.3; - - Transform::from_xyz(x, y, TARGET_HEIGHT * 0.5 - TARGET_WALL_DISTANCE) - .with_scale(Vec3::new(scale, 1.0, scale)) - .with_rotation(Quat::from_rotation_x(std::f32::consts::PI * 0.5)) - } -} - -fn setup_target_wall( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - commands.spawn(( - Mesh3d(meshes.add(Cuboid::from_corners( - Vec3::new(0.0, 0.0, 0.0), TARGET_WALL_SIZE - ))), - MeshMaterial3d(materials.add(StandardMaterial { - base_color: TARGET_WALL_COLOR, - ..default() - })), - Transform::from_xyz(0.0, TARGET_WALL_SIZE.y * 0.5, -TARGET_WALL_DISTANCE), - Collider::cuboid(TARGET_WALL_SIZE.x, TARGET_WALL_SIZE.y, TARGET_WALL_SIZE.z), - RigidBody::Static, - )); - - let target_mesh = meshes.add(Cylinder { - radius: TARGET_RADIUS, - half_height: TARGET_HEIGHT * 0.5, - }); - let target_material = materials.add(StandardMaterial { - base_color: TARGET_COLOR, - ..default() - }); - let target_collider = Collider::cylinder( - TARGET_RADIUS, TARGET_HEIGHT - ); - - for _ in 0..TARGET_COUNT - { - let transform = Target::random_transform(); - - commands.spawn(( - Target, - Mesh3d(target_mesh.clone()), - MeshMaterial3d(target_material.clone()), - transform, - target_collider.clone(), - RigidBody::Static, - )); - } -} - -fn on_targets_shot( - mut commands: Commands, - mut collision_events: EventReader, - mut bullet_query: Query>, - mut target_query: Query<&mut Transform, With>, -) { - let mut bullets_to_despawn = HashSet::with_capacity(8); - - for collision in collision_events.read() - { - // resolve the query data from the collision, if the collision is - // between a bullet and a target - // - let Some(bullet_entity) = util::physics::query_collision(&mut bullet_query, collision) else { - continue; - }; - let Some(mut target_transform) = util::physics::query_collision(&mut target_query, collision) else { - continue; - }; - - // mark the bullet for despawn - bullets_to_despawn.insert(bullet_entity); - - // respawn the target - *target_transform = Target::random_transform(); - } - - // despawn the bullets that were involved in the collisions - // - for bullet_entity in bullets_to_despawn - { - commands.entity(bullet_entity).despawn(); - } -} diff --git a/src/main.rs b/src/main.rs index cc90a12..0ded365 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,26 +2,16 @@ use bevy::prelude::*; mod util; -mod window; -mod camera; -mod physics; -mod player; mod ui; +mod physics; mod game; -// TODO -// 1. spawn bullets in front of the player -// 2. fix the camera position to the player's translation - fn main() { let mut app = App::new(); app.add_plugins(DefaultPlugins); - app.add_plugins(window::WindowPlugin); - app.add_plugins(camera::CameraPlugin); + app.add_plugins(ui::UiPlugins); app.add_plugins(physics::PhysicsPlugins); - app.add_plugins(ui::UiPlugin); - app.add_plugins(player::PlayerPlugin); app.add_plugins(game::GamePlugin); app.run(); } diff --git a/src/physics/frame_raycast_anti_ghost.rs b/src/physics/frame_raycast_anti_ghost.rs deleted file mode 100644 index a0f59d5..0000000 --- a/src/physics/frame_raycast_anti_ghost.rs +++ /dev/null @@ -1,55 +0,0 @@ - -use bevy::prelude::*; -use avian3d::prelude::*; - -pub struct FrameRaycastAntiGhostPlugin; - -impl Plugin for FrameRaycastAntiGhostPlugin -{ - fn build(&self, app: &mut App) - { - app.add_systems(PreUpdate, do_frame_raycast_origin); - app.add_systems(PhysicsSchedule, do_frame_raycast_anti_ghost.in_set(PhysicsStepSet::First)); - } -} - -#[derive(Component)] -pub struct FrameRaycastAntiGhost; - -#[derive(Component)] -struct FrameRaycastOrigin(Vec3); - -fn do_frame_raycast_origin( - mut commands: Commands, - query: Query<(Entity, &GlobalTransform), With>, -) { - for (entity, transform) in query.iter() - { - let frame_ray_origin = FrameRaycastOrigin(transform.translation()); - commands.entity(entity).insert(frame_ray_origin); - } -} - -fn do_frame_raycast_anti_ghost( - mut query: Query<(Entity, &FrameRaycastOrigin, &mut Transform), With>, - ray_caster: SpatialQuery, -) { - for (entity, frame_ray_origin, mut transform) in query.iter_mut() - { - let ray_origin = frame_ray_origin.0; - let ray_direction = transform.translation - ray_origin; - let ray_distance = ray_direction.length(); - let ray_solid = true; - let ray_filter = SpatialQueryFilter::default().with_excluded_entities([entity]); - - let Ok(ray_direction) = Dir3::new(ray_direction) else { - continue; - }; - - if let Some(ray_hit) = ray_caster.cast_ray(ray_origin, ray_direction, ray_distance, ray_solid, &ray_filter) - { - // correct for ghosting by moving the entity to the raycast hit point - transform.translation = ray_origin + ray_direction * ray_hit.distance; - } - } -} diff --git a/src/physics/mod.rs b/src/physics/mod.rs index 9a6b809..a08f6bd 100644 --- a/src/physics/mod.rs +++ b/src/physics/mod.rs @@ -2,9 +2,6 @@ use bevy::prelude::*; use bevy::app::PluginGroupBuilder; -mod frame_raycast_anti_ghost; -pub use frame_raycast_anti_ghost::*; - mod projectiles; pub use projectiles::*; @@ -23,7 +20,6 @@ impl PluginGroup for PhysicsPlugins .add(avian3d::debug_render::PhysicsDebugPlugin::default()) .add(WorldBoundsPlugin) - .add(FrameRaycastAntiGhostPlugin) .add(ProjectilesPlugin) } } diff --git a/src/physics/projectiles.rs b/src/physics/projectiles.rs index ed03031..c9b7e51 100644 --- a/src/physics/projectiles.rs +++ b/src/physics/projectiles.rs @@ -2,8 +2,6 @@ use bevy::prelude::*; use avian3d::prelude::*; -use super::FrameRaycastAntiGhost; - pub struct ProjectilesPlugin; impl Plugin for ProjectilesPlugin @@ -31,12 +29,12 @@ fn setup_projectiles( mut materials: ResMut>, ) { commands.insert_resource(BulletResources { - mesh: Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + mesh: Mesh3d(meshes.add(Sphere::new(1.0))), material: MeshMaterial3d(materials.add(StandardMaterial { base_color: Color::srgb(0.7, 0.1, 0.1), ..default() })), - collider: Collider::cuboid(1.0, 1.0, 1.0), + collider: Collider::sphere(1.0), }); } @@ -48,7 +46,6 @@ pub struct BulletFiredEvent pub radius: f32, pub density: f32, pub velocity: f32, - pub gyro: f32, } #[derive(Component)] @@ -57,35 +54,31 @@ pub struct Bullet; fn on_bullet_fired_event( mut commands: Commands, mut events: EventReader, - resources: Res, + bullet_resources: Res, ) { - let to_bullet_entity = |event: &BulletFiredEvent| + let to_projectile = |event: &BulletFiredEvent| { - let rotation = Quat::from_scaled_axis(Vec3::new( - rand::random::() * f32::to_radians(360.0), - rand::random::() * f32::to_radians(360.0), - rand::random::() * f32::to_radians(360.0), - )); let transform = Transform::IDENTITY .with_translation(event.position) - .with_rotation(rotation) .with_scale(Vec3::splat(event.radius)) .looking_to(event.direction, Vec3::Y); ( Bullet, - resources.mesh.clone(), - resources.material.clone(), - resources.collider.clone(), + bullet_resources.mesh.clone(), + bullet_resources.material.clone(), + bullet_resources.collider.clone(), transform, RigidBody::Dynamic, ColliderDensity(event.density), LinearVelocity(event.velocity * event.direction), - AngularVelocity(event.gyro * event.direction), - FrameRaycastAntiGhost, + SpeculativeMargin(0.0), + SweptCcd::LINEAR, ) }; - let bullet_entities = events.read().map(to_bullet_entity).collect::>(); - commands.spawn_batch(bullet_entities); + commands.spawn_batch(events.read() + .map(to_projectile) + .collect::>() + ); } diff --git a/src/camera.rs b/src/ui/camera.rs similarity index 67% rename from src/camera.rs rename to src/ui/camera.rs index db0e8cc..c005a49 100644 --- a/src/camera.rs +++ b/src/ui/camera.rs @@ -3,7 +3,7 @@ use bevy::prelude::*; use bevy::input::mouse::{AccumulatedMouseMotion, MouseMotion}; use bevy::ecs::query::QuerySingleError; -use crate::window::CursorGrabEvent; +use super::cursor::CursorGrabState; pub struct CameraPlugin; @@ -11,32 +11,19 @@ impl Plugin for CameraPlugin { fn build(&self, app: &mut App) { - app.add_systems(PreUpdate, on_cursor_grab_event.run_if(on_event::)); - app.add_systems(PreUpdate, do_camera_rotation.run_if(on_event::)); + app.add_systems(PreUpdate, do_camera_rotation + .run_if(in_state(CursorGrabState(true))) + .run_if(on_event::) + ); } } #[derive(Component)] pub struct CameraController { - pub active: bool, pub sensitivity: Vec2, } -fn on_cursor_grab_event( - mut query: Query<&mut CameraController>, - mut events: EventReader, -) { - let Ok(mut controller) = query.get_single_mut() else { - return; - }; - - for CursorGrabEvent(grabbed) in events.read() - { - controller.active = *grabbed; - } -} - fn do_camera_rotation( mut query: Query<(&CameraController, &mut Transform)>, mouse_motion: Res, @@ -47,10 +34,6 @@ fn do_camera_rotation( Err(QuerySingleError::MultipleEntities(_)) => panic!("Multiple camera controllers found!"), }; - if ! controller.active { - return; - } - if mouse_motion.delta == Vec2::ZERO { return; } diff --git a/src/ui/crosshair.rs b/src/ui/crosshair.rs index 3e6d082..8fdf829 100644 --- a/src/ui/crosshair.rs +++ b/src/ui/crosshair.rs @@ -1,6 +1,10 @@ use bevy::prelude::*; +// TODO: add a crosshair component which projects a crosshair to infinity. +// For now, we'll just draw a crosshair in the center of the screen. +// This causes a lot of warnings without a camera! + pub struct CrosshairPlugin; impl Plugin for CrosshairPlugin diff --git a/src/ui/cursor.rs b/src/ui/cursor.rs new file mode 100644 index 0000000..6a23737 --- /dev/null +++ b/src/ui/cursor.rs @@ -0,0 +1,54 @@ + +use bevy::prelude::*; +use bevy::input::{ButtonState, keyboard::KeyboardInput}; +use bevy::window::{CursorGrabMode, PrimaryWindow}; + +const CURSOR_TOGGLE_KEY: KeyCode = KeyCode::Escape; +const CURSOR_TOGGLE_STATE: ButtonState = ButtonState::Released; + +pub struct CursorPlugin; + +impl Plugin for CursorPlugin +{ + fn build(&self, app: &mut App) + { + app.insert_state(CursorGrabState(false)); + app.add_systems(PreUpdate, on_cursor_grab_toggled.in_set(CursorSet)); + } +} + +#[derive(SystemSet, Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct CursorSet; + +#[derive(States, Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct CursorGrabState(pub bool); + +fn on_cursor_grab_toggled( + mut keyboard_events: EventReader, + mut window: Single<&mut Window, With>, + cursor_state: Res>, + mut next_cursor_state: ResMut>, +) { + let cursor_toggle_count = keyboard_events.read() + .filter(|event| event.key_code == CURSOR_TOGGLE_KEY) + .filter(|event| event.state == CURSOR_TOGGLE_STATE) + .count(); + + if cursor_toggle_count % 2 == 0 { + return; + } + + match cursor_state.get() + { + CursorGrabState(true) => { + window.cursor_options.visible = false; + window.cursor_options.grab_mode = CursorGrabMode::Locked; + next_cursor_state.set(CursorGrabState(false)); + } + CursorGrabState(false) => { + window.cursor_options.visible = true; + window.cursor_options.grab_mode = CursorGrabMode::None; + next_cursor_state.set(CursorGrabState(true)); + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5a7880b..a464241 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,14 +1,22 @@ use bevy::prelude::*; +use bevy::app::PluginGroupBuilder; -mod crosshair; +pub mod window; +pub mod cursor; +pub mod camera; +pub mod crosshair; -pub struct UiPlugin; +pub struct UiPlugins; -impl Plugin for UiPlugin +impl PluginGroup for UiPlugins { - fn build(&self, app: &mut App) + fn build(self) -> PluginGroupBuilder { - app.add_plugins(crosshair::CrosshairPlugin); + PluginGroupBuilder::start::() + .add(window::WindowPlugin) + .add(cursor::CursorPlugin) + .add(camera::CameraPlugin) + .add(crosshair::CrosshairPlugin) } } diff --git a/src/ui/window.rs b/src/ui/window.rs new file mode 100644 index 0000000..24bdbfe --- /dev/null +++ b/src/ui/window.rs @@ -0,0 +1,45 @@ + +use bevy::prelude::*; +use bevy::window::{CursorGrabMode, PrimaryWindow, WindowResolution}; + +use super::cursor::CursorGrabState; + +pub struct WindowPlugin; + +impl Plugin for WindowPlugin +{ + fn build(&self, app: &mut App) + { + app.add_systems(PreStartup, setup_window); + app.add_systems(OnEnter(CursorGrabState(true)), on_cursor_grab_toggled); + app.add_systems(OnEnter(CursorGrabState(false)), on_cursor_grab_toggled); + } +} + +fn setup_window( + mut window: Query<&mut Window, With>, +) { + let mut window = window.single_mut(); + window.title = "Bevy FPS Game".to_string(); + window.resolution = WindowResolution::new(1920.0, 1080.0); + window.position = WindowPosition::Centered(MonitorSelection::Primary); +} + +fn on_cursor_grab_toggled( + mut window: Query<&mut Window, With>, + cursor_state: Res>, +) { + let mut window = window.single_mut(); + + match cursor_state.get() + { + CursorGrabState(true) => { + window.cursor_options.visible = false; + window.cursor_options.grab_mode = CursorGrabMode::Locked; + } + CursorGrabState(false) => { + window.cursor_options.visible = true; + window.cursor_options.grab_mode = CursorGrabMode::None; + } + } +} diff --git a/src/util/physics.rs b/src/util/physics.rs deleted file mode 100644 index a2ec621..0000000 --- a/src/util/physics.rs +++ /dev/null @@ -1,16 +0,0 @@ - -use bevy::prelude::*; -use bevy::ecs::query::{QueryData, QueryFilter, QueryItem}; -use avian3d::prelude::*; - -pub fn query_collision<'a,D,F>(query: &'a mut Query, collision: &CollisionStarted) - -> Option> -where - D: QueryData, - F: QueryFilter, -{ - let CollisionStarted(entity1, entity2) = collision; - if query.contains(*entity1) { return query.get_mut(*entity1).ok(); } - if query.contains(*entity2) { return query.get_mut(*entity2).ok(); } - None -} diff --git a/src/util/physics/collision_query.rs b/src/util/physics/collision_query.rs new file mode 100644 index 0000000..43f482d --- /dev/null +++ b/src/util/physics/collision_query.rs @@ -0,0 +1,86 @@ + +use bevy::prelude::*; +use bevy::ecs::query::{QueryData, QueryEntityError, QueryFilter, QueryItem}; +use avian3d::prelude::*; + +/// A trait that provides a method to retrieve the two entities involved in a collision. +/// +pub trait ToCollisionEntities +{ + /// Returns the two entities involved in this collision. + /// + fn to_collision_entities(&self) -> (Entity, Entity); +} + +impl ToCollisionEntities for Collision { + fn to_collision_entities(&self) -> (Entity, Entity) { + (self.0.entity1, self.0.entity2) + } +} + +impl ToCollisionEntities for CollisionStarted { + fn to_collision_entities(&self) -> (Entity, Entity) { + (self.0, self.1) + } +} + +impl ToCollisionEntities for CollisionEnded { + fn to_collision_entities(&self) -> (Entity, Entity) { + (self.0, self.1) + } +} + +/// Represents errors that can occur when querying collision data. +/// +pub enum CollisionQueryError<'w> +{ + /// Indicates that both collision entities match the query. + CollisionQueryNotUnique, + /// Indicates that neither collision entity matches the query. + NoMatchingCollisionEntity, + /// Indicates that an error occurred during the query. + QueryError(#[allow(unused)] QueryEntityError<'w>), +} + +pub trait CollisionQuery +{ + /// Queries the collision data for the unique entity associated with this collision query. + /// + /// # Parameters + /// - `query`: A mutable reference to the query to search for the collision entities. + /// + /// # Returns + /// - `Ok(QueryItem)`: The query item for the found unique collision entity. + /// - `Err(CollisionQueryError::CollisionQueryNotUnique)`: If both collision entities match the query. + /// - `Err(CollisionQueryError::NoMatchingCollisionEntity)`: If neither collision entity matches the query. + /// - `Err(CollisionQueryError::QueryError)`: If an error occurs during the query. + /// + fn query_unique<'a,'w,D,F>(&self, query: &'a mut Query) + -> Result,CollisionQueryError<'w>> + where + 'a: 'w, + D: QueryData, + F: QueryFilter, + ; +} + +impl CollisionQuery for T where T: ToCollisionEntities +{ + fn query_unique<'a,'w,D,F>(&self, query: &'a mut Query) + -> Result,CollisionQueryError<'w>> + where + 'a: 'w, + D: QueryData, + F: QueryFilter, + { + let (entity1, entity2) = self.to_collision_entities(); + + match (query.contains(entity1), query.contains(entity2)) + { + (true, true) => Err(CollisionQueryError::CollisionQueryNotUnique), + (true, false) => query.get_mut(entity1).map_err(|error| CollisionQueryError::QueryError(error)), + (false, true) => query.get_mut(entity2).map_err(|error| CollisionQueryError::QueryError(error)), + (false, false) => Err(CollisionQueryError::NoMatchingCollisionEntity), + } + } +} diff --git a/src/util/physics/mod.rs b/src/util/physics/mod.rs new file mode 100644 index 0000000..fb16957 --- /dev/null +++ b/src/util/physics/mod.rs @@ -0,0 +1,3 @@ + +mod collision_query; +pub use collision_query::*; diff --git a/src/window.rs b/src/window.rs deleted file mode 100644 index 7b05303..0000000 --- a/src/window.rs +++ /dev/null @@ -1,57 +0,0 @@ - -use bevy::prelude::*; -use bevy::window::{CursorGrabMode, PrimaryWindow, WindowResolution}; -use bevy::input::{ButtonState, keyboard::KeyboardInput}; - -pub struct WindowPlugin; - -impl Plugin for WindowPlugin -{ - fn build(&self, app: &mut App) - { - app.add_systems(PreStartup, setup_window); - - app.add_event::(); - app.add_systems(PreUpdate, on_cursor_grab_toggled.run_if(on_event::)); - } -} - -#[derive(Event)] -pub struct CursorGrabEvent(pub bool); - -fn setup_window( - mut window: Query<&mut Window, With>, -) { - let mut window = window.single_mut(); - window.title = "Bevy FPS Game".to_string(); - window.resolution = WindowResolution::new(1920.0, 1080.0); - window.position = WindowPosition::Centered(MonitorSelection::Primary); -} - -fn on_cursor_grab_toggled( - mut window: Query<&mut Window, With>, - mut events: EventReader, - mut cursor_events: EventWriter, -) { - let mut window = window.single_mut(); - - for _ in events.read() - .filter(|event| event.key_code == KeyCode::Escape) - .filter(|event| event.state == ButtonState::Pressed) - { - match window.cursor_options.grab_mode - { - CursorGrabMode::None => { - window.cursor_options.visible = false; - window.cursor_options.grab_mode = CursorGrabMode::Locked; - cursor_events.send(CursorGrabEvent(true)); - } - CursorGrabMode::Locked => { - window.cursor_options.visible = true; - window.cursor_options.grab_mode = CursorGrabMode::None; - cursor_events.send(CursorGrabEvent(false)); - } - _ => panic!("Invalid cursor grab mode"), - } - } -}