feat: hitscan projectiles
This commit is contained in:
parent
6e2d70d376
commit
2f8f81ec37
@ -4,7 +4,7 @@ use bevy::prelude::*;
|
|||||||
use bevy::input::mouse::MouseButtonInput;
|
use bevy::input::mouse::MouseButtonInput;
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
|
|
||||||
use crate::physics::BulletFiredEvent;
|
use crate::physics::{ProjectileData, ProjectileSpawner};
|
||||||
use crate::ui::camera::CameraController;
|
use crate::ui::camera::CameraController;
|
||||||
use crate::ui::cursor::CursorGrabState;
|
use crate::ui::cursor::CursorGrabState;
|
||||||
|
|
||||||
@ -87,22 +87,19 @@ fn setup_level(
|
|||||||
fn do_shoot_on_left_click(
|
fn do_shoot_on_left_click(
|
||||||
camera_query: Single<&GlobalTransform, With<Camera>>,
|
camera_query: Single<&GlobalTransform, With<Camera>>,
|
||||||
mut mouse_events: EventReader<MouseButtonInput>,
|
mut mouse_events: EventReader<MouseButtonInput>,
|
||||||
mut bullet_events: EventWriter<BulletFiredEvent>,
|
mut projectiles: ResMut<ProjectileSpawner>,
|
||||||
) {
|
) {
|
||||||
let camera_transform = camera_query.into_inner();
|
let camera_transform = camera_query.into_inner();
|
||||||
|
|
||||||
let bullet_fired_event = BulletFiredEvent {
|
for _ in mouse_events.read()
|
||||||
position: camera_transform.translation(),
|
|
||||||
direction: camera_transform.forward(),
|
|
||||||
speed: 200.0,
|
|
||||||
radius: 0.008,
|
|
||||||
density: 11.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
bullet_events.send_batch(mouse_events.read()
|
|
||||||
.filter(|event| event.button == MouseButton::Left)
|
.filter(|event| event.button == MouseButton::Left)
|
||||||
.filter(|event| event.state == ButtonState::Pressed)
|
.filter(|event| event.state == ButtonState::Pressed)
|
||||||
.map(|_| bullet_fired_event)
|
{
|
||||||
.collect::<Vec<_>>()
|
projectiles.spawn(ProjectileData {
|
||||||
);
|
position: camera_transform.translation(),
|
||||||
|
direction: camera_transform.forward(),
|
||||||
|
speed: 900.0,
|
||||||
|
mass: 1.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,7 @@ use std::collections::HashMap;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use avian3d::prelude::*;
|
use avian3d::prelude::*;
|
||||||
|
|
||||||
use crate::physics::Bullet;
|
use crate::physics::ProjectileHitEvent;
|
||||||
use crate::util::physics::CollisionQuery;
|
|
||||||
|
|
||||||
const WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1);
|
const WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1);
|
||||||
const WALL_FACE_Z: f32 = -25.0;
|
const WALL_FACE_Z: f32 = -25.0;
|
||||||
@ -29,7 +28,7 @@ impl Plugin for TargetWallPlugin
|
|||||||
});
|
});
|
||||||
|
|
||||||
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::<CollisionEnded>));
|
app.add_systems(PostProcessCollisions, on_targets_shot.run_if(on_event::<ProjectileHitEvent>));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,34 +149,16 @@ fn setup_target_wall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_targets_shot(
|
fn on_targets_shot(
|
||||||
mut commands: Commands,
|
mut query: Query<(&TargetHandle, &mut Transform), With<Target>>,
|
||||||
mut collision_events: EventReader<CollisionEnded>,
|
mut projectile_hit_events: EventReader<ProjectileHitEvent>,
|
||||||
mut query_set: ParamSet<(
|
|
||||||
Query<Entity, With<Bullet>>,
|
|
||||||
Query<(&TargetHandle, &mut Transform), With<Target>>,
|
|
||||||
)>,
|
|
||||||
mut target_resources: ResMut<TargetResources>,
|
mut target_resources: ResMut<TargetResources>,
|
||||||
) {
|
) {
|
||||||
for collision in collision_events.read()
|
for hit_event in projectile_hit_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
|
continue;
|
||||||
//
|
|
||||||
let bullet_query = &mut query_set.p0();
|
|
||||||
let Ok(bullet_entity) = collision.query_unique(bullet_query) else {
|
|
||||||
continue; // the collision doesn't involve a bullet
|
|
||||||
};
|
|
||||||
//
|
|
||||||
let target_query = &mut query_set.p1();
|
|
||||||
let Ok((target_handle, mut target_transform)) = collision.query_unique(target_query) else {
|
|
||||||
continue; // the collision doesn't involve a target
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// move the target
|
*transform = target_resources.random_transform(handle);
|
||||||
*target_transform = target_resources.random_transform(target_handle);
|
|
||||||
|
|
||||||
// despawn the bullet
|
|
||||||
// TODO: this should be handled by the projectiles system
|
|
||||||
commands.entity(bullet_entity).despawn_recursive();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::app::PluginGroupBuilder;
|
use bevy::app::PluginGroupBuilder;
|
||||||
|
|
||||||
mod projectiles;
|
mod projectile;
|
||||||
pub use projectiles::*;
|
pub use projectile::*;
|
||||||
|
|
||||||
mod world_bounds;
|
mod world_bounds;
|
||||||
pub use world_bounds::*;
|
pub use world_bounds::*;
|
||||||
@ -19,7 +19,7 @@ impl PluginGroup for PhysicsPlugins
|
|||||||
.add_group(avian3d::PhysicsPlugins::default())
|
.add_group(avian3d::PhysicsPlugins::default())
|
||||||
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
|
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
|
||||||
|
|
||||||
.add(ProjectilesPlugin)
|
.add(ProjectilePlugin)
|
||||||
.add(WorldBoundsPlugin)
|
.add(WorldBoundsPlugin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,87 +0,0 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use avian3d::prelude::*;
|
|
||||||
|
|
||||||
use crate::util::ecs::PathTracer;
|
|
||||||
|
|
||||||
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(Sphere::new(1.0))),
|
|
||||||
material: MeshMaterial3d(materials.add(StandardMaterial {
|
|
||||||
base_color: Color::srgb(0.7, 0.1, 0.1),
|
|
||||||
..default()
|
|
||||||
})),
|
|
||||||
collider: Collider::sphere(1.0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Event, Copy, Clone, PartialEq, Debug)]
|
|
||||||
pub struct BulletFiredEvent
|
|
||||||
{
|
|
||||||
pub position: Vec3,
|
|
||||||
pub direction: Dir3,
|
|
||||||
pub speed: f32,
|
|
||||||
pub radius: f32,
|
|
||||||
pub density: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct Bullet;
|
|
||||||
|
|
||||||
fn on_bullet_fired_event(
|
|
||||||
mut commands: Commands,
|
|
||||||
mut events: EventReader<BulletFiredEvent>,
|
|
||||||
bullet_resources: Res<BulletResources>,
|
|
||||||
) {
|
|
||||||
let to_projectile = |event: &BulletFiredEvent|
|
|
||||||
{
|
|
||||||
let transform = Transform::IDENTITY
|
|
||||||
.with_translation(event.position)
|
|
||||||
.with_scale(Vec3::splat(event.radius))
|
|
||||||
.looking_to(event.direction, Vec3::Y);
|
|
||||||
|
|
||||||
(
|
|
||||||
Bullet,
|
|
||||||
bullet_resources.mesh.clone(),
|
|
||||||
bullet_resources.material.clone(),
|
|
||||||
bullet_resources.collider.clone(),
|
|
||||||
transform,
|
|
||||||
RigidBody::Dynamic,
|
|
||||||
ColliderDensity(event.density),
|
|
||||||
SpeculativeMargin(event.radius),
|
|
||||||
SweptCcd::LINEAR,
|
|
||||||
LinearVelocity(event.speed * event.direction),
|
|
||||||
PathTracer::default(),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
commands.spawn_batch(events.read()
|
|
||||||
.map(to_projectile)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user