diff --git a/src/lib.rs b/src/lib.rs index 5ae66e6..48e3545 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,29 @@ +#![warn(missing_docs)] + +// TODO: add more precise crate documentation +//! Simple 2D collision system, inspired by Godot Engine. + use bevy::math::bounding::IntersectsVolume; -#[warn(missing_docs)] use bevy::{math::bounding::{Aabb2d, BoundingCircle}, prelude::*}; -// TODO: add more precise crate documentation -// TODO: implement event emitting on collision -/// Simple 2D collision system, inspired by Godot Engine. - -#[derive(Component)] +#[derive(Component, PartialEq, Debug)] /// Enum of available shapes pub enum CollisionShape { /// Rectangle shape Aabb { - /// The shape itself, tied to **global** position + /// The shape itself, tied to [`Transform`] shape: Aabb2d, - /// Start offset, used to calculate global position. + /// Start offset, used to calculate current position inside shape. offset: Vec2, /// Size of the shape, used to calculate global position. size: Vec2, }, /// Circle shape Circle { - /// The shape itself, tied to **global** position + /// The shape itself, tied to [`Transform`] shape: BoundingCircle, - /// Start offset, used to calculate global position + /// Start offset, used to calculate current position inside shape. offset: Vec2, }, } @@ -46,9 +46,35 @@ impl CollisionShape { }, } } + + /// Create [`CollisionShape::Aabb`] + pub fn new_aabb(offset: Vec2, size: Vec2) -> Self { + let half_size = Vec2 { x: size.x / 2f32, y: size.y / 2f32 }; + Self::Aabb { shape: Aabb2d::new(offset, half_size), offset, size } + } + + /// Create [`CollisionShape::Circle`] + pub fn new_circle(offset: Vec2, radius: f32) -> Self { + Self::Circle { shape: BoundingCircle::new(offset, radius), offset } + } + + /// Returns current centered offset for the shape + pub fn current_offset(&self) -> Vec2 { + match self { + Self::Aabb { shape, .. } => { + Vec2::new( + (shape.min.x + shape.max.x) / 2., + (shape.min.y + shape.max.y) / 2., + ) + }, + Self::Circle { shape, .. } => { + shape.center + }, + } + } } -#[derive(Component)] +#[derive(Component, PartialEq, Debug)] #[require(Transform)] /// Component, handling multiple collision shapes for single entity pub struct Collider { @@ -79,50 +105,80 @@ impl Collider { /// Returns true if any of self shapes intersect other's shapes, respecting groups. /// Wrapper around [intersects_unchecked](Self::intersects_unchecked) pub fn intersects(&self, other: &Collider) -> bool { + println!("groups: {}", self.check_mask & other.mask ); if self.check_mask & other.mask == 0 { return false; } self.intersects_unchecked(other) } - /// Update shapes global position - pub fn update_shapes_position(&mut self, global_position: &Vec2) { + /// Update shapes' current position + pub fn update_shapes_position(&mut self, new_position: &Vec2) { for shape in self.shapes.iter_mut() { match shape { CollisionShape::Aabb { shape, offset, size } => { - shape.min.x = global_position.x + offset.x; - shape.min.y = global_position.y + offset.y; + shape.min.x = new_position.x + offset.x - size.x / 2.; + shape.min.y = new_position.y + offset.y - size.y / 2.; shape.max.x = shape.min.x + size.x; shape.max.y = shape.min.y + size.y; }, CollisionShape::Circle { shape, offset } => { - shape.center.x = global_position.x + offset.x; - shape.center.y = global_position.y + offset.y; + shape.center.x = new_position.x + offset.x; + shape.center.y = new_position.y + offset.y; }, } } } } -#[derive(Component)] +#[derive(Component, Debug)] +/// Add this component on entities, which have their [`Transform`] updated. +/// Used for [`update_collider_shapes`] system pub struct UpdateShapes; -/// Update collider shapes to match new global transform +/// Update collider shapes to match new [`Transform`] pub fn update_collider_shapes( mut commands: Commands, - colliders: Query<(&mut Collider, &GlobalTransform, Entity), With>, + colliders: Query<(&mut Collider, &Transform, Entity), With>, ) { for (mut collider, transform, entity) in colliders { - collider.update_shapes_position(&transform.translation().xy()); + collider.update_shapes_position(&transform.translation.xy()); commands.entity(entity).remove::(); } } +#[derive(EntityEvent, Debug)] +/// Emitted when colliders intersect in [`check_for_collisions`] system +pub struct CollisionEvent { + /// Entity, whose [`Collider`] detected another [`Collider`] + pub entity: Entity, + /// Entity, whose [`Collider`] was detected as intersecting + pub detected: Entity, +} + +/// Check each [`Collider`] for collisions with another colliders. +/// Triggers [`CollisionEvent`] on collider intersection +pub fn check_for_collisions( + mut commands: Commands, + colliders: Query<(&Collider, Entity)>, +) { + let mut visited_colliders: Vec<&Collider> = Vec::new(); + for (collider, entity) in colliders.iter() { + for (other_collider, other_entity) in colliders.iter().filter(|(c, _)| !visited_colliders.contains(c)) { + if collider.intersects(other_collider) { + commands.trigger(CollisionEvent { entity, detected: other_entity }); + } + } + visited_colliders.push(collider); + println!("{visited_colliders:?}"); + } +} + /// Plugin that adds collision systems pub struct CollisionPlugin; impl Plugin for CollisionPlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, update_collider_shapes); + app.add_systems(PostUpdate, (update_collider_shapes, check_for_collisions).chain()); } } diff --git a/tests/collider_position.rs b/tests/collider_position.rs new file mode 100644 index 0000000..e009a70 --- /dev/null +++ b/tests/collider_position.rs @@ -0,0 +1,130 @@ +use bevy::prelude::*; +use bevy_collision_plugin::{Collider, CollisionEvent, CollisionPlugin, CollisionShape, UpdateShapes}; + +const CHECKED_GROUPS: usize = 1 >> 0 | 1 >> 1; + +#[derive(Component)] +struct MovingCollider; + +fn moving_collider_bundle() -> impl Bundle { + ( + Collider { + shapes: vec![ + CollisionShape::new_aabb(Vec2::new(0., 0.), Vec2::new(10., 10.)), + CollisionShape::new_circle(Vec2::new(0., 15.), 5.), + ], + mask: 0, + check_mask: CHECKED_GROUPS, + }, + Transform::from_translation(Vec3::new(0., 0., 0.)), + MovingCollider, + ) +} + +fn zerogroup_collider_bundle() -> impl Bundle { + ( + Collider { + shapes: vec![ + CollisionShape::new_aabb(Vec2::new(0., 0.), Vec2::new(10., 10.)), + ], + mask: 1 >> 0, + check_mask: 0, + }, + Transform::from_translation(Vec3::new(53., 5., 0.)), + ) +} + +fn firstgroup_collider_bundle() -> impl Bundle { + ( + Collider { + shapes: vec![ + CollisionShape::new_circle(Vec2::new(0., 0.), 5.), + ], + mask: 1 >> 1, + check_mask: 0, + }, + Transform::from_translation(Vec3::new(53., 18., 0.)), + ) +} + +fn move_collider( + mut commands: Commands, + query: Query<(Entity, &mut Transform), With>, +) { + for (entity, mut transform) in query { + transform.translation.x += 50.; + commands.entity(entity) + .insert(UpdateShapes); + } +} + +fn setup( + mut commands: Commands, +) { + commands.spawn(moving_collider_bundle()); +} + +fn configured_app() -> App { + let mut app = App::new(); + + app.add_plugins(CollisionPlugin) + .add_systems(Startup, setup) + .add_systems(Update, move_collider); + + app +} + +#[test] +fn collider_shapes_update_properly() { + let mut app = configured_app(); + + app.update(); + + let mut query = app.world_mut().query::<&Collider>(); + for collider in query.iter(app.world()) { + let aabb_offset = collider.shapes[0].current_offset(); + let circle_offset = collider.shapes[1].current_offset(); + + let expected_aabb = Vec2::new(50., 0.); + let expected_circle = Vec2::new(50., 15.); + + assert_eq!(aabb_offset, expected_aabb); + assert_eq!(circle_offset, expected_circle); + } +} + +#[derive(Resource)] +struct CollisionList(Vec); + +fn setup_others( + mut commands: Commands, +) { + commands.spawn(zerogroup_collider_bundle()); + commands.spawn(firstgroup_collider_bundle()); +} + +fn on_collision_event( + event: On, + mut res: ResMut, +) { + if !res.0.contains(&event.detected) { + res.0.push(event.detected); + } +} + +#[test] +fn collider_detects_different_groups() { + let mut app = configured_app(); + + app.insert_resource(CollisionList(vec![])) + .add_systems(Startup, setup_others) + .add_observer(on_collision_event); + + app.update(); + + let collisions = app.world().get_resource::().unwrap(); + let got_collisions = collisions.0.len(); + let expected = 2; + + assert_eq!(got_collisions, expected); +}