Skip to content

Commit

Permalink
use sat theory to detect map collision
Browse files Browse the repository at this point in the history
  • Loading branch information
jjyr committed Sep 20, 2024
1 parent 8780331 commit b67ce42
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 92 deletions.
46 changes: 34 additions & 12 deletions src/collision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ pub(crate) fn entities_separate_on_x_axis(
if bounce > ENTITY_MIN_BOUNCE_VELOCITY {
left.vel.x -= bounce;
}

entity_move(eng, left, Vec2::new(-overlap * left_move, 0.0));
}

Expand Down Expand Up @@ -163,10 +164,10 @@ pub(crate) fn entities_separate_on_y_axis(
fn handle_trace_result(eng: &mut Engine, ent: &mut Ent, t: Trace) {
ent.pos = t.pos;

// FIXME call check collision rule
if t.tile == 0 {
if !t.is_collide {
return;
}
ent.vel = Vec2::ZERO;

eng.collide(ent.ent_ref, t.normal, Some(t.clone()));

Expand Down Expand Up @@ -265,16 +266,37 @@ pub(crate) fn calc_bounds(pos: Vec2, half_size: Vec2, angle: f32) -> Rect {
}
}

pub(crate) fn calc_overlap(w: &mut World, ent1: EntRef, ent2: EntRef) -> Option<Vec2> {
pub(crate) fn calc_ent_overlap(w: &mut World, ent1: EntRef, ent2: EntRef) -> Option<Vec2> {
let [ent1, ent2] = w.many([ent1, ent2]);
calc_overlap(
&Shape {
pos: ent1.pos,
angle: ent1.angle,
half_size: ent1.scaled_size() * 0.5,
},
&Shape {
pos: ent2.pos,
angle: ent2.angle,
half_size: ent2.scaled_size() * 0.5,
},
)
}

pub(crate) struct Shape {
pub pos: Vec2,
pub angle: f32,
pub half_size: Vec2,
}

pub(crate) fn calc_overlap(s1: &Shape, s2: &Shape) -> Option<Vec2> {
let b1 = calc_bounds(s1.pos, s1.half_size, s1.angle);
let b2 = calc_bounds(s2.pos, s2.half_size, s2.angle);
// check bounds
let b1 = ent1.bounds();
let b2 = ent2.bounds();
if !b1.is_touching(&b2) {
return None;
}
// test if ent is rotated
if is_right_angle(ent1.angle) && is_right_angle(ent2.angle) {
if is_right_angle(s1.angle) && is_right_angle(s2.angle) {
// not rotated, calculate overlap with bounds
let overlap_x: f32 = if b1.min.x < b2.min.x {
b1.max.x - b2.min.x
Expand All @@ -294,14 +316,14 @@ pub(crate) fn calc_overlap(w: &mut World, ent1: EntRef, ent2: EntRef) -> Option<
} else {
// rotated, perform sat check
let rect1 = SatRect {
angle: ent1.angle,
pos: ent1.pos,
half_size: ent1.scaled_size() * 0.5,
angle: s1.angle,
pos: s1.pos,
half_size: s1.half_size,
};
let rect2 = SatRect {
angle: ent2.angle,
pos: ent2.pos,
half_size: ent2.scaled_size() * 0.5,
angle: s2.angle,
pos: s2.pos,
half_size: s2.half_size,
};
calc_sat_overlap(&rect1, &rect2)
}
Expand Down
4 changes: 2 additions & 2 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use glam::{UVec2, Vec2};
use crate::{
asset::{AssetManager, FetchedTask},
camera::Camera,
collision::{calc_overlap, entity_move, resolve_collision},
collision::{calc_ent_overlap, entity_move, resolve_collision},
collision_map::{CollisionMap, COLLISION_MAP},
commands::{Command, Commands},
entity::{Ent, EntCollidesMode, EntPhysics, EntRef, EntType, EntTypeId},
Expand Down Expand Up @@ -425,7 +425,7 @@ impl Engine {
break;
}
self.perf.checks += 1;
if let Some(overlap) = calc_overlap(w, ent1, ent2) {
if let Some(overlap) = calc_ent_overlap(w, ent1, ent2) {
let res = {
let [ent1, ent2] = w.many([ent1, ent2]);

Expand Down
117 changes: 39 additions & 78 deletions src/trace.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use glam::{IVec2, Vec2};

use crate::{
collision::is_right_angle,
collision::{calc_overlap, Shape},
prelude::CollisionMap,
sat::{calc_sat_overlap, SatRect},
};

#[derive(Debug, Clone)]
pub struct Trace {
// The tile that was hit. 0 if no hit.
pub tile: i32,
pub is_collide: bool,

// The tile position (in tile space) of the hit
pub tile_pos: IVec2,
Expand All @@ -28,7 +27,7 @@ pub struct Trace {
impl Default for Trace {
fn default() -> Self {
Trace {
tile: 0,
is_collide: false,
pos: Vec2::ZERO,
normal: Vec2::ZERO,
length: 1.,
Expand Down Expand Up @@ -81,16 +80,9 @@ pub(crate) fn trace(
let step_size = vel / steps;

let mut last_tile_pos = IVec2::splat(-16);
let mut extra_step_for_slope = false;

// used to perform sat collision
let sat_rect = SatRect {
angle,
pos: from_center,
half_size,
};
let tile_hf_size = Vec2::splat(map.tile_size * 0.5);
let is_right_angle = is_right_angle(angle);
for i in 0..=(steps as usize) {
let tile_pos: IVec2 = {
let tile_px = corner + step_size * i as f32;
Expand All @@ -114,22 +106,27 @@ pub(crate) fn trace(
.ceil() as i32;
for t in 0..num_tiles {
let tile_pos = IVec2::new(tile_pos.x, tile_pos.y + dir.y as i32 * t);
if !is_right_angle {
// check tile collision with sat
let tile_shape = {
let pos = Vec2::new(
(tile_pos.x as f32) * map.tile_size + tile_hf_size.x,
(tile_pos.y as f32) * map.tile_size + tile_hf_size.y,
);
// check tile collision with sat
let tile_rect = SatRect {

Shape {
angle: 0.0,
half_size: tile_hf_size,
pos,
};
if calc_sat_overlap(&sat_rect, &tile_rect).is_none() {
continue;
}
};
let shape = Shape {
pos: from_center,
angle,
half_size,
};
if let Some(overlap) = calc_overlap(&tile_shape, &shape) {
check_tile(map, from, vel, tile_pos, overlap, &mut res);
}
check_tile(map, from, vel, size, tile_pos, &mut res);
}

last_tile_pos.x = tile_pos.x;
Expand All @@ -151,36 +148,35 @@ pub(crate) fn trace(
.ceil() as i32;
for t in corner_tile_checked..num_tiles {
let tile_pos = IVec2::new(tile_pos.x + dir.x as i32 * t, tile_pos.y);
if !is_right_angle {
// check tile collision with sat
let tile_shape = {
let pos = Vec2::new(
(tile_pos.x as f32) * map.tile_size + tile_hf_size.x,
(tile_pos.y as f32) * map.tile_size + tile_hf_size.y,
);
// check tile collision with sat
let tile_rect = SatRect {

Shape {
angle: 0.0,
half_size: tile_hf_size,
pos,
};
if calc_sat_overlap(&sat_rect, &tile_rect).is_none() {
continue;
}
};
let shape = Shape {
pos: from_center,
angle,
half_size,
};
if let Some(overlap) = calc_overlap(&tile_shape, &shape) {
check_tile(map, from, vel, tile_pos, overlap, &mut res);
}
check_tile(map, from, vel, size, tile_pos, &mut res);
}

last_tile_pos.y = tile_pos.y;
}

// If we collided with a sloped tile, we have to check one more step
// forward because we may still collide with another tile at an
// earlier .length point. For fully solid tiles (id: 1), we can
// return here.
if res.tile > 0 && (res.tile == 1 || extra_step_for_slope) {
res.pos += half_size;
return res;
if res.is_collide {
break;
}
extra_step_for_slope = true;
}

res.pos += half_size;
Expand All @@ -191,65 +187,30 @@ fn check_tile(
map: &CollisionMap,
pos: Vec2,
vel: Vec2,
size: Vec2,
tile_pos: IVec2,
overlap: Vec2,
res: &mut Trace,
) {
if map.is_collide(tile_pos) {
resolve_full_tile(map, pos, vel, size, tile_pos, res);
resolve_full_tile(pos, vel, tile_pos, overlap, res);
}
}

fn resolve_full_tile(
map: &CollisionMap,
pos: Vec2,
vel: Vec2,
size: Vec2,
tile_pos: IVec2,
res: &mut Trace,
) {
fn resolve_full_tile(pos: Vec2, vel: Vec2, tile_pos: IVec2, overlap: Vec2, res: &mut Trace) {
// Resolved position, the minimum resulting x or y position in case of a collision.
// Only the x or y coordinate is correct - depending on if we enter the tile
// horizontaly or vertically. We will recalculate the wrong one again.

let mut rp: Vec2 = Vec2::new(
tile_pos.x as f32 * map.tile_size,
tile_pos.y as f32 * map.tile_size,
) + Vec2::new(
if vel.x > 0. { -size.x } else { map.tile_size },
if vel.y > 0. { -size.y } else { map.tile_size },
);

// The steps from pos to rp
let length;

// If we don't move in Y direction, or we do move in X and the tile
// corners's cross product with the movement vector has the correct sign,
// this is a horizontal collision, otherwise it's vertical.
// float sign = vec2_cross(vel, vec2_sub(rp, pos)) * vel.x * vel.y;
let sign = (vel.x * (rp.y - pos.y) - vel.y * (rp.x - pos.x)) * vel.x * vel.y;

if sign < 0. || vel.y == 0. {
// Horizontal collison (x direction, left or right edge)
length = ((pos.x - rp.x) / vel.x).abs();
if length > res.length {
return;
};
let rp: Vec2 = pos + overlap;
res.normal = overlap.normalize_or_zero();

rp.y = pos.y + length * vel.y;
res.normal = Vec2::new(if vel.x > 0.0 { -1.0 } else { 1.0 }, 0.0);
let length = if overlap.x.abs() > overlap.y.abs() {
(overlap.x / vel.x).abs()
} else {
// Vertical collision (y direction, top or bottom edge)
length = ((pos.y - rp.y) / vel.y).abs();
if length > res.length {
return;
};

rp.x = pos.x + length * vel.x;
res.normal = Vec2::new(0.0, if vel.y > 0.0 { -1.0 } else { 1.0 });
}
(overlap.y / vel.y).abs()
};

res.tile = 1;
res.is_collide = true;
res.tile_pos = tile_pos;
res.length = length;
res.pos = rp;
Expand Down

0 comments on commit b67ce42

Please sign in to comment.