First Person Controller
This commit is contained in:
+2
-8
@@ -23,11 +23,9 @@
|
||||
//! Quake-System gerechnet (so sind Offsets/Scale definiert), die Position
|
||||
//! danach umgerechnet: `(qx, qy, qz) → (qx, qz, -qy)`, mal `MAP_SCALE`.
|
||||
|
||||
use crate::engine::map::{Face, Map};
|
||||
use crate::engine::map::{self, Face, Map};
|
||||
use crate::render::scene::{Batch, Mesh, Vertex};
|
||||
|
||||
/// Quake-Einheiten pro Engine-Einheit (32 ≈ klassische „1 Meter"-Annahme).
|
||||
const MAP_SCALE: f32 = 1.0 / 32.0;
|
||||
/// Punkt liegt „auf" einer Ebene / „innerhalb" eines Halbraums (Quake-Units).
|
||||
const ON_EPS: f32 = 1e-2;
|
||||
|
||||
@@ -207,7 +205,7 @@ fn emit_face(
|
||||
// UV in Texeln, dann auf 0..1 normalisiert.
|
||||
let u = (dot(*p, su) + fp.face.offset[0]) / tw as f32;
|
||||
let v = (dot(*p, sv) + fp.face.offset[1]) / th as f32;
|
||||
verts.push(Vertex { pos: to_engine(*p), uv: [u, v] });
|
||||
verts.push(Vertex { pos: map::to_engine(*p), uv: [u, v] });
|
||||
}
|
||||
for t in 1..(fp.poly.len() as u32 - 1) {
|
||||
per_tex[ti].extend([base, base + t, base + t + 1]);
|
||||
@@ -273,10 +271,6 @@ fn base_axes(n: [f32; 3]) -> ([f32; 3], [f32; 3]) {
|
||||
(BA[bi][1], BA[bi][2])
|
||||
}
|
||||
|
||||
fn to_engine(p: [f32; 3]) -> [f32; 3] {
|
||||
[p[0] * MAP_SCALE, p[2] * MAP_SCALE, -p[1] * MAP_SCALE]
|
||||
}
|
||||
|
||||
// --- kleine Vektor-Helfer auf [f32; 3] ---------------------------------------
|
||||
|
||||
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] { [a[0] - b[0], a[1] - b[1], a[2] - b[2]] }
|
||||
|
||||
+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()
|
||||
|
||||
+4
-2
@@ -226,7 +226,9 @@ pub(crate) fn layout(internal: [f32; 2], fonts: &Fonts, session: &Session, cur:
|
||||
// Maus (im Dialog ist die Maus ohnehin nie gefangen).
|
||||
if !cur.grabbed {
|
||||
match &session.mode {
|
||||
Mode::Free => {} // freie Welt: nur HUD/Cursor, kein Panel
|
||||
// Spiel und Flycam: nur HUD/Cursor, kein Panel (beide laufen
|
||||
// ohnehin gefangen — dieser Zweig greift nur im Übergang).
|
||||
Mode::Play | Mode::Free => {}
|
||||
Mode::Menu => menu(&mut ui, f, internal, cur.pos, &mut hover),
|
||||
Mode::Dialog(d) => dialog(&mut ui, f, internal, d, cur.pos, &mut hover),
|
||||
}
|
||||
@@ -247,7 +249,7 @@ pub(crate) fn layout(internal: [f32; 2], fonts: &Fonts, session: &Session, cur:
|
||||
/// Menu-Modus: das (noch demohafte) Menü, zentriert.
|
||||
fn menu(ui: &mut Ui, f: &Font, internal: [f32; 2], cur: [f32; 2], hover: &mut Option<String>) {
|
||||
let items = [
|
||||
("Fortsetzen", "menu"), // schließt das Menü (Toggle zurück zu Free)
|
||||
("Fortsetzen", "menu"), // schließt das Menü → ins Spiel (Play)
|
||||
("Hilfe", "help"),
|
||||
("KV anzeigen", "kv"),
|
||||
("Beenden", "quit"),
|
||||
|
||||
Reference in New Issue
Block a user