Cleanup
This commit is contained in:
+93
-11
@@ -5,8 +5,20 @@ use crate::engine::map::{self, Map};
|
||||
use crate::engine::player::Player;
|
||||
use crate::engine::{kv, signals, story_ctrl};
|
||||
|
||||
/// Maus-Empfindlichkeit der Spieler-Sicht (Radiant/Pixel).
|
||||
const LOOK_SENS: f32 = 0.0025;
|
||||
/// Maus-Empfindlichkeit der Sicht (Radiant/Pixel). Geteilt zwischen der
|
||||
/// First-Person-Sim hier und der Debug-Flycam im Fenster-Frontend, damit sich
|
||||
/// beide gleich anfühlen (das Frontend referenziert diese Konstante).
|
||||
pub const LOOK_SENS: f32 = 0.0025;
|
||||
|
||||
/// Fester Simulationsschritt (s) = 60 Hz. Die Physik läuft ausschließlich in
|
||||
/// diesem Takt — unabhängig von der Render-Framerate — damit Step-up und
|
||||
/// Ground-Snap sich exakt so verhalten wie in den (bei 1/60 geschriebenen)
|
||||
/// Player-Tests, statt bei 144 Hz oder Rucklern durch andere dt-Werte zu driften.
|
||||
const FIXED_DT: f32 = 1.0 / 60.0;
|
||||
/// Deckel für den Sim-Akkumulator (s): bei einem schweren Ruckler holen wir
|
||||
/// höchstens so viel Zeit nach, sonst spiralt die Sim bei einem Hänger davon
|
||||
/// („spiral of death"). ~0.25 s = maximal 15 Nachhol-Schritte pro Frame.
|
||||
const MAX_ACCUM: f32 = 0.25;
|
||||
|
||||
pub enum Mode {
|
||||
FirstPerson,
|
||||
@@ -29,6 +41,12 @@ pub struct Session {
|
||||
pub player: Player,
|
||||
pub collision: CollisionWorld,
|
||||
signals_path: String,
|
||||
/// Aufgelaufene, noch nicht simulierte Zeit (s) für den Fixed-Timestep.
|
||||
sim_accum: f32,
|
||||
/// Über Frames gepufferte Sprung-Flanke: eine Sprungtaste, die in einen
|
||||
/// Frame ohne Sim-Schritt fällt (hohe FPS), ginge sonst verloren. Wird beim
|
||||
/// nächsten Sub-Schritt konsumiert.
|
||||
pending_jump: bool,
|
||||
}
|
||||
|
||||
/// Kontinuierliche Eingabe für einen Simulationsschritt — vom Frontend pro
|
||||
@@ -66,6 +84,8 @@ impl Session {
|
||||
player: Player::new([0.0, 1.0, 0.0]),
|
||||
collision: CollisionWorld::empty(),
|
||||
signals_path,
|
||||
sim_accum: 0.0,
|
||||
pending_jump: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +101,23 @@ impl Session {
|
||||
/// `exec`. Nur im Spielmodus bewegt sich der Spieler; Menü/Dialog/Flycam
|
||||
/// pausieren die Sim (die Flycam ist reine Frontend-Sicht).
|
||||
pub fn tick(&mut self, input: &FrameInput, dt: f32) {
|
||||
if matches!(self.mode, Mode::FirstPerson) {
|
||||
self.player.look(input.look_dx, input.look_dy, LOOK_SENS);
|
||||
self.player.step(&self.collision, input.fwd, input.right, input.jump, dt);
|
||||
if !matches!(self.mode, Mode::FirstPerson) { return; }
|
||||
|
||||
// Blick ist ein direkter Maus-Delta (nicht zeitintegriert) → genau
|
||||
// einmal pro Frame anwenden, sonst skalierte ihn die Zahl der
|
||||
// Sub-Schritte und die Empfindlichkeit schwankte mit der Framerate.
|
||||
self.player.look(input.look_dx, input.look_dy, LOOK_SENS);
|
||||
|
||||
// Sprung-Flanke puffern, bis ein Sub-Schritt sie verbraucht.
|
||||
self.pending_jump |= input.jump;
|
||||
|
||||
// Physik in festen FIXED_DT-Schritten treiben; Rest-Zeit über Frames
|
||||
// tragen. Der Deckel verhindert die Spirale bei einem einzelnen Hänger.
|
||||
self.sim_accum = (self.sim_accum + dt).min(MAX_ACCUM);
|
||||
while self.sim_accum >= FIXED_DT {
|
||||
let jump = std::mem::take(&mut self.pending_jump);
|
||||
self.player.step(&self.collision, input.fwd, input.right, jump, FIXED_DT);
|
||||
self.sim_accum -= FIXED_DT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,12 +311,11 @@ impl Session {
|
||||
/// Spawn-Fußpunkt (Engine-Koords) aus `info_player_start`; sonst ein Default.
|
||||
fn player_spawn(map: &Map) -> [f32; 3] {
|
||||
for e in &map.entities {
|
||||
if e.classname() == Some("info_player_start") {
|
||||
if let Some(origin) = e.props.get("origin") {
|
||||
let v: Vec<f32> = origin.split_whitespace().filter_map(|s| s.parse().ok()).collect();
|
||||
if let [x, y, z] = v[..] {
|
||||
return map::to_engine([x, y, z]);
|
||||
}
|
||||
if e.classname() == Some("info_player_start")
|
||||
&& let Some(origin) = e.props.get("origin") {
|
||||
let v: Vec<f32> = origin.split_whitespace().filter_map(|s| s.parse().ok()).collect();
|
||||
if let [x, y, z] = v[..] {
|
||||
return map::to_engine([x, y, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,6 +417,55 @@ mod tests {
|
||||
assert!(matches!(s.mode, Mode::FirstPerson));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_timestep_is_framerate_independent() {
|
||||
// Gleiche Gesamtzeit, einmal als grober und einmal als viele feine
|
||||
// Frames → dieselbe Zahl fester Sim-Schritte, also (bis auf FP-Rauschen)
|
||||
// dasselbe Ergebnis. Die leere Kollisionswelt lässt den Spieler frei
|
||||
// fallen — genügt, um die Integration zu vergleichen.
|
||||
let mut coarse = empty_session();
|
||||
coarse.mode = Mode::FirstPerson;
|
||||
let mut fine = empty_session();
|
||||
fine.mode = Mode::FirstPerson;
|
||||
|
||||
let input = FrameInput::default();
|
||||
coarse.tick(&input, 0.1);
|
||||
for _ in 0..60 { fine.tick(&input, 0.1 / 60.0); }
|
||||
|
||||
assert!((coarse.player.pos[1] - fine.player.pos[1]).abs() < 1e-3,
|
||||
"framerate-unabhängig: {} vs {}", coarse.player.pos[1], fine.player.pos[1]);
|
||||
assert!((coarse.player.vel[1] - fine.player.vel[1]).abs() < 1e-3,
|
||||
"gleiche Fallgeschwindigkeit: {} vs {}", coarse.player.vel[1], fine.player.vel[1]);
|
||||
assert!(coarse.player.pos[1] < 1.0, "sollte gefallen sein: {}", coarse.player.pos[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_press_survives_a_substepless_frame() {
|
||||
// Sprungtaste in einem Frame kürzer als FIXED_DT: es läuft noch kein
|
||||
// Sim-Schritt, die Flanke darf aber nicht verfallen, sondern muss beim
|
||||
// nächsten Schritt zünden.
|
||||
let mut s = empty_session();
|
||||
s.mode = Mode::FirstPerson;
|
||||
s.player.grounded = true;
|
||||
let jump = FrameInput { jump: true, ..Default::default() };
|
||||
s.tick(&jump, FIXED_DT * 0.5);
|
||||
assert!(s.player.vel[1] <= 0.0, "noch kein Schritt → noch kein Absprung");
|
||||
|
||||
s.tick(&FrameInput::default(), FIXED_DT * 0.6);
|
||||
assert!(s.player.vel[1] > 0.0,
|
||||
"gepufferter Sprung sollte abheben: {}", s.player.vel[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_only_runs_in_first_person() {
|
||||
// Menü/Free/Dialog pausieren die Sim: kein Schritt, keine Bewegung.
|
||||
let mut s = empty_session();
|
||||
s.mode = Mode::Menu;
|
||||
let before = s.player.pos;
|
||||
s.tick(&FrameInput::default(), 0.1);
|
||||
assert_eq!(s.player.pos, before, "Menü pausiert die Sim");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_command_reports_and_stays_free() {
|
||||
let mut s = empty_session();
|
||||
|
||||
Reference in New Issue
Block a user