use crate::math::{Position, Size, Vec2};
use crate::entity::EntityId;
use crate::EngineResult;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use parry2d::{
shape::{Cuboid, Ball, SharedShape},
math::{Point, Vector},
query::Ray,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CollisionShape {
Rectangle,
Circle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RigidBody {
pub position: Position,
pub velocity: Vec2,
pub acceleration: Vec2,
pub mass: f32,
pub is_static: bool,
pub drag: f32,
pub bounce: f32,
}
impl Default for RigidBody {
fn default() -> Self {
Self {
position: Position::new(0.0, 0.0),
velocity: Vec2::new(0.0, 0.0),
acceleration: Vec2::new(0.0, 0.0),
mass: 1.0,
is_static: false,
drag: 0.98,
bounce: 0.5,
}
}
}
impl RigidBody {
pub fn new() -> Self {
Default::default()
}
pub fn with_mass(mut self, mass: f32) -> Self {
self.mass = mass.max(0.001);
self
}
pub fn with_position(mut self, position: Position) -> Self {
self.position = position;
self
}
pub fn with_velocity(mut self, velocity: Vec2) -> Self {
self.velocity = velocity;
self
}
pub fn static_body(mut self) -> Self {
self.is_static = true;
self.mass = f32::INFINITY;
self
}
pub fn apply_force(&mut self, force: Vec2) {
if !self.is_static {
self.acceleration += force / self.mass;
}
}
pub fn apply_impulse(&mut self, impulse: Vec2) {
if !self.is_static {
self.velocity += impulse / self.mass;
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collider {
pub shape: CollisionShape,
pub size: Size,
pub offset: Vec2,
pub is_trigger: bool,
pub collision_layers: u32,
pub collision_mask: u32,
}
impl Default for Collider {
fn default() -> Self {
Self {
shape: CollisionShape::Rectangle,
size: Size::new(32.0, 32.0),
offset: Vec2::new(0.0, 0.0),
is_trigger: false,
collision_layers: 1,
collision_mask: u32::MAX,
}
}
}
impl Collider {
pub fn rectangle(width: f32, height: f32) -> Self {
Self {
shape: CollisionShape::Rectangle,
size: Size::new(width, height),
..Default::default()
}
}
pub fn circle(radius: f32) -> Self {
Self {
shape: CollisionShape::Circle,
size: Size::new(radius * 2.0, radius * 2.0),
..Default::default()
}
}
pub fn trigger(mut self) -> Self {
self.is_trigger = true;
self
}
pub fn with_layers(mut self, layers: u32) -> Self {
self.collision_layers = layers;
self
}
pub fn with_mask(mut self, mask: u32) -> Self {
self.collision_mask = mask;
self
}
}
#[derive(Debug, Clone)]
pub struct CollisionEvent {
pub entity_a: EntityId,
pub entity_b: EntityId,
pub contact_point: Position,
pub normal: Vec2,
pub penetration: f32,
}
pub struct PhysicsWorld {
gravity: Vec2,
rigid_bodies: HashMap<EntityId, RigidBody>,
colliders: HashMap<EntityId, Collider>,
collision_events: Vec<CollisionEvent>,
time_accumulator: f32,
fixed_timestep: f32,
}
impl Default for PhysicsWorld {
fn default() -> Self {
Self::new()
}
}
impl PhysicsWorld {
pub fn new() -> Self {
Self {
gravity: Vec2::new(0.0, -9.81 * 100.0),
rigid_bodies: HashMap::new(),
colliders: HashMap::new(),
collision_events: Vec::new(),
time_accumulator: 0.0,
fixed_timestep: 1.0 / 60.0,
}
}
pub fn set_gravity(&mut self, gravity: Vec2) {
self.gravity = gravity;
}
pub fn add_rigid_body(&mut self, entity: EntityId, body: RigidBody) {
self.rigid_bodies.insert(entity, body);
}
pub fn add_collider(&mut self, entity: EntityId, collider: Collider) {
self.colliders.insert(entity, collider);
}
pub fn get_rigid_body(&self, entity: EntityId) -> Option<&RigidBody> {
self.rigid_bodies.get(&entity)
}
pub fn get_rigid_body_mut(&mut self, entity: EntityId) -> Option<&mut RigidBody> {
self.rigid_bodies.get_mut(&entity)
}
pub fn get_collider(&self, entity: EntityId) -> Option<&Collider> {
self.colliders.get(&entity)
}
pub fn remove_entity(&mut self, entity: EntityId) {
self.rigid_bodies.remove(&entity);
self.colliders.remove(&entity);
}
pub fn update(&mut self, delta_time: f32) -> EngineResult<()> {
self.time_accumulator += delta_time;
self.collision_events.clear();
while self.time_accumulator >= self.fixed_timestep {
self.physics_step(self.fixed_timestep)?;
self.time_accumulator -= self.fixed_timestep;
}
Ok(())
}
fn physics_step(&mut self, dt: f32) -> EngineResult<()> {
for (_, body) in self.rigid_bodies.iter_mut() {
if !body.is_static {
body.acceleration += self.gravity;
body.velocity += body.acceleration * dt;
body.velocity *= body.drag;
body.position.x += body.velocity.x * dt;
body.position.y += body.velocity.y * dt;
body.acceleration = Vec2::new(0.0, 0.0);
}
}
self.detect_collisions()?;
Ok(())
}
fn detect_collisions(&mut self) -> EngineResult<()> {
let entities: Vec<EntityId> = self.colliders.keys().copied().collect();
for i in 0..entities.len() {
for j in (i + 1)..entities.len() {
let entity_a = entities[i];
let entity_b = entities[j];
let collider_a = self.colliders.get(&entity_a).unwrap().clone();
let collider_b = self.colliders.get(&entity_b).unwrap().clone();
if (collider_a.collision_layers & collider_b.collision_mask) == 0 ||
(collider_b.collision_layers & collider_a.collision_mask) == 0 {
continue;
}
if let (Some(body_a), Some(body_b)) = (
self.rigid_bodies.get(&entity_a),
self.rigid_bodies.get(&entity_b)
) {
if let Some(collision) = self.check_collision(
entity_a, &body_a.position, &collider_a,
entity_b, &body_b.position, &collider_b
) {
self.collision_events.push(collision.clone());
if !collider_a.is_trigger && !collider_b.is_trigger {
self.resolve_collision(collision);
}
}
}
}
}
Ok(())
}
fn check_collision(
&self,
entity_a: EntityId,
pos_a: &Position,
collider_a: &Collider,
entity_b: EntityId,
pos_b: &Position,
collider_b: &Collider,
) -> Option<CollisionEvent> {
match (collider_a.shape, collider_b.shape) {
(CollisionShape::Rectangle, CollisionShape::Rectangle) => {
self.check_aabb_collision(entity_a, pos_a, collider_a, entity_b, pos_b, collider_b)
}
(CollisionShape::Circle, CollisionShape::Circle) => {
self.check_circle_collision(entity_a, pos_a, collider_a, entity_b, pos_b, collider_b)
}
_ => {
self.check_aabb_collision(entity_a, pos_a, collider_a, entity_b, pos_b, collider_b)
}
}
}
fn check_aabb_collision(
&self,
entity_a: EntityId,
pos_a: &Position,
collider_a: &Collider,
entity_b: EntityId,
pos_b: &Position,
collider_b: &Collider,
) -> Option<CollisionEvent> {
let a_min_x = pos_a.x + collider_a.offset.x - collider_a.size.x / 2.0;
let a_max_x = pos_a.x + collider_a.offset.x + collider_a.size.x / 2.0;
let a_min_y = pos_a.y + collider_a.offset.y - collider_a.size.y / 2.0;
let a_max_y = pos_a.y + collider_a.offset.y + collider_a.size.y / 2.0;
let b_min_x = pos_b.x + collider_b.offset.x - collider_b.size.x / 2.0;
let b_max_x = pos_b.x + collider_b.offset.x + collider_b.size.x / 2.0;
let b_min_y = pos_b.y + collider_b.offset.y - collider_b.size.y / 2.0;
let b_max_y = pos_b.y + collider_b.offset.y + collider_b.size.y / 2.0;
if a_max_x >= b_min_x && a_min_x <= b_max_x && a_max_y >= b_min_y && a_min_y <= b_max_y {
let overlap_x = (a_max_x - b_min_x).min(b_max_x - a_min_x);
let overlap_y = (a_max_y - b_min_y).min(b_max_y - a_min_y);
let (normal, penetration) = if overlap_x < overlap_y {
let normal_x = if pos_a.x < pos_b.x { -1.0 } else { 1.0 };
(Vec2::new(normal_x, 0.0), overlap_x)
} else {
let normal_y = if pos_a.y < pos_b.y { -1.0 } else { 1.0 };
(Vec2::new(0.0, normal_y), overlap_y)
};
Some(CollisionEvent {
entity_a,
entity_b,
contact_point: Position::new(
(pos_a.x + pos_b.x) / 2.0,
(pos_a.y + pos_b.y) / 2.0,
),
normal,
penetration,
})
} else {
None
}
}
fn check_circle_collision(
&self,
entity_a: EntityId,
pos_a: &Position,
collider_a: &Collider,
entity_b: EntityId,
pos_b: &Position,
collider_b: &Collider,
) -> Option<CollisionEvent> {
let radius_a = collider_a.size.x / 2.0;
let radius_b = collider_b.size.x / 2.0;
let center_a = Vec2::new(pos_a.x + collider_a.offset.x, pos_a.y + collider_a.offset.y);
let center_b = Vec2::new(pos_b.x + collider_b.offset.x, pos_b.y + collider_b.offset.y);
let distance_vec = center_b - center_a;
let distance = distance_vec.length();
let min_distance = radius_a + radius_b;
if distance < min_distance {
let normal = if distance > 0.0 {
distance_vec / distance
} else {
Vec2::new(1.0, 0.0)
};
Some(CollisionEvent {
entity_a,
entity_b,
contact_point: Position::new(
center_a.x + normal.x * radius_a,
center_a.y + normal.y * radius_a,
),
normal,
penetration: min_distance - distance,
})
} else {
None
}
}
fn resolve_collision(&mut self, collision: CollisionEvent) {
let body_a = self.rigid_bodies.get(&collision.entity_a).cloned();
let body_b = self.rigid_bodies.get(&collision.entity_b).cloned();
if let (Some(mut body_a), Some(mut body_b)) = (body_a, body_b) {
if body_a.is_static && body_b.is_static {
return;
}
let total_mass = if body_a.is_static {
body_b.mass
} else if body_b.is_static {
body_a.mass
} else {
body_a.mass + body_b.mass
};
let separation = collision.normal * collision.penetration;
if !body_a.is_static {
let ratio_a = if body_b.is_static { 1.0 } else { body_b.mass / total_mass };
body_a.position.x -= separation.x * ratio_a;
body_a.position.y -= separation.y * ratio_a;
}
if !body_b.is_static {
let ratio_b = if body_a.is_static { 1.0 } else { body_a.mass / total_mass };
body_b.position.x += separation.x * ratio_b;
body_b.position.y += separation.y * ratio_b;
}
let relative_velocity = body_b.velocity - body_a.velocity;
let velocity_along_normal = relative_velocity.x * collision.normal.x +
relative_velocity.y * collision.normal.y;
if velocity_along_normal > 0.0 {
return;
}
let restitution = (body_a.bounce + body_b.bounce) / 2.0;
let impulse_magnitude = -(1.0 + restitution) * velocity_along_normal /
(1.0/body_a.mass + 1.0/body_b.mass);
let impulse = collision.normal * impulse_magnitude;
if !body_a.is_static {
body_a.velocity -= impulse / body_a.mass;
}
if !body_b.is_static {
body_b.velocity += impulse / body_b.mass;
}
self.rigid_bodies.insert(collision.entity_a, body_a);
self.rigid_bodies.insert(collision.entity_b, body_b);
}
}
pub fn raycast(&self, origin: Position, direction: Vec2, max_distance: f32) -> Option<(EntityId, f32, Position)> {
let ray = Ray::new(
Point::new(origin.x, origin.y),
Vector::new(direction.x, direction.y)
);
let mut closest_hit: Option<(EntityId, f32, Position)> = None;
for (&entity_id, collider) in &self.colliders {
if let Some(body) = self.rigid_bodies.get(&entity_id) {
let shape = match collider.shape {
CollisionShape::Rectangle => {
SharedShape::new(Cuboid::new(Vector::new(
collider.size.x / 2.0,
collider.size.y / 2.0,
)))
}
CollisionShape::Circle => {
SharedShape::new(Ball::new(collider.size.x / 2.0))
}
};
let pos = Point::new(
body.position.x + collider.offset.x,
body.position.y + collider.offset.y,
);
if let Some(toi) = shape.cast_ray(&pos.into(), &ray, max_distance, true) {
let hit_point = Position::new(
origin.x + direction.x * toi,
origin.y + direction.y * toi,
);
if closest_hit.is_none() || toi < closest_hit.as_ref().unwrap().1 {
closest_hit = Some((entity_id, toi, hit_point));
}
}
}
}
closest_hit
}
pub fn get_collision_events(&self) -> &[CollisionEvent] {
&self.collision_events
}
}