feat: hitscan projectiles

This commit is contained in:
Robert Fry 2025-01-09 14:23:27 +00:00
parent 6e2d70d376
commit 2f8f81ec37
Signed by: robertfry
GPG Key ID: E89FFC8597BFE26C
5 changed files with 159 additions and 131 deletions

View File

@ -4,7 +4,7 @@ use bevy::prelude::*;
use bevy::input::mouse::MouseButtonInput;
use avian3d::prelude::*;
use crate::physics::BulletFiredEvent;
use crate::physics::{ProjectileData, ProjectileSpawner};
use crate::ui::camera::CameraController;
use crate::ui::cursor::CursorGrabState;
@ -87,22 +87,19 @@ fn setup_level(
fn do_shoot_on_left_click(
camera_query: Single<&GlobalTransform, With<Camera>>,
mut mouse_events: EventReader<MouseButtonInput>,
mut bullet_events: EventWriter<BulletFiredEvent>,
mut projectiles: ResMut<ProjectileSpawner>,
) {
let camera_transform = camera_query.into_inner();
let bullet_fired_event = BulletFiredEvent {
position: camera_transform.translation(),
direction: camera_transform.forward(),
speed: 200.0,
radius: 0.008,
density: 11.0,
};
bullet_events.send_batch(mouse_events.read()
for _ in mouse_events.read()
.filter(|event| event.button == MouseButton::Left)
.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,
});
}
}

View File

@ -4,8 +4,7 @@ use std::collections::HashMap;
use bevy::prelude::*;
use avian3d::prelude::*;
use crate::physics::Bullet;
use crate::util::physics::CollisionQuery;
use crate::physics::ProjectileHitEvent;
const WALL_SIZE: Vec3 = Vec3::new(20.0, 10.0, 0.1);
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(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(
mut commands: Commands,
mut collision_events: EventReader<CollisionEnded>,
mut query_set: ParamSet<(
Query<Entity, With<Bullet>>,
Query<(&TargetHandle, &mut Transform), With<Target>>,
)>,
mut query: Query<(&TargetHandle, &mut Transform), With<Target>>,
mut projectile_hit_events: EventReader<ProjectileHitEvent>,
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
// between a bullet and a target
//
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
let Ok((handle, mut transform)) = query.get_mut(hit_event.entity) else {
continue;
};
// move the target
*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();
*transform = target_resources.random_transform(handle);
}
}

View File

@ -2,8 +2,8 @@
use bevy::prelude::*;
use bevy::app::PluginGroupBuilder;
mod projectiles;
pub use projectiles::*;
mod projectile;
pub use projectile::*;
mod world_bounds;
pub use world_bounds::*;
@ -19,7 +19,7 @@ impl PluginGroup for PhysicsPlugins
.add_group(avian3d::PhysicsPlugins::default())
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
.add(ProjectilesPlugin)
.add(ProjectilePlugin)
.add(WorldBoundsPlugin)
}
}

137
src/physics/projectile.rs Normal file
View 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();
}
}

View File

@ -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<_>>()
);
}