Skip to content

Commit

Permalink
✨ explosion damage
Browse files Browse the repository at this point in the history
  • Loading branch information
fabienjuif committed Nov 12, 2023
1 parent 4ce1e60 commit ef9d564
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/castles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ impl CastleBundle {
.with_health_bar_size(Vec2::new(size.x, 5.)),
rewards: Rewards { gold: 500. },
rigid_body: RigidBody::Dynamic,
collider: Collider::cuboid(size.x / 2., size.y / 2.),
collider: Collider::cuboid((size.x / 2.) * 0.98, (size.y / 2.) * 0.98),
events: ActiveEvents::COLLISION_EVENTS,
mass: ColliderMassProperties::Mass(0.),
}
Expand Down
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod castles;
mod common;
mod health;
mod minions;
mod physics;
mod player;
mod racks;
mod teams;
Expand All @@ -15,6 +16,7 @@ use bevy_rapier2d::prelude::*;
use castles::CastlesPlugin;
use health::HealthPlugin;
use minions::MinionsPlugin;
use physics::PhysicsPlugin;
use player::{LocalPlayer, LocalPlayerPlugin};
use racks::RacksPlugin;
use teams::TeamsPlugin;
Expand All @@ -28,9 +30,10 @@ fn main() {
app.add_plugins((
DefaultPlugins.set(LogPlugin {
level: Level::TRACE,
filter: "wgpu=error,bevy_render=warn,bevy_app=warn,bevy_ecs=warn,naga=warn,gilrs=warn"
filter: "wgpu=error,bevy_render=warn,bevy_app=warn,bevy_ecs=warn,naga=warn,gilrs=warn,game::health=info,game::racks=info"
.to_string(),
}),
PhysicsPlugin,
TeamsPlugin,
MinionsPlugin,
RacksPlugin,
Expand Down
72 changes: 54 additions & 18 deletions src/minions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{common::*, health::Health, teams::Team};
use crate::{common::*, health::Health, physics::CollisionEvent, teams::Team};
use bevy::{
prelude::*,
sprite::MaterialMesh2dBundle,
Expand Down Expand Up @@ -29,7 +29,12 @@ impl Plugin for MinionsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(update_move_minions, check_collisions_minions, decay_life),
(
update_move_minions,
check_collisions_minions,
decay_life,
explosion_damage,
),
)
.add_systems(PostUpdate, (destroy_minions, destroy_after_timer));
}
Expand All @@ -47,7 +52,6 @@ pub struct MinionBundle {
// physics
body: RigidBody,
collider: Collider,
events: ActiveEvents,
timer_destroyable: TimeDestroyable,
}

Expand All @@ -58,9 +62,10 @@ impl MinionBundle {
translation: Vec3,
team: Team,
) -> Self {
let radius = 6.0;
MinionBundle {
mesh: MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::new(6.).into()).into(),
mesh: meshes.add(shape::Circle::new(radius).into()).into(),
material: materials.add(ColorMaterial::from(team.color)),
transform: Transform::from_translation(translation),
..default()
Expand All @@ -85,8 +90,7 @@ impl MinionBundle {
team,
// physics
body: RigidBody::Dynamic,
collider: Collider::ball(6.),
events: ActiveEvents::COLLISION_EVENTS,
collider: Collider::ball(radius * 0.98),
timer_destroyable: TimeDestroyable {
timer: Timer::from_seconds(DESTROY_MINIONS_AFTER_SECS, bevy::time::TimerMode::Once),
},
Expand All @@ -95,7 +99,9 @@ impl MinionBundle {
}

#[derive(Component)]
struct Explosion;
struct Explosion {
pub damage: f32,
}

#[derive(Bundle)]
struct ExplosionBundle {
Expand All @@ -111,23 +117,26 @@ impl ExplosionBundle {
pub fn new(
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<ColorMaterial>>,
translation: Vec3,
mut translation: Vec3,
team: Team,
) -> Self {
let mut color = team.color;
color.set_a(0.4);
let radius = 20.;
translation.z = 10.0;
ExplosionBundle {
mesh: MaterialMesh2dBundle {
mesh: meshes.add(shape::Circle::new(radius).into()).into(),
material: materials.add(ColorMaterial::from(team.color)),
material: materials.add(ColorMaterial::from(color)),
transform: Transform::from_translation(translation),
..default()
},
explosion: Explosion,
explosion: Explosion { damage: 30.0 },
team,
collider: Collider::ball(radius),
collider: Collider::ball(radius * 0.98),
sensor: Sensor,
timer_destroyable: TimeDestroyable {
timer: Timer::from_seconds(0.5, bevy::time::TimerMode::Once),
timer: Timer::from_seconds(0.2, bevy::time::TimerMode::Once),
},
}
}
Expand Down Expand Up @@ -225,10 +234,6 @@ fn destroy_after_timer(
}
}

// maybe this is a bad idea to have a system per component since the collision event is having all contacts
// it makes us loop inside collision events multiple time
// TODO: We can have a more global system to handle that in one loop querying "atker" component (with a team),
// TODO: and putting reward in the team rather than in the player, or we want to stick of having reward on each entity so when a entity die its reward are huge?
// FIXME: With explosion implem, it can be simplify
fn check_collisions_minions(
mut commands: Commands,
Expand All @@ -241,7 +246,7 @@ fn check_collisions_minions(
) {
for collision_event in collision_events.iter() {
match collision_event {
CollisionEvent::Started(e1, e2, _) => {
CollisionEvent::Started(e1, e2) => {
// between minions
if query_minions.contains(*e1) && query_minions.contains(*e2) {
let [(transform_a, team_a, mut minion_a), (_, team_b, _)] =
Expand Down Expand Up @@ -306,7 +311,7 @@ fn check_collisions_minions(

minion.had_exploded = true;
}
CollisionEvent::Stopped(_, _, _) => {}
CollisionEvent::Stopped(_, _) => {}
}
}
}
Expand All @@ -316,3 +321,34 @@ fn decay_life(time: Res<Time>, mut query_minions: Query<&mut Health, With<Minion
health.hit(DECAY_VALUE_PER_SEC * time.delta_seconds());
}
}

fn explosion_damage(
mut collision_events: EventReader<CollisionEvent>,
mut query_hit_entities: Query<&mut Health, Without<Explosion>>,
query_explosions: Query<&Explosion>,
) {
for collision_event in collision_events.iter() {
match collision_event {
CollisionEvent::Started(e1, e2) => {
let explosion = match query_explosions.get(*e1) {
Err(_) => match query_explosions.get(*e2) {
Err(_) => continue,
Ok(value) => value,
},
Ok(value) => value,
};

let mut health = match query_hit_entities.get_mut(*e1) {
Err(_) => match query_hit_entities.get_mut(*e2) {
Err(_) => continue,
Ok(value) => value,
},
Ok(value) => value,
};

health.hit(explosion.damage);
}
CollisionEvent::Stopped(_, _) => {}
}
}
}
144 changes: 144 additions & 0 deletions src/physics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use std::{collections::HashMap, hash::Hash, hash::Hasher};

use bevy::prelude::*;
use bevy_rapier2d::prelude::RapierContext;

#[derive(Event)]
pub enum CollisionEvent {
/// Event occurring when two colliders start colliding
Started(Entity, Entity),
/// Event occurring when two colliders stop colliding
Stopped(Entity, Entity),
}

#[derive(Clone, Copy)]
pub struct EntityPair {
pub entity1: Entity,
pub entity2: Entity,
}

impl EntityPair {
pub fn new(entity1: Entity, entity2: Entity) -> Self {
// order is important this is used for the hash trait impl
if entity1 < entity2 {
return EntityPair { entity1, entity2 };
}
EntityPair {
entity1: entity2,
entity2: entity1,
}
}
}

impl PartialEq for EntityPair {
fn eq(&self, other: &Self) -> bool {
(self.entity1 == other.entity1 && self.entity2 == other.entity2)
|| (self.entity1 == other.entity2 && self.entity2 == other.entity1)
}
}

impl Eq for EntityPair {}

impl Hash for EntityPair {
fn hash<H: Hasher>(&self, state: &mut H) {
self.entity1.hash(state);
self.entity2.hash(state);
}
}

#[derive(Resource, Clone)]
pub struct Collisions {
pub pairs: HashMap<EntityPair, bool>,
}

impl Collisions {
pub fn new() -> Self {
Collisions {
pairs: HashMap::new(),
}
}

pub fn contains(&self, pair: &EntityPair) -> bool {
self.pairs.contains_key(pair)
}

pub fn add(&mut self, pair: EntityPair) {
self.pairs.insert(pair, true);
}

pub fn remove(&mut self, pair: &EntityPair) {
self.pairs.remove(pair);
}
}

pub struct PhysicsPlugin;

impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(Collisions::new())
.add_event::<CollisionEvent>()
.add_systems(PostUpdate, check_collisions);
}
}

fn check_collisions(
mut collisions: ResMut<Collisions>,
rapier_context: Res<RapierContext>,
mut event_writer: EventWriter<CollisionEvent>,
query: Query<Entity>,
) {
let mut before = collisions.clone();
let mut events = Vec::<CollisionEvent>::new();

// first pass detects intersections
for (e1, e2, _) in rapier_context.intersection_pairs() {
if !query.contains(e1) || !query.contains(e2) {
// entity are removed from ECS
// so we are not spawning events for them
// they will be removed in last loop
continue;
}

let pair = EntityPair::new(e1, e2);
before.remove(&pair);
if collisions.contains(&pair) {
// we already have a started event for this intersection
continue;
}

collisions.add(pair);
events.push(CollisionEvent::Started(pair.entity1, pair.entity2));
}

// second pass detects contacts
for c in rapier_context.contact_pairs() {
let e1 = c.collider1();
let e2 = c.collider2();

if !query.contains(e1) || !query.contains(e2) {
// entity are removed from ECS
// so we are not spawning events for them
// they will be removed in last loop
continue;
}

let pair = EntityPair::new(e1, e2);
before.remove(&pair);
if collisions.contains(&pair) {
// we already have a started event for this intersection
continue;
}

collisions.add(pair);
events.push(CollisionEvent::Started(e1, e2));
}

// emit ended collisions
// TODO: implement iter() that return an iterator on the collisions struct
for (pair, _) in before.pairs {
collisions.remove(&pair);
events.push(CollisionEvent::Stopped(pair.entity1, pair.entity2));
}

event_writer.send_batch(events);
}
11 changes: 5 additions & 6 deletions src/player.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::common::*;
use crate::health::Health;
use crate::physics::CollisionEvent;
use crate::racks::{RackBundle, RACK_GOLD_VALUE};
use crate::teams::{Team, Teams};
use bevy::sprite::MaterialMesh2dBundle;
Expand Down Expand Up @@ -60,7 +61,7 @@ impl SwordBundle {
transform: Transform::from_xyz(0.0, 35.0, 10.0),
..default()
},
collider: Collider::cuboid(50.0, 25.),
collider: Collider::cuboid(49.0, 24.),
sensor: Sensor,
sword: Sword {
entity: parent_entity,
Expand Down Expand Up @@ -126,8 +127,7 @@ fn setup(
// },
RigidBody::KinematicVelocityBased,
// RigidBody::Dynamic,
Collider::ball(30.),
ActiveEvents::COLLISION_EVENTS,
Collider::ball(28.),
LocalPlayer {},
Player {
gold: 20.,
Expand Down Expand Up @@ -272,15 +272,14 @@ fn update_ui(
// maybe this is a bad idea to have a system per component since the collision event is having all contacts
// it makes us loop inside collision events multiple time
fn check_collisions_sword(
// mut commands: Commands,
query_swords: Query<(Entity, &Sword)>,
mut query_player: Query<(&mut Player, &Team)>,
mut query_hit_entities: Query<(Option<&Rewards>, &Team, &mut Health)>,
mut collision_events: EventReader<CollisionEvent>,
) {
for collision_event in collision_events.iter() {
match collision_event {
CollisionEvent::Started(e1, e2, _) => {
CollisionEvent::Started(e1, e2) => {
let sword = query_swords
.get(*e1)
.ok()
Expand Down Expand Up @@ -312,7 +311,7 @@ fn check_collisions_sword(
}
}
}
CollisionEvent::Stopped(_, _, _) => {}
CollisionEvent::Stopped(_, _) => {}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/racks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ impl RackBundle {
.with_health_bar_size(Vec2::new(size.x, 5.)),
rewards: Rewards { gold: 100. },
rigid_body: RigidBody::Dynamic,
collider: Collider::cuboid(size.x / 2., size.y / 2.),
collider: Collider::cuboid((size.x / 2.) * 0.98, (size.y / 2.) * 0.98),
events: ActiveEvents::COLLISION_EVENTS,
mass: ColliderMassProperties::Mass(0.),
}
Expand Down

0 comments on commit ef9d564

Please sign in to comment.