fix: collision tunneling, spawn at surface, physics substepping

- Fix: rewrite resolve_grid_collision to push body out of solid cells
  when center is inside (was only pushing to small radius from center)
- Fix: max velocity clamp (0.8) + 4x substepping prevents tunneling
- Fix: player/goblin spawn at terrain surface instead of mid-air
- Fix: gravity reduced from 0.3 to 0.08 for substepping
- Add: headless mode (--headless-ticks N) dumps grid state to file
- Add: velocity zeroing on collision for stable resting
This commit is contained in:
Emil
2026-06-20 18:26:33 +03:00
parent 68f6292c4d
commit 3cd94a386a
5 changed files with 642 additions and 82 deletions
+88 -46
View File
@@ -33,65 +33,107 @@ pub fn resolve_grid_collision(grid: &Grid, body: &mut SubBody) -> CollisionResul
let min_y = (body.y - r).floor() as i32;
let max_y = (body.y + r).ceil() as i32;
for cy in min_y..=max_y {
for cx in min_x..=max_x {
if !grid.in_bounds(cx, cy) {
continue;
}
let cell = grid.get(cx, cy);
if cell.is_empty() {
continue;
}
for _iteration in 0..3 {
let mut any_collision = false;
if cell.is_liquid() {
result.in_liquid = true;
result.liquid_density = result.liquid_density.max(cell.density());
if cell.material == MaterialId::Lava {
result.touching_lava = true;
for cy in min_y..=max_y {
for cx in min_x..=max_x {
if !grid.in_bounds(cx, cy) {
continue;
}
if cell.material == MaterialId::Acid {
result.touching_acid = true;
let cell = grid.get(cx, cy);
if cell.is_empty() {
continue;
}
apply_liquid_drag(body, cell.density());
continue;
}
if cell.material == MaterialId::Fire {
result.touching_fire = true;
continue;
}
if cell.is_liquid() {
result.in_liquid = true;
result.liquid_density = result.liquid_density.max(cell.density());
if cell.material == MaterialId::Lava {
result.touching_lava = true;
}
if cell.material == MaterialId::Acid {
result.touching_acid = true;
}
apply_liquid_drag(body, cell.density());
continue;
}
if cell.is_solid() {
let closest_x = body.x.max(cx as f32).min((cx + 1) as f32);
let closest_y = body.y.max(cy as f32).min((cy + 1) as f32);
let dx = body.x - closest_x;
let dy = body.y - closest_y;
let dist_sq = dx * dx + dy * dy;
if dist_sq < r * r {
let dist = dist_sq.sqrt();
if dist > 0.0001 {
let overlap = r - dist;
let nx = dx / dist;
let ny = dy / dist;
body.x += nx * overlap;
body.y += ny * overlap;
if ny < -0.5 {
if cell.material == MaterialId::Fire {
result.touching_fire = true;
continue;
}
if cell.is_solid() {
let cell_min_x = cx as f32;
let cell_max_x = (cx + 1) as f32;
let cell_min_y = cy as f32;
let cell_max_y = (cy + 1) as f32;
let inside_x = body.x >= cell_min_x && body.x < cell_max_x;
let inside_y = body.y >= cell_min_y && body.y < cell_max_y;
if inside_x && inside_y {
let dist_left = body.x - cell_min_x;
let dist_right = cell_max_x - body.x;
let dist_top = body.y - cell_min_y;
let dist_bottom = cell_max_y - body.y;
let min_dist = dist_left.min(dist_right).min(dist_top).min(dist_bottom);
if min_dist == dist_top {
body.y = cell_min_y - r;
result.on_ground = true;
} else if min_dist == dist_bottom {
body.y = cell_max_y + r;
} else if min_dist == dist_left {
body.x = cell_min_x - r;
} else {
body.x = cell_max_x + r;
}
let vy = body.vy();
if min_dist == dist_top && vy > 0.0 {
body.set_vel(body.vx(), 0.0);
}
let vx = body.vx();
if (min_dist == dist_left || min_dist == dist_right) && vx != 0.0 {
body.set_vel(0.0, body.vy());
}
any_collision = true;
} else {
let bcx = cx as f32 + 0.5;
let bcy = cy as f32 + 0.5;
let dx = body.x - bcx;
let dy = body.y - bcy;
let dist = (dx * dx + dy * dy).sqrt();
if dist > 0.0001 {
body.x = bcx + dx / dist * r * 1.1;
body.y = bcy + dy / dist * r * 1.1;
let closest_x = body.x.max(cell_min_x).min(cell_max_x);
let closest_y = body.y.max(cell_min_y).min(cell_max_y);
let dx = body.x - closest_x;
let dy = body.y - closest_y;
let dist_sq = dx * dx + dy * dy;
if dist_sq < r * r && dist_sq > 0.0001 {
let dist = dist_sq.sqrt();
let overlap = r - dist;
let nx = dx / dist;
let ny = dy / dist;
body.x += nx * overlap;
body.y += ny * overlap;
if ny < -0.5 {
result.on_ground = true;
if body.vy() > 0.0 {
body.set_vel(body.vx(), 0.0);
}
}
any_collision = true;
}
}
}
}
}
if !any_collision {
break;
}
}
result
+14 -3
View File
@@ -87,14 +87,18 @@ pub struct VerletSolver {
pub gravity: f32,
pub damping: f32,
pub dt: f32,
pub max_vel: f32,
pub substeps: u32,
}
impl VerletSolver {
pub fn new() -> Self {
Self {
gravity: 0.3,
gravity: 0.08,
damping: 0.98,
dt: 1.0,
max_vel: 0.8,
substeps: 4,
}
}
@@ -103,8 +107,15 @@ impl VerletSolver {
if !b.alive {
continue;
}
let vx = (b.x - b.old_x) * self.damping;
let vy = (b.y - b.old_y) * self.damping;
let mut vx = (b.x - b.old_x) * self.damping;
let mut vy = (b.y - b.old_y) * self.damping;
let v_mag = (vx * vx + vy * vy).sqrt();
if v_mag > self.max_vel {
vx = vx / v_mag * self.max_vel;
vy = vy / v_mag * self.max_vel;
}
b.old_x = b.x;
b.old_y = b.y;
b.x += vx + b.ax * self.dt * self.dt;