fix: bullet ghost hits and refactor
This commit is contained in:
parent
eb95345745
commit
d1230fdcad
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
107
src/game/level/target_wall.rs
Normal file
107
src/game/level/target_wall.rs
Normal file
@ -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::<CollisionEnded>));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct Target;
|
||||
|
||||
impl Target
|
||||
{
|
||||
fn random_transform() -> Transform
|
||||
{
|
||||
let radius = rand::random::<f32>() * 0.4 + 0.3;
|
||||
let x = rand::random::<f32>() * (WALL_SIZE.x - radius) - WALL_SIZE.x * 0.5;
|
||||
let y = rand::random::<f32>() * (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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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<CollisionEnded>,
|
||||
mut bullet_query: Query<Entity, With<Bullet>>,
|
||||
mut target_query: Query<&mut Transform, With<Target>>,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PrimaryWindow>>,
|
||||
) {
|
||||
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<Camera>>,
|
||||
mut mouse_events: EventReader<MouseButtonInput>,
|
||||
mut bullet_events: EventWriter<BulletFiredEvent>,
|
||||
) {
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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::<CollisionStarted>));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct Target;
|
||||
|
||||
impl Target
|
||||
{
|
||||
fn random_transform() -> Transform
|
||||
{
|
||||
let x = rand::random::<f32>() * TARGET_WALL_SIZE.x - TARGET_WALL_SIZE.x * 0.5;
|
||||
let y = rand::random::<f32>() * TARGET_WALL_SIZE.y;
|
||||
let scale = rand::random::<f32>() * 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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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<CollisionStarted>,
|
||||
mut bullet_query: Query<Entity, With<Bullet>>,
|
||||
mut target_query: Query<&mut Transform, With<Target>>,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
14
src/main.rs
14
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();
|
||||
}
|
||||
|
||||
@ -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<FrameRaycastAntiGhost>>,
|
||||
) {
|
||||
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<FrameRaycastAntiGhost>>,
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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<BulletFiredEvent>,
|
||||
resources: Res<BulletResources>,
|
||||
bullet_resources: Res<BulletResources>,
|
||||
) {
|
||||
let to_bullet_entity = |event: &BulletFiredEvent|
|
||||
let to_projectile = |event: &BulletFiredEvent|
|
||||
{
|
||||
let rotation = Quat::from_scaled_axis(Vec3::new(
|
||||
rand::random::<f32>() * f32::to_radians(360.0),
|
||||
rand::random::<f32>() * f32::to_radians(360.0),
|
||||
rand::random::<f32>() * 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::<Vec<_>>();
|
||||
commands.spawn_batch(bullet_entities);
|
||||
commands.spawn_batch(events.read()
|
||||
.map(to_projectile)
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
@ -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::<CursorGrabEvent>));
|
||||
app.add_systems(PreUpdate, do_camera_rotation.run_if(on_event::<MouseMotion>));
|
||||
app.add_systems(PreUpdate, do_camera_rotation
|
||||
.run_if(in_state(CursorGrabState(true)))
|
||||
.run_if(on_event::<MouseMotion>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CameraController
|
||||
{
|
||||
pub active: bool,
|
||||
pub sensitivity: Vec2,
|
||||
}
|
||||
|
||||
fn on_cursor_grab_event(
|
||||
mut query: Query<&mut CameraController>,
|
||||
mut events: EventReader<CursorGrabEvent>,
|
||||
) {
|
||||
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<AccumulatedMouseMotion>,
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
54
src/ui/cursor.rs
Normal file
54
src/ui/cursor.rs
Normal file
@ -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<KeyboardInput>,
|
||||
mut window: Single<&mut Window, With<PrimaryWindow>>,
|
||||
cursor_state: Res<State<CursorGrabState>>,
|
||||
mut next_cursor_state: ResMut<NextState<CursorGrabState>>,
|
||||
) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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::<Self>()
|
||||
.add(window::WindowPlugin)
|
||||
.add(cursor::CursorPlugin)
|
||||
.add(camera::CameraPlugin)
|
||||
.add(crosshair::CrosshairPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
45
src/ui/window.rs
Normal file
45
src/ui/window.rs
Normal file
@ -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<PrimaryWindow>>,
|
||||
) {
|
||||
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<PrimaryWindow>>,
|
||||
cursor_state: Res<State<CursorGrabState>>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<D,F>, collision: &CollisionStarted)
|
||||
-> Option<QueryItem<'a, D>>
|
||||
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
|
||||
}
|
||||
86
src/util/physics/collision_query.rs
Normal file
86
src/util/physics/collision_query.rs
Normal file
@ -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<D>)`: 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<D,F>)
|
||||
-> Result<QueryItem<'w, D>,CollisionQueryError<'w>>
|
||||
where
|
||||
'a: 'w,
|
||||
D: QueryData,
|
||||
F: QueryFilter,
|
||||
;
|
||||
}
|
||||
|
||||
impl<T> CollisionQuery for T where T: ToCollisionEntities
|
||||
{
|
||||
fn query_unique<'a,'w,D,F>(&self, query: &'a mut Query<D,F>)
|
||||
-> Result<QueryItem<'w, D>,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/util/physics/mod.rs
Normal file
3
src/util/physics/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
mod collision_query;
|
||||
pub use collision_query::*;
|
||||
@ -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::<CursorGrabEvent>();
|
||||
app.add_systems(PreUpdate, on_cursor_grab_toggled.run_if(on_event::<KeyboardInput>));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct CursorGrabEvent(pub bool);
|
||||
|
||||
fn setup_window(
|
||||
mut window: Query<&mut Window, With<PrimaryWindow>>,
|
||||
) {
|
||||
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<PrimaryWindow>>,
|
||||
mut events: EventReader<KeyboardInput>,
|
||||
mut cursor_events: EventWriter<CursorGrabEvent>,
|
||||
) {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user