Compare commits
10 Commits
52c9050d54
...
2f8f81ec37
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8f81ec37 | |||
| 6e2d70d376 | |||
| 5ea8392068 | |||
| 14e383864d | |||
| 9b9c8f1a54 | |||
| eb67c2bf23 | |||
| 490fef5393 | |||
| 216a50ddb8 | |||
| d1230fdcad | |||
| eb95345745 |
@ -1,60 +0,0 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use avian3d::prelude::*;
|
|
||||||
|
|
||||||
use super::target_wall::TargetWallPlugin;
|
|
||||||
|
|
||||||
pub struct LevelPlugin;
|
|
||||||
|
|
||||||
impl Plugin for LevelPlugin
|
|
||||||
{
|
|
||||||
fn build(&self, app: &mut App)
|
|
||||||
{
|
|
||||||
app.add_plugins(TargetWallPlugin);
|
|
||||||
app.add_systems(Startup, setup_level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_level(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
) {
|
|
||||||
const GROUND_SIZE: f32 = 100.0;
|
|
||||||
|
|
||||||
// spawn the ground plane
|
|
||||||
commands.spawn((
|
|
||||||
Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(GROUND_SIZE * 0.5)))),
|
|
||||||
MeshMaterial3d(materials.add(StandardMaterial {
|
|
||||||
base_color: Color::srgb(0.761, 0.698, 0.502),
|
|
||||||
..default()
|
|
||||||
})),
|
|
||||||
Transform::IDENTITY,
|
|
||||||
Collider::cuboid(GROUND_SIZE, 0.0, GROUND_SIZE),
|
|
||||||
RigidBody::Static,
|
|
||||||
));
|
|
||||||
|
|
||||||
// spawn a box
|
|
||||||
commands.spawn((
|
|
||||||
Mesh3d(meshes.add(Cuboid::from_corners(
|
|
||||||
Vec3::new(-0.5, -0.5, -0.5), Vec3::new(0.5, 0.5, 0.5)
|
|
||||||
))),
|
|
||||||
MeshMaterial3d(materials.add(StandardMaterial {
|
|
||||||
base_color: Color::srgb(0.7, 0.1, 0.1),
|
|
||||||
..default()
|
|
||||||
})),
|
|
||||||
Transform::from_xyz(2.0, 0.5, -5.0),
|
|
||||||
Collider::cuboid(1.0, 1.0, 1.0),
|
|
||||||
RigidBody::Dynamic,
|
|
||||||
));
|
|
||||||
|
|
||||||
// spawn a light source
|
|
||||||
commands.spawn((
|
|
||||||
DirectionalLight {
|
|
||||||
illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
|
|
||||||
shadows_enabled: true,
|
|
||||||
..default()
|
|
||||||
},
|
|
||||||
Transform::from_xyz(100.0, 200.0, 100.0).looking_at(Vec3::ZERO, Vec3::Y),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
@ -1,8 +1,15 @@
|
|||||||
|
|
||||||
|
use bevy::input::ButtonState;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use bevy::input::mouse::MouseButtonInput;
|
||||||
|
use avian3d::prelude::*;
|
||||||
|
|
||||||
|
use crate::physics::{ProjectileData, ProjectileSpawner};
|
||||||
|
use crate::ui::camera::CameraController;
|
||||||
|
use crate::ui::cursor::CursorGrabState;
|
||||||
|
|
||||||
mod level;
|
|
||||||
mod target_wall;
|
mod target_wall;
|
||||||
|
use target_wall::TargetWallPlugin;
|
||||||
|
|
||||||
pub struct GamePlugin;
|
pub struct GamePlugin;
|
||||||
|
|
||||||
@ -10,6 +17,89 @@ impl Plugin for GamePlugin
|
|||||||
{
|
{
|
||||||
fn build(&self, app: &mut App)
|
fn build(&self, app: &mut App)
|
||||||
{
|
{
|
||||||
app.add_plugins(level::LevelPlugin);
|
app.add_plugins(TargetWallPlugin);
|
||||||
|
|
||||||
|
app.add_systems(Startup, setup_level);
|
||||||
|
|
||||||
|
app.add_systems(Update, do_shoot_on_left_click
|
||||||
|
.run_if(in_state(CursorGrabState(true)))
|
||||||
|
.run_if(on_event::<MouseButtonInput>))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_level(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
) {
|
||||||
|
const GROUND_SIZE: f32 = 100.0;
|
||||||
|
|
||||||
|
// spawn the camera
|
||||||
|
commands.spawn((
|
||||||
|
CameraController {
|
||||||
|
sensitivity: Vec2::new(0.0007, 0.0007),
|
||||||
|
},
|
||||||
|
Camera3d::default(),
|
||||||
|
Transform::from_xyz(0.0, 1.75, 0.0),
|
||||||
|
));
|
||||||
|
|
||||||
|
// spawn the ground plane
|
||||||
|
commands.spawn((
|
||||||
|
Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(GROUND_SIZE * 0.5)))),
|
||||||
|
MeshMaterial3d(materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.761, 0.698, 0.502),
|
||||||
|
..default()
|
||||||
|
})),
|
||||||
|
Transform::IDENTITY,
|
||||||
|
Collider::cuboid(GROUND_SIZE, 0.0, GROUND_SIZE),
|
||||||
|
RigidBody::Static,
|
||||||
|
));
|
||||||
|
|
||||||
|
// spawn some boxes to shoot at
|
||||||
|
for x in [0.0, 1.5, 3.0]
|
||||||
|
{
|
||||||
|
commands.spawn((
|
||||||
|
Mesh3d(meshes.add(Cuboid::from_corners(
|
||||||
|
Vec3::ZERO, Vec3::ONE,
|
||||||
|
))),
|
||||||
|
MeshMaterial3d(materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.7, 0.1, 0.1),
|
||||||
|
..default()
|
||||||
|
})),
|
||||||
|
Transform::from_xyz(x, 0.5, -2.0),
|
||||||
|
Collider::cuboid(1.0, 1.0, 1.0),
|
||||||
|
RigidBody::Dynamic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// spawn a light source
|
||||||
|
commands.spawn((
|
||||||
|
DirectionalLight {
|
||||||
|
illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
|
||||||
|
shadows_enabled: true,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(100.0, 200.0, 100.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_shoot_on_left_click(
|
||||||
|
camera_query: Single<&GlobalTransform, With<Camera>>,
|
||||||
|
mut mouse_events: EventReader<MouseButtonInput>,
|
||||||
|
mut projectiles: ResMut<ProjectileSpawner>,
|
||||||
|
) {
|
||||||
|
let camera_transform = camera_query.into_inner();
|
||||||
|
|
||||||
|
for _ in mouse_events.read()
|
||||||
|
.filter(|event| event.button == MouseButton::Left)
|
||||||
|
.filter(|event| event.state == ButtonState::Pressed)
|
||||||
|
{
|
||||||
|
projectiles.spawn(ProjectileData {
|
||||||
|
position: camera_transform.translation(),
|
||||||
|
direction: camera_transform.forward(),
|
||||||
|
speed: 900.0,
|
||||||
|
mass: 1.0,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
|
|
||||||
use crate::util;
|
use crate::physics::ProjectileHitEvent;
|
||||||
use crate::physics::Bullet;
|
|
||||||
|
|
||||||
const TARGET_WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1);
|
const WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1);
|
||||||
const TARGET_WALL_DISTANCE: f32 = 25.0;
|
const WALL_FACE_Z: f32 = -25.0;
|
||||||
const TARGET_WALL_COLOR: Color = Color::srgb(0.000, 0.412, 0.580);
|
const WALL_COLOR: Color = Color::srgb(0.000, 0.412, 0.580);
|
||||||
|
|
||||||
const TARGET_COUNT: usize = 10;
|
const TARGET_COUNT: usize = 10;
|
||||||
const TARGET_RADIUS: f32 = 1.0;
|
const TARGET_RADIUS: f32 = 1.0;
|
||||||
@ -22,43 +21,99 @@ impl Plugin for TargetWallPlugin
|
|||||||
{
|
{
|
||||||
fn build(&self, app: &mut App)
|
fn build(&self, app: &mut App)
|
||||||
{
|
{
|
||||||
|
app.insert_resource(TargetResources {
|
||||||
|
target_separation: 0.1,
|
||||||
|
target_handle_counter: 0,
|
||||||
|
targets: HashMap::new(),
|
||||||
|
});
|
||||||
|
|
||||||
app.add_systems(Startup, setup_target_wall);
|
app.add_systems(Startup, setup_target_wall);
|
||||||
app.add_systems(PostProcessCollisions, on_targets_shot.run_if(on_event::<CollisionStarted>));
|
app.add_systems(PostProcessCollisions, on_targets_shot.run_if(on_event::<ProjectileHitEvent>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
struct TargetHandle(i32); // todo: would be nice to use the Entity handle here
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct TargetResources
|
||||||
|
{
|
||||||
|
target_separation: f32,
|
||||||
|
target_handle_counter: i32,
|
||||||
|
targets: HashMap<TargetHandle,(Vec2,Circle)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetResources
|
||||||
|
{
|
||||||
|
fn new_handle(&mut self) -> TargetHandle
|
||||||
|
{
|
||||||
|
self.target_handle_counter += 1;
|
||||||
|
TargetHandle(self.target_handle_counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_transform(&mut self, handle: &TargetHandle) -> Transform
|
||||||
|
{
|
||||||
|
self.targets.remove(&handle);
|
||||||
|
|
||||||
|
let (transform, cache_entry) = loop
|
||||||
|
{
|
||||||
|
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) - WALL_SIZE.y * 0.5;
|
||||||
|
|
||||||
|
let position = Vec2::new(x, y);
|
||||||
|
let circle = Circle::new(radius);
|
||||||
|
|
||||||
|
let has_overlap = self.targets.values().any(|&(position_other, circle_other)|
|
||||||
|
{
|
||||||
|
let distance_squared = Vec2::distance_squared(position, position_other);
|
||||||
|
let max_distance = circle.radius + circle_other.radius + self.target_separation;
|
||||||
|
distance_squared < max_distance * max_distance
|
||||||
|
});
|
||||||
|
|
||||||
|
if has_overlap {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transform = Transform::IDENTITY
|
||||||
|
.with_translation(Vec3::new(x, y, TARGET_HEIGHT * 0.5))
|
||||||
|
.with_rotation(Quat::from_rotation_x(std::f32::consts::PI * 0.5))
|
||||||
|
.with_scale(Vec3::new(radius, 1.0, radius))
|
||||||
|
;
|
||||||
|
|
||||||
|
break (transform, (position, circle));
|
||||||
|
};
|
||||||
|
|
||||||
|
self.targets.insert(*handle, cache_entry);
|
||||||
|
return transform;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
struct Target;
|
pub struct TargetWall;
|
||||||
|
|
||||||
impl Target
|
#[derive(Component)]
|
||||||
{
|
pub struct 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(
|
fn setup_target_wall(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
mut target_resources: ResMut<TargetResources>,
|
||||||
) {
|
) {
|
||||||
commands.spawn((
|
// spawn the wall
|
||||||
|
//
|
||||||
|
let mut wall_commands = commands.spawn((
|
||||||
|
TargetWall,
|
||||||
Mesh3d(meshes.add(Cuboid::from_corners(
|
Mesh3d(meshes.add(Cuboid::from_corners(
|
||||||
Vec3::new(0.0, 0.0, 0.0), TARGET_WALL_SIZE
|
Vec3::ZERO, WALL_SIZE
|
||||||
))),
|
))),
|
||||||
MeshMaterial3d(materials.add(StandardMaterial {
|
MeshMaterial3d(materials.add(StandardMaterial {
|
||||||
base_color: TARGET_WALL_COLOR,
|
base_color: WALL_COLOR,
|
||||||
..default()
|
..default()
|
||||||
})),
|
})),
|
||||||
Transform::from_xyz(0.0, TARGET_WALL_SIZE.y * 0.5, -TARGET_WALL_DISTANCE),
|
Transform::from_xyz(0.0, WALL_SIZE.y * 0.5, WALL_FACE_Z),
|
||||||
Collider::cuboid(TARGET_WALL_SIZE.x, TARGET_WALL_SIZE.y, TARGET_WALL_SIZE.z),
|
Collider::cuboid(WALL_SIZE.x, WALL_SIZE.y, WALL_SIZE.z * 0.5),
|
||||||
RigidBody::Static,
|
RigidBody::Static,
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -74,52 +129,36 @@ fn setup_target_wall(
|
|||||||
TARGET_RADIUS, TARGET_HEIGHT
|
TARGET_RADIUS, TARGET_HEIGHT
|
||||||
);
|
);
|
||||||
|
|
||||||
for _ in 0..TARGET_COUNT
|
// spawn the targets as children of the wall
|
||||||
|
//
|
||||||
|
(0..TARGET_COUNT).for_each(|_|
|
||||||
{
|
{
|
||||||
let transform = Target::random_transform();
|
let handle = target_resources.new_handle();
|
||||||
|
let transform = target_resources.random_transform(&handle);
|
||||||
|
|
||||||
commands.spawn((
|
wall_commands.with_child((
|
||||||
Target,
|
Target,
|
||||||
|
handle,
|
||||||
Mesh3d(target_mesh.clone()),
|
Mesh3d(target_mesh.clone()),
|
||||||
MeshMaterial3d(target_material.clone()),
|
MeshMaterial3d(target_material.clone()),
|
||||||
transform,
|
transform,
|
||||||
target_collider.clone(),
|
target_collider.clone(),
|
||||||
RigidBody::Static,
|
RigidBody::Static,
|
||||||
));
|
));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_targets_shot(
|
fn on_targets_shot(
|
||||||
mut commands: Commands,
|
mut query: Query<(&TargetHandle, &mut Transform), With<Target>>,
|
||||||
mut collision_events: EventReader<CollisionStarted>,
|
mut projectile_hit_events: EventReader<ProjectileHitEvent>,
|
||||||
mut bullet_query: Query<Entity, With<Bullet>>,
|
mut target_resources: ResMut<TargetResources>,
|
||||||
mut target_query: Query<&mut Transform, With<Target>>,
|
|
||||||
) {
|
) {
|
||||||
let mut bullets_to_despawn = HashSet::with_capacity(8);
|
for hit_event in projectile_hit_events.read()
|
||||||
|
|
||||||
for collision in collision_events.read()
|
|
||||||
{
|
{
|
||||||
// resolve the query data from the collision, if the collision is
|
let Ok((handle, mut transform)) = query.get_mut(hit_event.entity) else {
|
||||||
// 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;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// mark the bullet for despawn
|
*transform = target_resources.random_transform(handle);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/main.rs
33
src/main.rs
@ -1,27 +1,18 @@
|
|||||||
|
|
||||||
|
mod util;
|
||||||
|
mod ui;
|
||||||
|
mod physics;
|
||||||
|
mod game;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
mod util;
|
|
||||||
mod window;
|
|
||||||
mod camera;
|
|
||||||
mod physics;
|
|
||||||
mod player;
|
|
||||||
mod ui;
|
|
||||||
mod game;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// 1. spawn bullets in front of the player
|
|
||||||
// 2. fix the camera position to the player's translation
|
|
||||||
|
|
||||||
fn main()
|
fn main()
|
||||||
{
|
{
|
||||||
let mut app = App::new();
|
App::new()
|
||||||
app.add_plugins(DefaultPlugins);
|
.add_plugins(DefaultPlugins)
|
||||||
app.add_plugins(window::WindowPlugin);
|
.add_plugins(util::UtilPlugins)
|
||||||
app.add_plugins(camera::CameraPlugin);
|
.add_plugins(ui::UiPlugins)
|
||||||
app.add_plugins(physics::PhysicsPlugins);
|
.add_plugins(physics::PhysicsPlugins)
|
||||||
app.add_plugins(ui::UiPlugin);
|
.add_plugins(game::GamePlugin)
|
||||||
app.add_plugins(player::PlayerPlugin);
|
.run();
|
||||||
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,11 +2,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::app::PluginGroupBuilder;
|
use bevy::app::PluginGroupBuilder;
|
||||||
|
|
||||||
mod frame_raycast_anti_ghost;
|
mod projectile;
|
||||||
pub use frame_raycast_anti_ghost::*;
|
pub use projectile::*;
|
||||||
|
|
||||||
mod projectiles;
|
|
||||||
pub use projectiles::*;
|
|
||||||
|
|
||||||
mod world_bounds;
|
mod world_bounds;
|
||||||
pub use world_bounds::*;
|
pub use world_bounds::*;
|
||||||
@ -15,15 +12,14 @@ pub struct PhysicsPlugins;
|
|||||||
|
|
||||||
impl PluginGroup for PhysicsPlugins
|
impl PluginGroup for PhysicsPlugins
|
||||||
{
|
{
|
||||||
fn build(self) -> bevy::app::PluginGroupBuilder
|
fn build(self) -> PluginGroupBuilder
|
||||||
{
|
{
|
||||||
PluginGroupBuilder::start::<Self>()
|
PluginGroupBuilder::start::<Self>()
|
||||||
|
|
||||||
.add_group(avian3d::PhysicsPlugins::default())
|
.add_group(avian3d::PhysicsPlugins::default())
|
||||||
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
|
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
|
||||||
|
|
||||||
|
.add(ProjectilePlugin)
|
||||||
.add(WorldBoundsPlugin)
|
.add(WorldBoundsPlugin)
|
||||||
.add(FrameRaycastAntiGhostPlugin)
|
|
||||||
.add(ProjectilesPlugin)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
137
src/physics/projectile.rs
Normal file
137
src/physics/projectile.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::math::NormedVectorSpace;
|
||||||
|
use avian3d::prelude::*;
|
||||||
|
|
||||||
|
pub struct ProjectilePlugin;
|
||||||
|
|
||||||
|
impl Plugin for ProjectilePlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.add_systems(Startup, setup_projectiles);
|
||||||
|
|
||||||
|
app.insert_resource(ProjectileSpawner::default());
|
||||||
|
app.add_systems(PreUpdate, do_projectile_spawn);
|
||||||
|
|
||||||
|
app.add_event::<ProjectileHitEvent>();
|
||||||
|
app.add_systems(PhysicsSchedule, do_projectile_hitscan_flight.in_set(PhysicsStepSet::Last));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ProjectileResources
|
||||||
|
{
|
||||||
|
pub mesh: Mesh3d,
|
||||||
|
pub material: MeshMaterial3d<StandardMaterial>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_projectiles(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
) {
|
||||||
|
commands.insert_resource(ProjectileResources {
|
||||||
|
mesh: Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 2.0))),
|
||||||
|
material: MeshMaterial3d(materials.add(StandardMaterial {
|
||||||
|
base_color: Color::srgb(0.3, 0.3, 0.3),
|
||||||
|
..default()
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProjectileData
|
||||||
|
{
|
||||||
|
pub position: Vec3,
|
||||||
|
pub direction: Dir3,
|
||||||
|
pub speed: f32,
|
||||||
|
pub mass: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct ProjectileSpawner
|
||||||
|
{
|
||||||
|
queue: VecDeque<ProjectileData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectileSpawner
|
||||||
|
{
|
||||||
|
pub fn spawn(&mut self, data: ProjectileData)
|
||||||
|
{
|
||||||
|
self.queue.push_back(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Projectile;
|
||||||
|
|
||||||
|
fn do_projectile_spawn(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut spawner: ResMut<ProjectileSpawner>,
|
||||||
|
resources: Res<ProjectileResources>,
|
||||||
|
) {
|
||||||
|
while let Some(spawn_data) = spawner.queue.pop_front()
|
||||||
|
{
|
||||||
|
let rotation = Quat::from_rotation_arc(Vec3::NEG_Z, spawn_data.direction.into());
|
||||||
|
let transform = Transform::from_translation(spawn_data.position).with_rotation(rotation);
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
Projectile,
|
||||||
|
resources.mesh.clone(),
|
||||||
|
resources.material.clone(),
|
||||||
|
transform,
|
||||||
|
LinearVelocity(spawn_data.direction * spawn_data.speed),
|
||||||
|
Mass(spawn_data.mass),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub struct ProjectileHitEvent
|
||||||
|
{
|
||||||
|
pub entity: Entity,
|
||||||
|
pub normal: Vec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_projectile_hitscan_flight(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut query: Query<(Entity, &mut Transform, &LinearVelocity), With<Projectile>>,
|
||||||
|
mut event_writer: EventWriter<ProjectileHitEvent>,
|
||||||
|
spatial_query: SpatialQuery,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (entity, mut transform, velocity) in query.iter_mut()
|
||||||
|
{
|
||||||
|
let Ok(direction) = Dir3::new(velocity.0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let flight_distance = velocity.norm() * time.delta_secs();
|
||||||
|
|
||||||
|
let ray_cast = spatial_query.cast_ray(
|
||||||
|
transform.translation,
|
||||||
|
direction,
|
||||||
|
flight_distance,
|
||||||
|
true,
|
||||||
|
&SpatialQueryFilter::default()
|
||||||
|
);
|
||||||
|
|
||||||
|
let flight_distance = ray_cast.map(|hit| hit.distance).unwrap_or(flight_distance);
|
||||||
|
transform.translation += direction * flight_distance;
|
||||||
|
|
||||||
|
let Some(ray_hit) = ray_cast else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: impart an impulse on the hit dynamic entity
|
||||||
|
|
||||||
|
event_writer.send(ProjectileHitEvent {
|
||||||
|
entity: ray_hit.entity,
|
||||||
|
normal: ray_hit.normal,
|
||||||
|
});
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,88 +0,0 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use avian3d::prelude::*;
|
|
||||||
|
|
||||||
use super::FrameRaycastAntiGhost;
|
|
||||||
|
|
||||||
pub struct ProjectilesPlugin;
|
|
||||||
|
|
||||||
impl Plugin for ProjectilesPlugin
|
|
||||||
{
|
|
||||||
fn build(&self, app: &mut App)
|
|
||||||
{
|
|
||||||
app.add_systems(Startup, setup_projectiles);
|
|
||||||
|
|
||||||
app.add_event::<BulletFiredEvent>();
|
|
||||||
app.add_systems(Update, on_bullet_fired_event.run_if(on_event::<BulletFiredEvent>));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
pub struct BulletResources
|
|
||||||
{
|
|
||||||
pub mesh: Mesh3d,
|
|
||||||
pub material: MeshMaterial3d<StandardMaterial>,
|
|
||||||
pub collider: Collider,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_projectiles(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
) {
|
|
||||||
commands.insert_resource(BulletResources {
|
|
||||||
mesh: Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Event, Debug)]
|
|
||||||
pub struct BulletFiredEvent
|
|
||||||
{
|
|
||||||
pub position: Vec3,
|
|
||||||
pub direction: Dir3,
|
|
||||||
pub radius: f32,
|
|
||||||
pub density: f32,
|
|
||||||
pub velocity: f32,
|
|
||||||
pub gyro: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct Bullet;
|
|
||||||
|
|
||||||
fn on_bullet_fired_event(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut events: EventReader<BulletFiredEvent>,
|
|
||||||
resources: Res<BulletResources>,
|
|
||||||
) {
|
|
||||||
for event in events.read()
|
|
||||||
{
|
|
||||||
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_scale(Vec3::splat(event.radius))
|
|
||||||
.with_rotation(rotation)
|
|
||||||
.looking_to(event.direction, Vec3::Y);
|
|
||||||
|
|
||||||
commands.spawn((
|
|
||||||
Bullet,
|
|
||||||
resources.mesh.clone(),
|
|
||||||
resources.material.clone(),
|
|
||||||
resources.collider.clone(),
|
|
||||||
transform,
|
|
||||||
ColliderDensity(event.density),
|
|
||||||
AngularVelocity(event.gyro * event.direction),
|
|
||||||
RigidBody::Dynamic,
|
|
||||||
FrameRaycastAntiGhost,
|
|
||||||
LinearVelocity(event.velocity * event.direction),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
pub struct PlayerPlugin;
|
|
||||||
|
|
||||||
impl Plugin for PlayerPlugin
|
|
||||||
{
|
|
||||||
fn build(&self, app: &mut App)
|
|
||||||
{
|
|
||||||
app.add_systems(Startup, setup_player);
|
|
||||||
app.add_systems(PreUpdate, do_shoot_on_left_click.run_if(on_event::<MouseButtonInput>));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct Player;
|
|
||||||
|
|
||||||
fn setup_player(
|
|
||||||
mut commands: Commands,
|
|
||||||
window: Query<&Window, With<PrimaryWindow>>,
|
|
||||||
) {
|
|
||||||
let window = window.single();
|
|
||||||
|
|
||||||
commands
|
|
||||||
.spawn((
|
|
||||||
Player,
|
|
||||||
Transform::from_xyz(0.0, 1.0, 0.0),
|
|
||||||
Collider::capsule(0.5, 1.0),
|
|
||||||
RigidBody::Kinematic,
|
|
||||||
Dominance(32),
|
|
||||||
Visibility::default(),
|
|
||||||
))
|
|
||||||
.with_child((
|
|
||||||
CameraController {
|
|
||||||
active: window.cursor_options.grab_mode == CursorGrabMode::Locked,
|
|
||||||
sensitivity: Vec2::new(0.0007, 0.0007),
|
|
||||||
},
|
|
||||||
Camera3d::default(),
|
|
||||||
Transform::from_xyz(0.0, 0.75, 0.0),
|
|
||||||
))
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_shoot_on_left_click(
|
|
||||||
query: Query<(&GlobalTransform, &CameraController)>,
|
|
||||||
mut mouse_events: EventReader<MouseButtonInput>,
|
|
||||||
mut bullet_events: EventWriter<BulletFiredEvent>,
|
|
||||||
) {
|
|
||||||
let (transform, controller) = query.single();
|
|
||||||
|
|
||||||
if ! controller.active {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for _ in mouse_events.read()
|
|
||||||
.filter(|event| event.button == MouseButton::Left)
|
|
||||||
.filter(|event| event.state == ButtonState::Pressed)
|
|
||||||
{
|
|
||||||
bullet_events.send(BulletFiredEvent {
|
|
||||||
position: transform.translation() + transform.forward() * 0.5,
|
|
||||||
direction: transform.forward(),
|
|
||||||
radius: 0.008,
|
|
||||||
density: 11.0,
|
|
||||||
velocity: 910.0,
|
|
||||||
gyro: f32::to_radians(360.0 * 4.0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,7 +3,7 @@ use bevy::prelude::*;
|
|||||||
use bevy::input::mouse::{AccumulatedMouseMotion, MouseMotion};
|
use bevy::input::mouse::{AccumulatedMouseMotion, MouseMotion};
|
||||||
use bevy::ecs::query::QuerySingleError;
|
use bevy::ecs::query::QuerySingleError;
|
||||||
|
|
||||||
use crate::window::CursorGrabEvent;
|
use super::cursor::CursorGrabState;
|
||||||
|
|
||||||
pub struct CameraPlugin;
|
pub struct CameraPlugin;
|
||||||
|
|
||||||
@ -11,32 +11,19 @@ impl Plugin for CameraPlugin
|
|||||||
{
|
{
|
||||||
fn build(&self, app: &mut App)
|
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
|
||||||
app.add_systems(PreUpdate, do_camera_rotation.run_if(on_event::<MouseMotion>));
|
.run_if(in_state(CursorGrabState(true)))
|
||||||
|
.run_if(on_event::<MouseMotion>)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct CameraController
|
pub struct CameraController
|
||||||
{
|
{
|
||||||
pub active: bool,
|
|
||||||
pub sensitivity: Vec2,
|
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(
|
fn do_camera_rotation(
|
||||||
mut query: Query<(&CameraController, &mut Transform)>,
|
mut query: Query<(&CameraController, &mut Transform)>,
|
||||||
mouse_motion: Res<AccumulatedMouseMotion>,
|
mouse_motion: Res<AccumulatedMouseMotion>,
|
||||||
@ -47,10 +34,6 @@ fn do_camera_rotation(
|
|||||||
Err(QuerySingleError::MultipleEntities(_)) => panic!("Multiple camera controllers found!"),
|
Err(QuerySingleError::MultipleEntities(_)) => panic!("Multiple camera controllers found!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if ! controller.active {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if mouse_motion.delta == Vec2::ZERO {
|
if mouse_motion.delta == Vec2::ZERO {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1,6 +1,33 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
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!
|
||||||
|
|
||||||
|
/*
|
||||||
|
In bevy 0.15.x, you can automatically insert components based on other components using required components
|
||||||
|
https://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html#required-components
|
||||||
|
|
||||||
|
For removal, I would implement it using component hooks
|
||||||
|
https://docs.rs/bevy/latest/bevy/ecs/component/struct.ComponentHooks.html
|
||||||
|
|
||||||
|
An example where adding or removing A adds or removes B:
|
||||||
|
```rust
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct B;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
#[require(B)]
|
||||||
|
#[component(on_remove = remove_B)]
|
||||||
|
pub struct A;
|
||||||
|
|
||||||
|
fn remove_B(world : DefferedWorld, entity : Entity, _id : ComponentId) {
|
||||||
|
world.commands().entity(entity).remove::<B>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
|
||||||
pub struct CrosshairPlugin;
|
pub struct CrosshairPlugin;
|
||||||
|
|
||||||
impl Plugin for 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,24 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/util/ecs/despawn_timer.rs
Normal file
30
src/util/ecs/despawn_timer.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct DespawnTimerPlugin;
|
||||||
|
|
||||||
|
impl Plugin for DespawnTimerPlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.add_systems(Update, do_despawn_timer_tick_and_despawn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct DespawnTimer(pub Timer);
|
||||||
|
|
||||||
|
fn do_despawn_timer_tick_and_despawn(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut query: Query<(Entity, &mut DespawnTimer)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (entity, mut timer) in query.iter_mut()
|
||||||
|
{
|
||||||
|
timer.0.tick(time.delta());
|
||||||
|
|
||||||
|
if timer.0.finished() {
|
||||||
|
commands.entity(entity).despawn_recursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/util/ecs/mod.rs
Normal file
20
src/util/ecs/mod.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
mod despawn_timer;
|
||||||
|
pub use despawn_timer::*;
|
||||||
|
|
||||||
|
mod path_tracer;
|
||||||
|
pub use path_tracer::*;
|
||||||
|
|
||||||
|
use bevy::app::{PluginGroup, PluginGroupBuilder};
|
||||||
|
|
||||||
|
pub struct UtilEcsPlugins;
|
||||||
|
|
||||||
|
impl PluginGroup for UtilEcsPlugins
|
||||||
|
{
|
||||||
|
fn build(self) -> bevy::app::PluginGroupBuilder
|
||||||
|
{
|
||||||
|
PluginGroupBuilder::start::<Self>()
|
||||||
|
.add(DespawnTimerPlugin)
|
||||||
|
.add(EntityPathTracerPlugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/util/ecs/path_tracer.rs
Normal file
156
src/util/ecs/path_tracer.rs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use avian3d::prelude::*;
|
||||||
|
|
||||||
|
use crate::util::physics::CollisionQuery;
|
||||||
|
|
||||||
|
const TRACER_PATH_COLOR: Color = Color::srgb(0.1, 0.7, 0.1);
|
||||||
|
const TRACER_HIT_COLOR_X: Color = Color::srgb(0.7, 0.0, 0.0);
|
||||||
|
const TRACER_HIT_COLOR_Y: Color = Color::srgb(0.0, 0.7, 0.0);
|
||||||
|
const TRACER_HIT_COLOR_Z: Color = Color::srgb(0.0, 0.0, 0.7);
|
||||||
|
const TRACER_HIT_RADIUS: f32 = 0.33;
|
||||||
|
|
||||||
|
pub struct EntityPathTracerPlugin;
|
||||||
|
|
||||||
|
impl Plugin for EntityPathTracerPlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.insert_resource(PathTracerResources {
|
||||||
|
tracers: HashMap::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.add_systems(PhysicsSchedule, do_tracer_log_path.in_set(SolverSet::PreSubstep));
|
||||||
|
app.add_systems(PostProcessCollisions, do_tracer_log_collisions);
|
||||||
|
app.add_systems(Last, do_draw_tracers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct PathTracerResources
|
||||||
|
{
|
||||||
|
tracers: HashMap<Entity,PathTraceData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, PartialEq)]
|
||||||
|
pub struct PathTraceData
|
||||||
|
{
|
||||||
|
path: Vec<Vec3>,
|
||||||
|
collisions: Vec<Isometry3d>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathTraceData
|
||||||
|
{
|
||||||
|
fn log_position(&mut self, isometry: Isometry3d)
|
||||||
|
{
|
||||||
|
self.path.push(isometry.translation.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_collision(&mut self, isometry: Isometry3d)
|
||||||
|
{
|
||||||
|
self.collisions.push(isometry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Clone, PartialEq)]
|
||||||
|
#[allow(unused)]
|
||||||
|
pub enum PathTracer
|
||||||
|
{
|
||||||
|
Transient(PathTraceData),
|
||||||
|
Persistent,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PathTracer
|
||||||
|
{
|
||||||
|
fn default() -> Self
|
||||||
|
{
|
||||||
|
PathTracer::Transient(PathTraceData {
|
||||||
|
path: Vec::new(),
|
||||||
|
collisions: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathTracer
|
||||||
|
{
|
||||||
|
fn mut_trace_data<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
entity: &Entity,
|
||||||
|
resources: &'a mut ResMut<PathTracerResources>
|
||||||
|
) -> &'a mut PathTraceData
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
PathTracer::Transient(trace_data) => trace_data,
|
||||||
|
PathTracer::Persistent => resources.tracers.get_mut(entity).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_trace_data<'a>(
|
||||||
|
&'a self,
|
||||||
|
entity: &Entity,
|
||||||
|
resources: &'a Res<PathTracerResources>
|
||||||
|
) -> &'a PathTraceData
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
PathTracer::Transient(trace_data) => trace_data,
|
||||||
|
PathTracer::Persistent => resources.tracers.get(entity).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_tracer_log_path(
|
||||||
|
mut query: Query<(Entity, &mut PathTracer, &GlobalTransform)>,
|
||||||
|
mut resources: ResMut<PathTracerResources>,
|
||||||
|
) {
|
||||||
|
for (entity, mut tracer, transform) in query.iter_mut()
|
||||||
|
{
|
||||||
|
let trace_data = tracer.mut_trace_data(&entity, &mut resources);
|
||||||
|
trace_data.log_position(transform.compute_transform().to_isometry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_tracer_log_collisions(
|
||||||
|
mut query: Query<(Entity, &mut PathTracer, &GlobalTransform)>,
|
||||||
|
mut collision_events: EventReader<CollisionStarted>,
|
||||||
|
mut resources: ResMut<PathTracerResources>,
|
||||||
|
) {
|
||||||
|
for collision in collision_events.read()
|
||||||
|
{
|
||||||
|
let Ok((entity, mut tracer, transform)) = collision.query_unique(&mut query) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let trace_data = tracer.mut_trace_data(&entity, &mut resources);
|
||||||
|
trace_data.log_collision(transform.compute_transform().to_isometry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_draw_tracers(
|
||||||
|
query: Query<(Entity, &PathTracer)>,
|
||||||
|
resources: Res<PathTracerResources>,
|
||||||
|
mut gizmos: Gizmos,
|
||||||
|
) {
|
||||||
|
use std::f32::consts::FRAC_PI_2;
|
||||||
|
let isometry_x = Isometry3d::from_rotation(Quat::from_rotation_x(FRAC_PI_2));
|
||||||
|
let isometry_y = Isometry3d::from_rotation(Quat::from_rotation_y(FRAC_PI_2));
|
||||||
|
let isometry_z = Isometry3d::from_rotation(Quat::from_rotation_z(FRAC_PI_2));
|
||||||
|
|
||||||
|
for (entity, tracer) in query.iter()
|
||||||
|
{
|
||||||
|
let trace_data = tracer.get_trace_data(&entity, &resources);
|
||||||
|
|
||||||
|
for window in trace_data.path.windows(2)
|
||||||
|
{
|
||||||
|
gizmos.line(window[0], window[1], TRACER_PATH_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
for isometry in trace_data.collisions.iter()
|
||||||
|
{
|
||||||
|
gizmos.circle(*isometry * isometry_x, TRACER_HIT_RADIUS, TRACER_HIT_COLOR_X);
|
||||||
|
gizmos.circle(*isometry * isometry_y, TRACER_HIT_RADIUS, TRACER_HIT_COLOR_Y);
|
||||||
|
gizmos.circle(*isometry * isometry_z, TRACER_HIT_RADIUS, TRACER_HIT_COLOR_Z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,2 +1,21 @@
|
|||||||
|
|
||||||
|
use bevy::app::{PluginGroup, PluginGroupBuilder};
|
||||||
|
|
||||||
|
pub mod ecs;
|
||||||
|
use ecs::UtilEcsPlugins;
|
||||||
|
|
||||||
pub mod physics;
|
pub mod physics;
|
||||||
|
use physics::UtilPhysicsPlugins;
|
||||||
|
|
||||||
|
pub struct UtilPlugins;
|
||||||
|
|
||||||
|
impl PluginGroup for UtilPlugins
|
||||||
|
{
|
||||||
|
fn build(self) -> bevy::app::PluginGroupBuilder
|
||||||
|
{
|
||||||
|
PluginGroupBuilder::start::<Self>()
|
||||||
|
|
||||||
|
.add_group(UtilEcsPlugins)
|
||||||
|
.add_group(UtilPhysicsPlugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/util/physics/mod.rs
Normal file
15
src/util/physics/mod.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
use bevy::app::{PluginGroup, PluginGroupBuilder};
|
||||||
|
|
||||||
|
mod collision_query;
|
||||||
|
pub use collision_query::*;
|
||||||
|
|
||||||
|
pub struct UtilPhysicsPlugins;
|
||||||
|
|
||||||
|
impl PluginGroup for UtilPhysicsPlugins
|
||||||
|
{
|
||||||
|
fn build(self) -> PluginGroupBuilder
|
||||||
|
{
|
||||||
|
PluginGroupBuilder::start::<Self>()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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