initial commit
This commit is contained in:
commit
52c9050d54
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
5074
Cargo.lock
generated
Normal file
5074
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "rs_fps"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
avian3d = "0.2.0"
|
||||||
|
bevy = "0.15.0"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
67
src/camera.rs
Normal file
67
src/camera.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::input::mouse::{AccumulatedMouseMotion, MouseMotion};
|
||||||
|
use bevy::ecs::query::QuerySingleError;
|
||||||
|
|
||||||
|
use crate::window::CursorGrabEvent;
|
||||||
|
|
||||||
|
pub struct CameraPlugin;
|
||||||
|
|
||||||
|
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>));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
) {
|
||||||
|
let (controller, mut transform) = match query.get_single_mut() {
|
||||||
|
Ok((controller, transform)) => (controller, transform),
|
||||||
|
Err(QuerySingleError::NoEntities(_)) => return,
|
||||||
|
Err(QuerySingleError::MultipleEntities(_)) => panic!("Multiple camera controllers found!"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if ! controller.active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mouse_motion.delta == Vec2::ZERO {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
|
||||||
|
|
||||||
|
let yaw = yaw - mouse_motion.delta.x * controller.sensitivity.x;
|
||||||
|
let pitch = pitch - mouse_motion.delta.y * controller.sensitivity.y;
|
||||||
|
|
||||||
|
const PITCH_LIMIT: f32 = std::f32::consts::FRAC_PI_2 - 0.0001;
|
||||||
|
let pitch = pitch.clamp(-PITCH_LIMIT, PITCH_LIMIT);
|
||||||
|
|
||||||
|
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
|
||||||
|
}
|
||||||
60
src/game/level.rs
Normal file
60
src/game/level.rs
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
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),
|
||||||
|
));
|
||||||
|
}
|
||||||
15
src/game/mod.rs
Normal file
15
src/game/mod.rs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
mod level;
|
||||||
|
mod target_wall;
|
||||||
|
|
||||||
|
pub struct GamePlugin;
|
||||||
|
|
||||||
|
impl Plugin for GamePlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.add_plugins(level::LevelPlugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/game/target_wall.rs
Normal file
125
src/game/target_wall.rs
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main.rs
Normal file
27
src/main.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(DefaultPlugins);
|
||||||
|
app.add_plugins(window::WindowPlugin);
|
||||||
|
app.add_plugins(camera::CameraPlugin);
|
||||||
|
app.add_plugins(physics::PhysicsPlugins);
|
||||||
|
app.add_plugins(ui::UiPlugin);
|
||||||
|
app.add_plugins(player::PlayerPlugin);
|
||||||
|
app.add_plugins(game::GamePlugin);
|
||||||
|
app.run();
|
||||||
|
}
|
||||||
55
src/physics/frame_raycast_anti_ghost.rs
Normal file
55
src/physics/frame_raycast_anti_ghost.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/physics/mod.rs
Normal file
29
src/physics/mod.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy::app::PluginGroupBuilder;
|
||||||
|
|
||||||
|
mod frame_raycast_anti_ghost;
|
||||||
|
pub use frame_raycast_anti_ghost::*;
|
||||||
|
|
||||||
|
mod projectiles;
|
||||||
|
pub use projectiles::*;
|
||||||
|
|
||||||
|
mod world_bounds;
|
||||||
|
pub use world_bounds::*;
|
||||||
|
|
||||||
|
pub struct PhysicsPlugins;
|
||||||
|
|
||||||
|
impl PluginGroup for PhysicsPlugins
|
||||||
|
{
|
||||||
|
fn build(self) -> bevy::app::PluginGroupBuilder
|
||||||
|
{
|
||||||
|
PluginGroupBuilder::start::<Self>()
|
||||||
|
|
||||||
|
.add_group(avian3d::PhysicsPlugins::default())
|
||||||
|
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
|
||||||
|
|
||||||
|
.add(WorldBoundsPlugin)
|
||||||
|
.add(FrameRaycastAntiGhostPlugin)
|
||||||
|
.add(ProjectilesPlugin)
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/physics/projectiles.rs
Normal file
88
src/physics/projectiles.rs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
|
||||||
|
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),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/physics/world_bounds.rs
Normal file
37
src/physics/world_bounds.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct WorldBoundsPlugin;
|
||||||
|
|
||||||
|
impl Plugin for WorldBoundsPlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.insert_resource(WorldBounds(1_000.0_f32));
|
||||||
|
app.add_systems(PostUpdate, do_despawn_out_of_bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
struct WorldBounds(f32);
|
||||||
|
|
||||||
|
fn do_despawn_out_of_bounds(
|
||||||
|
mut commands: Commands,
|
||||||
|
query: Query<(Entity, &GlobalTransform)>,
|
||||||
|
world_bounds: Res<WorldBounds>,
|
||||||
|
) {
|
||||||
|
let WorldBounds(max_range) = *world_bounds;
|
||||||
|
|
||||||
|
for (entity, transform) in query.iter()
|
||||||
|
{
|
||||||
|
let translation = transform.translation();
|
||||||
|
|
||||||
|
if
|
||||||
|
translation.x < -max_range || translation.x > max_range ||
|
||||||
|
translation.y < -max_range || translation.y > max_range ||
|
||||||
|
translation.z < -max_range || translation.z > max_range
|
||||||
|
{
|
||||||
|
commands.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/player.rs
Normal file
74
src/player.rs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/ui/crosshair.rs
Normal file
48
src/ui/crosshair.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct CrosshairPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CrosshairPlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.add_systems(Startup, setup_crosshair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_crosshair(
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
const CROSSHAIR_THICKNESS: f32 = 2.0;
|
||||||
|
const CROSSHAIR_LENGTH: f32 = 10.0;
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn(Node {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Percent(100.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_child((
|
||||||
|
Node {
|
||||||
|
width: Val::Px(CROSSHAIR_THICKNESS),
|
||||||
|
height: Val::Px(CROSSHAIR_LENGTH),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::WHITE),
|
||||||
|
))
|
||||||
|
.with_child((
|
||||||
|
Node {
|
||||||
|
width: Val::Px(CROSSHAIR_LENGTH),
|
||||||
|
height: Val::Px(CROSSHAIR_THICKNESS),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
BackgroundColor(Color::WHITE),
|
||||||
|
))
|
||||||
|
;
|
||||||
|
}
|
||||||
14
src/ui/mod.rs
Normal file
14
src/ui/mod.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
mod crosshair;
|
||||||
|
|
||||||
|
pub struct UiPlugin;
|
||||||
|
|
||||||
|
impl Plugin for UiPlugin
|
||||||
|
{
|
||||||
|
fn build(&self, app: &mut App)
|
||||||
|
{
|
||||||
|
app.add_plugins(crosshair::CrosshairPlugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/util/mod.rs
Normal file
2
src/util/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
pub mod physics;
|
||||||
16
src/util/physics.rs
Normal file
16
src/util/physics.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
57
src/window.rs
Normal file
57
src/window.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
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