churn debugging bullet physics

This commit is contained in:
Robert Fry 2025-01-08 14:28:34 +00:00
parent 14e383864d
commit 5ea8392068
Signed by: robertfry
GPG Key ID: E89FFC8597BFE26C
11 changed files with 348 additions and 139 deletions

View File

@ -1,60 +0,0 @@
use bevy::prelude::*;
use avian3d::prelude::*;
mod target_wall;
pub struct LevelPlugin;
impl Plugin for LevelPlugin
{
fn build(&self, app: &mut App)
{
app.add_plugins(target_wall::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),
));
}

View File

@ -1,8 +1,15 @@
use bevy::input::ButtonState;
use bevy::prelude::*;
use bevy::input::mouse::MouseButtonInput;
use avian3d::prelude::*;
mod player;
mod level;
use crate::physics::BulletFiredEvent;
use crate::ui::camera::CameraController;
use crate::ui::cursor::CursorGrabState;
mod target_wall;
use target_wall::TargetWallPlugin;
pub struct GamePlugin;
@ -10,7 +17,92 @@ impl Plugin for GamePlugin
{
fn build(&self, app: &mut App)
{
app.add_plugins(player::PlayerPlugin);
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, 2.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 bullet_events: EventWriter<BulletFiredEvent>,
) {
let camera_transform = camera_query.into_inner();
let bullet_fired_event = BulletFiredEvent {
position: camera_transform.translation() + Dir3::NEG_X * 2.0,
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.state == ButtonState::Pressed)
.map(|_| bullet_fired_event)
.collect::<Vec<_>>()
);
}

View File

@ -1,68 +0,0 @@
use bevy::prelude::*;
use bevy::input::{mouse::MouseButtonInput, ButtonState};
use avian3d::prelude::*;
use crate::physics::BulletFiredEvent;
use crate::ui::camera::CameraController;
use crate::ui::cursor::CursorGrabState;
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin
{
fn build(&self, app: &mut App)
{
app.add_systems(Startup, setup_player);
app.add_systems(Update, do_shoot_on_left_click
.run_if(in_state(CursorGrabState(true)))
.run_if(on_event::<MouseButtonInput>)
);
}
}
#[derive(Component)]
pub struct Player;
fn setup_player(
mut commands: Commands,
) {
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 {
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: Single<&GlobalTransform, With<Camera>>,
mut mouse_events: EventReader<MouseButtonInput>,
mut bullet_events: EventWriter<BulletFiredEvent>,
) {
let transform = query.into_inner();
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,
});
}
}

View File

@ -150,6 +150,7 @@ fn setup_target_wall(
}
fn on_targets_shot(
mut commands: Commands,
mut collision_events: EventReader<CollisionEnded>,
mut query_set: ParamSet<(
Query<Entity, With<Bullet>>,
@ -163,7 +164,7 @@ fn on_targets_shot(
// between a bullet and a target
//
let bullet_query = &mut query_set.p0();
let Ok(_) = collision.query_unique(bullet_query) else {
let Ok(bullet_entity) = collision.query_unique(bullet_query) else {
continue; // the collision doesn't involve a bullet
};
//
@ -174,5 +175,9 @@ fn on_targets_shot(
// 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();
}
}

View File

@ -12,14 +12,14 @@ pub struct PhysicsPlugins;
impl PluginGroup for PhysicsPlugins
{
fn build(self) -> bevy::app::PluginGroupBuilder
fn build(self) -> PluginGroupBuilder
{
PluginGroupBuilder::start::<Self>()
.add_group(avian3d::PhysicsPlugins::default())
.add(avian3d::debug_render::PhysicsDebugPlugin::default())
.add(WorldBoundsPlugin)
.add(ProjectilesPlugin)
.add(WorldBoundsPlugin)
}
}

View File

@ -2,6 +2,8 @@
use bevy::prelude::*;
use avian3d::prelude::*;
use crate::util::ecs::PathTracer;
pub struct ProjectilesPlugin;
impl Plugin for ProjectilesPlugin
@ -38,14 +40,14 @@ fn setup_projectiles(
});
}
#[derive(Event, Debug)]
#[derive(Event, Copy, Clone, PartialEq, Debug)]
pub struct BulletFiredEvent
{
pub position: Vec3,
pub direction: Dir3,
pub speed: f32,
pub radius: f32,
pub density: f32,
pub velocity: f32,
}
#[derive(Component)]
@ -71,9 +73,10 @@ fn on_bullet_fired_event(
transform,
RigidBody::Dynamic,
ColliderDensity(event.density),
LinearVelocity(event.velocity * event.direction),
SpeculativeMargin(0.0),
SpeculativeMargin(event.radius),
SweptCcd::LINEAR,
LinearVelocity(event.speed * event.direction),
PathTracer::default(),
)
};

View 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
View 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
View 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);
}
}
}

View File

@ -1,2 +1,21 @@
use bevy::app::{PluginGroup, PluginGroupBuilder};
pub mod ecs;
use ecs::UtilEcsPlugins;
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)
}
}

View File

@ -1,3 +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>()
}
}