use crate::sprite::Sprite;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::{
collections::HashSet,
f32::consts::{PI, TAU},
hash::Hash,
};
pub(crate) struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_message::<CollisionEvent>()
.add_systems(Update, collision_detection);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Message)]
pub struct CollisionEvent {
pub state: CollisionState,
pub pair: CollisionPair,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CollisionState {
Begin,
End,
}
impl CollisionState {
pub fn is_begin(&self) -> bool {
match self {
CollisionState::Begin => true,
CollisionState::End => false,
}
}
pub fn is_end(&self) -> bool {
match self {
CollisionState::Begin => false,
CollisionState::End => true,
}
}
}
#[derive(Debug, Default, Eq, Clone)]
pub struct CollisionPair(pub String, pub String);
impl CollisionPair {
pub fn either_contains<T: Into<String>>(&self, text: T) -> bool {
let text = text.into();
self.0.contains(&text) || self.1.contains(&text)
}
pub fn either_equals_to<T: Into<String>>(&self, text: T) -> bool {
let text = text.into();
(self.0 == text) || (self.1 == text)
}
pub fn either_starts_with<T: Into<String>>(&self, text: T) -> bool {
let text = text.into();
self.0.starts_with(&text) || self.1.starts_with(&text)
}
pub fn one_starts_with<T: Into<String>>(&self, text: T) -> bool {
let text = text.into();
let a_matches = self.0.starts_with(&text);
let b_matches = self.1.starts_with(&text);
(a_matches && !b_matches) || (!a_matches && b_matches)
}
pub fn array(&self) -> [&str; 2] {
[self.0.as_str(), self.1.as_str()]
}
pub fn array_mut(&mut self) -> [&mut String; 2] {
[&mut self.0, &mut self.1]
}
}
impl IntoIterator for CollisionPair {
type Item = String;
type IntoIter = std::array::IntoIter<Self::Item, 2>;
fn into_iter(self) -> Self::IntoIter {
[self.0, self.1].into_iter()
}
}
impl PartialEq for CollisionPair {
fn eq(&self, other: &Self) -> bool {
((self.0 == other.0) && (self.1 == other.1)) || ((self.0 == other.1) && (self.1 == other.0))
}
}
impl Hash for CollisionPair {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if self.0 < self.1 {
self.0.hash(state);
self.1.hash(state);
} else {
self.1.hash(state);
self.0.hash(state);
}
}
}
fn collision_detection(
mut existing_collisions: Local<HashSet<CollisionPair>>,
mut collision_events: MessageWriter<CollisionEvent>,
query: Query<&Sprite>,
) {
let mut current_collisions = HashSet::<CollisionPair>::new();
'outer: for sprite1 in query.iter().filter(|a| a.collision) {
for sprite2 in query.iter().filter(|a| a.collision) {
if sprite1.label == sprite2.label {
continue 'outer;
}
if Collider::colliding(sprite1, sprite2) {
current_collisions
.insert(CollisionPair(sprite1.label.clone(), sprite2.label.clone()));
}
}
}
let beginning_collisions: Vec<_> = current_collisions
.difference(&existing_collisions)
.cloned()
.collect();
collision_events.write_batch(beginning_collisions.iter().map(|p| CollisionEvent {
state: CollisionState::Begin,
pair: p.clone(),
}));
for beginning_collision in beginning_collisions {
existing_collisions.insert(beginning_collision);
}
let ending_collisions: Vec<_> = existing_collisions
.difference(¤t_collisions)
.cloned()
.collect();
collision_events.write_batch(ending_collisions.iter().map(|p| CollisionEvent {
state: CollisionState::End,
pair: p.clone(),
}));
for ending_collision in ending_collisions {
let _ = existing_collisions.remove(&ending_collision);
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub enum Collider {
#[default]
NoCollider,
Poly(Vec<Vec2>),
}
impl Collider {
pub fn rect<T: Into<Vec2>>(topleft: T, bottomright: T) -> Self {
let topleft = topleft.into();
let bottomright = bottomright.into();
Self::Poly(vec![
topleft,
Vec2::new(bottomright.x, topleft.y),
bottomright,
Vec2::new(topleft.x, bottomright.y),
])
}
pub fn poly<T: Into<Vec2> + Copy>(points: &[T]) -> Self {
Self::Poly(points.iter().map(|&x| x.into()).collect())
}
pub fn circle_custom(radius: f32, vertices: usize) -> Self {
let mut points = vec![];
for x in 0..vertices {
let inner = std::f64::consts::TAU / vertices as f64 * x as f64;
let mut inner_x = inner.cos() as f32 * radius;
let mut inner_y = inner.sin() as f32 * radius;
if (inner_x > -0.000001) && (inner_x < 0.000001) {
inner_x = 0.0;
}
if (inner_y > -0.000001) && (inner_y < 0.000001) {
inner_y = 0.0;
}
points.push(Vec2::new(inner_x, inner_y));
}
Self::Poly(points)
}
pub fn circle(radius: f32) -> Self {
Self::circle_custom(radius, 16)
}
pub fn is_poly(&self) -> bool {
matches!(self, Self::Poly(_))
}
pub fn is_convex(&self) -> bool {
if let Collider::Poly(points) = self {
let length = points.len();
if length < 3 {
return false; }
let mut old_x = points[length - 2].x;
let mut old_y = points[length - 2].y;
let mut new_x = points[length - 1].x;
let mut new_y = points[length - 1].y;
let mut new_direction = (new_y - old_y).atan2(new_x - old_x);
let mut angle_sum = 0.0;
let mut old_direction;
let mut orientation = 0.0;
for (idx, newpoint) in points.iter().enumerate() {
old_x = new_x;
old_y = new_y;
old_direction = new_direction;
new_x = newpoint.x;
new_y = newpoint.y;
new_direction = (new_y - old_y).atan2(new_x - old_x);
if (old_x == new_x) && (old_y == new_y) {
return false; }
let mut angle = new_direction - old_direction;
if angle <= -PI {
angle += TAU; } else if angle > PI {
angle -= TAU;
}
if idx == 0 {
if angle == 0.0 {
return false; }
if angle > 0.0 {
orientation = 1.0;
} else {
orientation = -1.0;
}
} else if orientation * angle <= 0.0 {
return false;
}
angle_sum += angle;
}
let full_turns = (angle_sum / TAU).abs();
return (full_turns > 0.9999) && (full_turns < 1.0001);
}
false
}
fn rotated(&self, rotation: f32) -> Vec<Vec2> {
let mut rotated_points = Vec::new();
if let Self::Poly(points) = self {
let sin = rotation.sin();
let cos = rotation.cos();
for point in points.iter() {
rotated_points.push(Vec2::new(
point.x * cos - point.y * sin,
point.x * sin + point.y * cos,
));
}
}
rotated_points
}
#[doc(hidden)]
pub fn relative_to(&self, sprite: &Sprite) -> Vec<Vec2> {
self.rotated(sprite.rotation)
.iter()
.map(|&v| v * sprite.scale + sprite.translation) .collect()
}
pub fn points(&self) -> Vec<Vec2> {
if let Self::Poly(points) = self {
points.clone()
} else {
Vec::with_capacity(0)
}
}
pub fn colliding(sprite1: &Sprite, sprite2: &Sprite) -> bool {
use Collider::*;
if let NoCollider = sprite1.collider {
return false;
}
if let NoCollider = sprite2.collider {
return false;
}
if sprite1.collider.is_poly() && sprite2.collider.is_poly() {
let poly1 = sprite1.collider.relative_to(sprite1);
let poly2 = sprite2.collider.relative_to(sprite2);
for poly in [poly1.clone(), poly2.clone()] {
for (idx, &p1) in poly.iter().enumerate() {
let p2 = poly[(idx + 1) % poly.len()];
let normal = Vec2::new(p2.y - p1.y, p1.x - p2.x);
let mut min_a = None;
let mut max_a = None;
for &p in poly1.iter() {
let projected = normal.x * p.x + normal.y * p.y;
if min_a.is_none() || projected < min_a.unwrap() {
min_a = Some(projected);
}
if max_a.is_none() || projected > max_a.unwrap() {
max_a = Some(projected);
}
}
let mut min_b = None;
let mut max_b = None;
for &p in poly2.iter() {
let projected = normal.x * p.x + normal.y * p.y;
if min_b.is_none() || projected < min_b.unwrap() {
min_b = Some(projected);
}
if max_b.is_none() || projected > max_b.unwrap() {
max_b = Some(projected);
}
}
if max_a < min_b || max_b < min_a {
return false;
}
}
}
return true;
}
false
}
}