block slope sliding

This commit is contained in:
2026-08-28 23:31:48 +02:00
parent 7b7228a248
commit 3571106251
+91 -4
View File
@@ -38,7 +38,11 @@ const PITCH_LIMIT: f32 = 1.55;
/// Halbmaße der Spieler-AABB (units): 0.6 m breit/tief, 1.8 m hoch.
/// Öffentlich, weil die Session dieselbe Box gegen Trigger-Zonen prüft.
pub const HALF_EXTENTS: [f32; 3] = [0.3, 0.9, 0.3];
/// Trefferflächen mit Normalen-Y darüber gelten als Boden (~45°-Rampen ok).
/// Grenzwinkel für begehbaren Boden, als Kosinus (= Y-Komponente der
/// Trefferormale einer Fläche mit diesem Neigungswinkel). 0.7 ≈ **45°**:
/// flacher wird gelaufen, steiler wird gerutscht. Das ist *der* Regler fürs
/// Hangverhalten — Wunschwinkel als Kosinus eintragen (30° → 0.866,
/// 50° → 0.643, 60° → 0.5).
const GROUND_NORMAL_Y: f32 = 0.7;
/// Slide-Iterationen pro Schritt (Wände, Ecken, Boden zugleich).
const MAX_SLIDES: u32 = 4;
@@ -103,14 +107,26 @@ impl Player {
self.vel[0] = (f[0] * fwd + r[0] * right) * WALK_SPEED;
self.vel[2] = (f[1] * fwd + r[1] * right) * WALK_SPEED;
// Sprung vor der Gravitation, nur vom Boden (Vorframe-`grounded`) —
// kein Doppelsprung.
// Sprung nur vom Boden (Vorframe-`grounded`) — kein Doppelsprung.
//
// Gravitation wirkt *nur in der Luft*. Am Boden wird die Vertikal-
// geschwindigkeit stattdessen genullt, denn `slide` projiziert sie auf
// die Trefferebene und lässt auf einer Schräge den hangparallelen Rest
// stehen — Hangabtrieb, der sich über die Frames aufschaukelt und den
// Spieler auch sanfte Rampen hinabkriechen lässt. Ohne vertikale
// Geschwindigkeit gibt es nichts zu projizieren, er bleibt stehen; ans
// Gefälle heftet ihn der Ground-Snap unten. Steiler als
// [`GROUND_NORMAL_Y`] wird `grounded` gar nicht erst gesetzt, dort
// greift der Luft-Zweig und der Spieler rutscht wie gewünscht ab.
let jumped = jump && was_grounded;
if jumped {
self.vel[1] = JUMP_SPEED;
self.grounded = false;
}
} else if was_grounded {
self.vel[1] = 0.0;
} else {
self.vel[1] -= GRAVITY * dt;
}
self.move_and_slide(world, dt);
@@ -462,6 +478,77 @@ mod tests {
])
}
/// Keilförmige Rampe, die in +X mit `deg` Grad ansteigt (Fuß bei x=0,
/// Länge 10). Bergab ist damit -X.
fn slope_world(deg: f32) -> CollisionWorld {
let (s, c) = deg.to_radians().sin_cos();
collision::from_brushes(vec![vec![
collision::Plane { n: [-s, c, 0.0], d: 0.0 }, // Rampenoberfläche
collision::Plane { n: [0.0, -1.0, 0.0], d: 0.0 }, // Unterseite (y≥0)
collision::Plane { n: [1.0, 0.0, 0.0], d: 10.0 }, // x≤10
collision::Plane { n: [-1.0, 0.0, 0.0], d: 0.0 }, // x≥0
collision::Plane { n: [0.0, 0.0, 1.0], d: 50.0 },
collision::Plane { n: [0.0, 0.0, -1.0], d: 50.0 },
]])
}
/// Auf eine Rampe fallen lassen und setzen — gibt den Ruhepunkt zurück.
fn settle(w: &CollisionWorld, x: f32, deg: f32) -> Player {
// Über der Fläche starten — mit Abstand, sonst steckt die aufgeblasene
// Box schon in der Rampe und die Trace lässt sie durchfallen.
let y = x * deg.to_radians().tan() + 1.0;
let mut p = Player::new([x, y, 0.0]);
for _ in 0..60 { p.step(w, 0.0, 0.0, false, 1.0 / 60.0); }
p
}
#[test]
fn stands_still_on_walkable_slope() {
// 30° < Grenzwinkel: ohne Eingabe darf der Spieler nicht abwärts
// kriechen — sonst ist der Hangabtrieb zurück.
let w = slope_world(30.0);
let mut p = settle(&w, 5.0, 30.0);
assert!(p.grounded, "sollte auf der 30°-Rampe stehen: {:?}", p.pos);
let (x0, y0) = (p.pos[0], p.pos[1]);
for _ in 0..300 { p.step(&w, 0.0, 0.0, false, 1.0 / 60.0); } // 5 Sekunden
assert!((p.pos[0] - x0).abs() < 0.01, "kein Hangabtrieb: x {x0} -> {}", p.pos[0]);
assert!((p.pos[1] - y0).abs() < 0.01, "keine Höhendrift: y {y0} -> {}", p.pos[1]);
assert!(p.grounded, "bleibt am Boden");
}
#[test]
fn slides_down_steep_slope() {
// 60° > Grenzwinkel: hier soll er abrutschen (bergab = -X).
let w = slope_world(60.0);
let mut p = settle(&w, 5.0, 60.0);
assert!(!p.grounded, "zu steil zum Stehen: {:?}", p.pos);
let (x0, y0) = (p.pos[0], p.pos[1]);
for _ in 0..60 { p.step(&w, 0.0, 0.0, false, 1.0 / 60.0); }
assert!(p.pos[0] < x0 - 0.5, "sollte hangabwärts rutschen: x {x0} -> {}", p.pos[0]);
assert!(p.pos[1] < y0 - 0.5, "und dabei sinken: y {y0} -> {}", p.pos[1]);
}
#[test]
fn walks_up_and_down_walkable_slope() {
// Der stehenbleibende Spieler muss trotzdem beide Richtungen laufen.
let w = slope_world(30.0);
let mut up = settle(&w, 5.0, 30.0);
up.yaw = -std::f32::consts::FRAC_PI_2; // vorwärts → +X, bergauf
let y0 = up.pos[1];
for _ in 0..60 { up.step(&w, 1.0, 0.0, false, 1.0 / 60.0); }
assert!(up.pos[1] > y0 + 0.5, "bergauf steigt y: {y0} -> {}", up.pos[1]);
assert!(up.grounded, "bleibt bergauf am Boden");
let mut down = settle(&w, 5.0, 30.0);
down.yaw = std::f32::consts::FRAC_PI_2; // vorwärts → -X, bergab
let y0 = down.pos[1];
for i in 0..60 {
down.step(&w, 1.0, 0.0, false, 1.0 / 60.0);
assert!(down.grounded, "bergab am Boden kleben (i={i}, {:?})", down.pos);
}
assert!(down.pos[1] < y0 - 0.5, "bergab sinkt y: {y0} -> {}", down.pos[1]);
}
#[test]
fn walks_down_slope_across_seams_at_60fps() {
let w = slope_between_floors();