First Person Controller
This commit is contained in:
+70
-10
@@ -33,17 +33,25 @@ use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
use winit::window::{CursorGrabMode, Window, WindowId};
|
||||
|
||||
use crate::engine::player;
|
||||
use crate::engine::tga::Image;
|
||||
use crate::engine::{assets, map, tga};
|
||||
use crate::session::{Mode, Session};
|
||||
use crate::session::{FrameInput, Mode, Session};
|
||||
use camera::Camera;
|
||||
use gpu::Gpu;
|
||||
use math::Mat4;
|
||||
use scene::Mesh;
|
||||
|
||||
/// Radiant pro Maus-Pixel.
|
||||
const MOUSE_SENS: f32 = 0.0025;
|
||||
/// Welt-Einheiten pro Sekunde.
|
||||
const MOVE_SPEED: f32 = 4.0;
|
||||
/// Zeitkonstante (s) des Eye-Height-Smoothings am Boden: die Kamera gleitet
|
||||
/// vertikal sanft auf Augenhöhe nach, statt Step-up-/Slope-Sprüngen sofort zu
|
||||
/// folgen (wie Source/HL2). Größer = weicher, aber träger.
|
||||
const EYE_SMOOTH_TAU: f32 = 0.06;
|
||||
/// Maximaler vertikaler Nachlauf der Kamera hinter den Füßen (units).
|
||||
const EYE_MAX_LAG: f32 = 0.6;
|
||||
|
||||
pub fn run(mut session: Session) {
|
||||
// Init-Signal feuern, bevor das Fenster steht (kann bereits einen
|
||||
@@ -62,6 +70,10 @@ pub fn run(mut session: Session) {
|
||||
let mesh = brush::build(&world, &tex_names, &dims);
|
||||
report_map(&world, &tex_names, &mesh);
|
||||
|
||||
// Sim-Welt (Collision-Brushes + Spawn) in die Session einspielen — die
|
||||
// Simulation gehört der Session, nicht dem Render-Frontend.
|
||||
session.load_world(&world);
|
||||
|
||||
// UI-Texturen, Reihenfolge = die Index-Konstanten in ui (WHITE, FONT_EGA,
|
||||
// FONT_CGA, CURSORS, ORN). Decode (CPU) bleibt in run(); Fonts, Cursor und
|
||||
// Ornament sind weiß-auf-schwarz und bekommen ihre Alpha-Maske aus der
|
||||
@@ -134,6 +146,8 @@ struct Input {
|
||||
right: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
/// Sprung-Flanke: bei Space-Druck gesetzt, nach `player::step` verbraucht.
|
||||
jump: bool,
|
||||
mouse_dx: f32,
|
||||
mouse_dy: f32,
|
||||
/// Cursor gefangen → Maus-Look aktiv.
|
||||
@@ -145,7 +159,10 @@ struct App {
|
||||
gpu: Option<Gpu>,
|
||||
session: Session,
|
||||
rx: Receiver<String>,
|
||||
/// Noclip-Debug-Flycam (reine Frontend-Sicht, kein Sim-Zustand).
|
||||
camera: Camera,
|
||||
/// Geglättete Kamera-Augenhöhe (Welt-Y) — siehe Eye-Height-Smoothing.
|
||||
eye_y: f32,
|
||||
input: Input,
|
||||
last: Option<Instant>,
|
||||
/// Letzte Mausposition in Fenster-Pixeln (für den UI-Cursor).
|
||||
@@ -169,6 +186,8 @@ impl App {
|
||||
ui_textures: Vec<Image>,
|
||||
fonts: font::Fonts,
|
||||
) -> Self {
|
||||
// Kamera-Augenhöhe initial auf den Spawn des Players (gehört der Session).
|
||||
let eye_y = session.player.pos[1] + player::EYE_HEIGHT;
|
||||
Self {
|
||||
window: None,
|
||||
gpu: None,
|
||||
@@ -176,6 +195,7 @@ impl App {
|
||||
rx,
|
||||
// Etwas zurück und erhöht, Blick Richtung Welt-Ursprung.
|
||||
camera: Camera::new([0.0, 1.0, 4.0]),
|
||||
eye_y,
|
||||
input: Input::default(),
|
||||
last: None,
|
||||
cursor_win: [0.0, 0.0],
|
||||
@@ -298,7 +318,12 @@ impl App {
|
||||
KeyCode::KeyS => self.input.back = down,
|
||||
KeyCode::KeyA => self.input.left = down,
|
||||
KeyCode::KeyD => self.input.right = down,
|
||||
KeyCode::Space => self.input.up = down,
|
||||
// Space: in der Flycam „hoch" (gehalten), im Spiel Sprung (Flanke,
|
||||
// nicht Auto-Repeat). Beide Deutungen aus derselben Taste.
|
||||
KeyCode::Space => {
|
||||
self.input.up = down;
|
||||
if down && !key.repeat { self.input.jump = true; }
|
||||
}
|
||||
KeyCode::ShiftLeft => self.input.down = down,
|
||||
_ => {}
|
||||
}
|
||||
@@ -310,25 +335,60 @@ impl App {
|
||||
let now = Instant::now();
|
||||
let dt = self.last.replace(now).map_or(0.0, |prev| (now - prev).as_secs_f32());
|
||||
|
||||
// Lock-Zustand folgt dem Modus: nur in Free ist die Flycam aktiv
|
||||
// (Maus gefangen, relativer Blick). Menü/Dialog geben die Maus frei
|
||||
// fürs UI. So gibt es keine parallele Lock-Logik mehr.
|
||||
let fly = matches!(self.session.mode, Mode::Free);
|
||||
if fly != self.input.grabbed { self.set_grab(fly); }
|
||||
// Lock-Zustand folgt dem Modus: Spiel (Play) und Flycam (Free) fangen
|
||||
// die Maus (relativer Blick); Menü/Dialog geben sie frei fürs UI. So
|
||||
// gibt es keine parallele Lock-Logik.
|
||||
let fly = matches!(self.session.mode, Mode::Free);
|
||||
let play = matches!(self.session.mode, Mode::Play);
|
||||
let grab = fly || play;
|
||||
if grab != self.input.grabbed { self.set_grab(grab); }
|
||||
|
||||
if fly {
|
||||
// Flycam ist Frontend-Sicht: hier (nicht in der Session) bewegt.
|
||||
self.camera.apply_mouse(self.input.mouse_dx, self.input.mouse_dy, MOUSE_SENS);
|
||||
let fwd = axis(self.input.fwd, self.input.back);
|
||||
let right = axis(self.input.right, self.input.left);
|
||||
let up = axis(self.input.up, self.input.down);
|
||||
self.camera.translate(fwd, right, up, MOVE_SPEED * dt);
|
||||
} else if play {
|
||||
// Eingaben einsammeln und die Simulation in der Session treiben.
|
||||
let input = FrameInput {
|
||||
fwd: axis(self.input.fwd, self.input.back),
|
||||
right: axis(self.input.right, self.input.left),
|
||||
jump: self.input.jump,
|
||||
look_dx: self.input.mouse_dx,
|
||||
look_dy: self.input.mouse_dy,
|
||||
};
|
||||
self.session.tick(&input, dt);
|
||||
}
|
||||
// Delta immer verwerfen, auch im Dialog — sonst springt der Blick
|
||||
// beim Fortsetzen um die aufgestaute Bewegung.
|
||||
// Deltas/Flanke immer verwerfen, auch im Dialog — sonst springt der
|
||||
// Blick beim Fortsetzen um die aufgestaute Bewegung.
|
||||
self.input.mouse_dx = 0.0;
|
||||
self.input.mouse_dy = 0.0;
|
||||
self.input.jump = false;
|
||||
|
||||
let view = self.camera.view();
|
||||
// Eye-Height-Smoothing: die Kamera-Augenhöhe gleitet am Boden sanft auf
|
||||
// den Zielwert nach, statt Step-up-/Slope-Kanten-Sprüngen sofort zu
|
||||
// folgen (versteckt den AABB-auf-Slope-„Pop" und macht Treppen weich).
|
||||
// In der Luft (Sprung/Fall) und außerhalb des Spiels exakt — da soll
|
||||
// die Kamera reagieren bzw. mit der Flycam synchron bleiben.
|
||||
let target_eye = self.session.player.pos[1] + player::EYE_HEIGHT;
|
||||
if play && self.session.player.grounded {
|
||||
let a = 1.0 - (-dt / EYE_SMOOTH_TAU).exp();
|
||||
self.eye_y += (target_eye - self.eye_y) * a;
|
||||
self.eye_y = self.eye_y.clamp(target_eye - EYE_MAX_LAG, target_eye + EYE_MAX_LAG);
|
||||
} else {
|
||||
self.eye_y = target_eye;
|
||||
}
|
||||
|
||||
// View: im Spiel aus dem Player der Session (geglättete Augenhöhe),
|
||||
// sonst aus der Flycam.
|
||||
let view = if play {
|
||||
let p = self.session.player.pos;
|
||||
Mat4::view([p[0], self.eye_y, p[2]], self.session.player.yaw, self.session.player.pitch)
|
||||
} else {
|
||||
self.camera.view()
|
||||
};
|
||||
// Mausposition über die Letterbox in interne Pixel mappen.
|
||||
let cursor = ui::Cursor {
|
||||
pos: self.gpu.as_ref()
|
||||
|
||||
Reference in New Issue
Block a user