From 35711062517a27ea21387e48b88057defb179947 Mon Sep 17 00:00:00 2001 From: irrlicht Date: Sun, 2 Aug 2026 20:43:52 +0200 Subject: [PATCH] block slope sliding --- src/engine/player.rs | 95 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/src/engine/player.rs b/src/engine/player.rs index e5d03ae..554e768 100644 --- a/src/engine/player.rs +++ b/src/engine/player.rs @@ -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.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();