refactor
This commit is contained in:
+35
-77
@@ -1,21 +1,14 @@
|
||||
//! Fenster-Frontend: winit-Loop um den wgpu-Renderer.
|
||||
//!
|
||||
//! Stand: Schritt 4 des Renderer-Plans (notes/renderer-plan.md) — Flycam
|
||||
//! über dem PS1-Szenen-Pass. WASD bewegt, Maus blickt (Klick fängt den
|
||||
//! Cursor, Escape gibt ihn frei bzw. beendet), Space/Shift hoch/runter.
|
||||
//! Als Nächstes: OBJ-Szene (Schritt 5).
|
||||
//!
|
||||
//! Steuert dieselbe [`Session`] wie die CLI: das Fenster *besitzt* sie,
|
||||
//! Terminal-Befehle kommen über einen Channel von einem stdin-Thread und
|
||||
//! werden pro Loop-Durchlauf eingespielt. Im Dialog-Modus pausiert die
|
||||
//! Welt (Sim-Uhr und Kamera stehen) — so sieht das Fenster nie einen
|
||||
//! anderen Zustand als die Konsole.
|
||||
//! Welt — so sieht das Fenster nie einen anderen Zustand als die Konsole.
|
||||
//!
|
||||
//! Schicht-Regel wie `cli`: Geschwister von `engine`/`session`, konsumiert
|
||||
//! deren Schnittstellen — nie umgekehrt.
|
||||
|
||||
mod audio;
|
||||
mod camera;
|
||||
mod font;
|
||||
mod gpu;
|
||||
mod math;
|
||||
@@ -39,19 +32,13 @@ use crate::engine::player;
|
||||
use crate::engine::tga::Image;
|
||||
use crate::engine::{assets, gltf, model, tga, wav};
|
||||
use crate::session::{FrameInput, Mode, Session};
|
||||
use camera::Camera;
|
||||
use gpu::Gpu;
|
||||
use math::Mat4;
|
||||
use scene::Mesh;
|
||||
|
||||
/// Radiant pro Maus-Pixel für die Flycam — dieselbe Empfindlichkeit wie die
|
||||
/// First-Person-Sicht, damit sich der Wechsel nahtlos anfühlt.
|
||||
const MOUSE_SENS: f32 = crate::session::LOOK_SENS;
|
||||
/// 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.
|
||||
/// folgen. 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;
|
||||
@@ -61,11 +48,8 @@ pub fn run(mut session: Session) {
|
||||
// Dialog öffnen — dann startet die Welt eben pausiert).
|
||||
for line in session.start() { println!("{line}"); }
|
||||
|
||||
// CPU-Assets dekodieren (Pull über die engine-Decoder). Das Hochladen
|
||||
// auf die GPU macht später `Gpu` — Decode (CPU) und Upload (GPU) bleiben
|
||||
// getrennt. Die Welt sind alle .glb unter assets/maps/props/ —
|
||||
// Autorinnen-Content, ein kaputtes File wird gemeldet und übersprungen,
|
||||
// nie fatal.
|
||||
// Die Welt sind alle .glb unter assets/maps/props/. Decode (CPU) hier,
|
||||
// Upload (GPU) später in `Gpu` — die beiden bleiben getrennt.
|
||||
let models = load_models(&assets::path("assets/maps/props"));
|
||||
|
||||
// Eine geteilte Texturliste über alle Modelle, damit ihre Meshes
|
||||
@@ -98,11 +82,10 @@ pub fn run(mut session: Session) {
|
||||
}
|
||||
|
||||
// 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
|
||||
// Luminanz.
|
||||
// UI-Texturen sind Projekt-Assets (kein Autorinnen-Content) — fehlen sie,
|
||||
// ist die Installation kaputt, da darf es scheitern.
|
||||
// FONT_CGA, CURSORS, ORN). Fonts, Cursor und Ornament sind weiß-auf-schwarz
|
||||
// und bekommen ihre Alpha-Maske aus der Luminanz. Anders als
|
||||
// Autorinnen-Content sind sie Projekt-Assets: fehlen sie, ist die
|
||||
// Installation kaputt und es darf scheitern.
|
||||
let load_keyed = |p: &str| {
|
||||
ui::key_luminance(&tga::load(&assets::path(p)).expect("UI-Textur (Projekt-Asset)"))
|
||||
};
|
||||
@@ -168,8 +151,8 @@ fn load_models(dir: &str) -> Vec<(String, model::Model)> {
|
||||
let tris: usize = m.objects.iter().map(|o| o.tris.len()).sum();
|
||||
println!("[model] {name}: {} Objekte, {tris} Dreiecke, {} Empties",
|
||||
m.objects.len(), m.empties.len());
|
||||
// Custom Properties sichtbar machen — noch konsumiert sie
|
||||
// niemand, aber die Autorin sieht so, dass sie ankommen.
|
||||
// Properties mitloggen: so sieht die Autorin, ob ihr Export
|
||||
// `collide`, `signal` und `role` mitgenommen hat.
|
||||
for o in &m.objects {
|
||||
for (k, v) in &o.props { println!("[model] {} · {k} = {v}", o.name); }
|
||||
}
|
||||
@@ -185,9 +168,6 @@ fn load_models(dir: &str) -> Vec<(String, model::Model)> {
|
||||
models
|
||||
}
|
||||
|
||||
/// Eine Welt-Textur laden: `assets/textures/{name}.tga`. Fehlt die Datei
|
||||
/// (Material ohne Textur, Tippfehler im Blender-Materialnamen), gibt es den
|
||||
/// Platzhalter statt eines Boot-Panics — Texturnamen sind Autorinnen-Content.
|
||||
/// Sound laden: `name` → `assets/sounds/{name}.wav`. Autorinnen-Content —
|
||||
/// Fehler werden gemeldet und ergeben Stille (`None`), nie einen Absturz.
|
||||
fn load_sound(name: &str) -> Option<audio::Sound> {
|
||||
@@ -238,8 +218,6 @@ 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,
|
||||
@@ -271,15 +249,12 @@ impl App {
|
||||
fonts: font::Fonts,
|
||||
audio: Option<audio::Audio>,
|
||||
) -> 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,
|
||||
session,
|
||||
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,
|
||||
@@ -296,7 +271,7 @@ impl App {
|
||||
|
||||
// Der OS-Cursor bleibt immer versteckt (in `resumed` einmal gesetzt) —
|
||||
// wir zeichnen stets ein eigenes Sprite. `set_grab` schaltet nur die
|
||||
// Greif-Art: Locked (Flycam, relativer Blick) ↔ None (freie Maus, UI).
|
||||
// Greif-Art: Locked (Spiel/Noclip, relativer Blick) ↔ None (freie Maus, UI).
|
||||
fn set_grab(&mut self, grab: bool) {
|
||||
let Some(window) = &self.window else { return; };
|
||||
if grab {
|
||||
@@ -366,7 +341,7 @@ impl ApplicationHandler for App {
|
||||
WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left, .. } => {
|
||||
// Klick nur vormerken; der nächste redraw entscheidet (mit der
|
||||
// frisch berechneten Hover-Aktion) zwischen UI-Klick und
|
||||
// Flycam-Greifen.
|
||||
// Maus-Greifen.
|
||||
self.pending_click = true;
|
||||
}
|
||||
WindowEvent::KeyboardInput { event: key, .. } => self.on_key(event_loop, key),
|
||||
@@ -404,7 +379,7 @@ impl App {
|
||||
KeyCode::KeyS => self.input.back = down,
|
||||
KeyCode::KeyA => self.input.left = down,
|
||||
KeyCode::KeyD => self.input.right = down,
|
||||
// Space: in der Flycam „hoch" (gehalten), im Spiel Sprung (Flanke,
|
||||
// Space: im Noclip „hoch" (gehalten), im Spiel Sprung (Flanke,
|
||||
// nicht Auto-Repeat). Beide Deutungen aus derselben Taste.
|
||||
KeyCode::Space => {
|
||||
self.input.up = down;
|
||||
@@ -416,38 +391,28 @@ impl App {
|
||||
}
|
||||
|
||||
fn redraw(&mut self, event_loop: &ActiveEventLoop) {
|
||||
// dt messen; im Dialog Kamera und Sim anhalten → Welt pausiert,
|
||||
// während die Konsole den Dialog treibt.
|
||||
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: 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.
|
||||
// Lock-Zustand folgt dem Modus: Spiel und Noclip fangen die Maus
|
||||
// (relativer Blick), Menü/Dialog geben sie frei fürs UI.
|
||||
let fly = matches!(self.session.mode, Mode::Free);
|
||||
let play = matches!(self.session.mode, Mode::FirstPerson);
|
||||
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,
|
||||
};
|
||||
// Betretens-Zonen melden ihre Signal-Ausgaben wie exec-Befehle.
|
||||
for out in self.session.tick(&input, dt) { println!("{out}"); }
|
||||
}
|
||||
// Eingaben einsammeln; ob daraus Physik oder Flug wird, entscheidet
|
||||
// die Session an ihrem Modus. Betretens-Zonen melden ihre
|
||||
// Signal-Ausgaben wie exec-Befehle.
|
||||
let input = FrameInput {
|
||||
fwd: axis(self.input.fwd, self.input.back),
|
||||
right: axis(self.input.right, self.input.left),
|
||||
up: axis(self.input.up, self.input.down),
|
||||
jump: self.input.jump,
|
||||
look_dx: self.input.mouse_dx,
|
||||
look_dy: self.input.mouse_dy,
|
||||
};
|
||||
for out in self.session.tick(&input, dt) { println!("{out}"); }
|
||||
// Deltas/Flanke immer verwerfen, auch im Dialog — sonst springt der
|
||||
// Blick beim Fortsetzen um die aufgestaute Bewegung.
|
||||
self.input.mouse_dx = 0.0;
|
||||
@@ -467,11 +432,8 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Am Boden gleitet die Augenhöhe sanft nach (siehe EYE_SMOOTH_TAU); in
|
||||
// der Luft und außerhalb des Spiels folgt sie exakt.
|
||||
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();
|
||||
@@ -481,16 +443,12 @@ impl App {
|
||||
self.eye_y = target_eye;
|
||||
}
|
||||
|
||||
// View: nur die Noclip-Flycam (Free) rendert aus `self.camera`. Alles
|
||||
// andere — Spiel, Menü, Dialog — sieht durch die Augen des Players
|
||||
// (geglättete Augenhöhe); Menü/Dialog frieren die Sim nur ein, die
|
||||
// Sicht bleibt die First-Person-Sicht.
|
||||
let view = if fly {
|
||||
self.camera.view()
|
||||
} else {
|
||||
let p = self.session.player.pos;
|
||||
Mat4::view([p[0], self.eye_y, p[2]], self.session.player.yaw, self.session.player.pitch)
|
||||
};
|
||||
// Alle Modi sehen durch die Augen des Players; Menü und Dialog frieren
|
||||
// nur die Sim ein, nicht die Sicht.
|
||||
let p = self.session.player.pos;
|
||||
let view = Mat4::view(
|
||||
[p[0], self.eye_y, p[2]], self.session.player.yaw, self.session.player.pitch);
|
||||
|
||||
// Fadenkreuz-Pick im Spielmodus: Blickstrahl aus Augenhöhe auf
|
||||
// Klick-Ziele (`signal`-Property). Steuert den Interakt-Cursor und
|
||||
// löst beim Klick unten das `use` aus.
|
||||
|
||||
Reference in New Issue
Block a user