#![warn(missing_docs)] // TODO: add more precise crate documentation //! Simple 2D collision system, inspired by Godot Engine. use bevy::math::bounding::IntersectsVolume; use bevy::{math::bounding::{Aabb2d, BoundingCircle}, prelude::*}; #[derive(Component, PartialEq, Debug)] /// Enum of available shapes pub enum CollisionShape { /// Rectangle shape Aabb { /// The shape itself, tied to [`Transform`] shape: Aabb2d, /// 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 [`Transform`] shape: BoundingCircle, /// Start offset, used to calculate current position inside shape. offset: Vec2, }, } impl CollisionShape { /// Returns true if shapes intersect pub fn intersects(&self, other: &CollisionShape) -> bool { match self { Self::Aabb { shape: aabb, .. } => { match other { CollisionShape::Aabb { shape: other_aabb, .. } => aabb.intersects(other_aabb), CollisionShape::Circle { shape: circle, .. } => aabb.intersects(circle), } }, Self::Circle{ shape: circle, .. } => { match other { CollisionShape::Aabb { shape: aabb, .. } => circle.intersects(aabb), CollisionShape::Circle { shape: other_circle, .. } => circle.intersects(other_circle), } }, } } /// 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, PartialEq, Debug)] #[require(Transform)] /// Component, handling multiple collision shapes for single entity pub struct Collider { /// Shapes of the collider pub shapes: Vec, /// Bitmask of collider groups. /// Setting this to 0 makes collider invisible for others pub mask: usize, /// Bitmask of groups which will be checked for collision. /// Setting this to 0 makes collider ignorant to others pub check_mask: usize, } impl Collider { #[inline] /// Returns true if any of self shapes intersect other's shapes, ignoring groups pub fn intersects_unchecked(&self, other: &Collider) -> bool { for shape in self.shapes.iter() { for other_shape in other.shapes.iter() { if shape.intersects(other_shape) { return true; } } } false } /// 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 { if self.check_mask & other.mask == 0 { return false; } self.intersects_unchecked(other) } /// 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 = 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 = new_position.x + offset.x; shape.center.y = new_position.y + offset.y; }, } } } } /// Update collider shapes to match new [`Transform`] pub fn update_collider_shapes( colliders: Query<(&mut Collider, &Transform), Changed>, ) { for (mut collider, transform) in colliders { collider.update_shapes_position(&transform.translation.xy()); } } #[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, } fn collider_filter(checker: &&Collider, checkable: &&Collider) -> bool { checker != checkable && checker.check_mask != 0 } /// 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)>, ) { for (collider, entity) in colliders.iter() { for (other_collider, other_entity) in colliders.iter().filter(|(c, _)| collider_filter(&collider, c)) { if collider.intersects(other_collider) { commands.trigger(CollisionEvent { entity, detected: other_entity }); } } } } /// Plugin that adds collision systems pub struct CollisionPlugin; impl Plugin for CollisionPlugin { fn build(&self, app: &mut App) { app.add_systems(PostUpdate, (update_collider_shapes, check_for_collisions).chain()); } }