gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-voxel/src/player.rs Rust · 283 lines · 10.5 KB History Who touched what raw

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Física de jugador en primera persona sobre un [`VoxelGrid`] — el otro
2//! ingrediente núcleo de un juego voxel (además del picking de [`raycast`]):
3//! **caminar el mundo con gravedad y colisión**. Reusable por cualquier juego
4//! voxel de la suite; no toca la GPU ni el render.
5//!
6//! Coordenadas en **espacio de grilla** (voxel = 1, mundo en `[0, dim]`), las
7//! mismas que [`raycast`](crate::raycast) y `eye_mundo + dim/2`. El jugador es
8//! una caja AABB parada sobre el terreno; `pos` es el **centro de los pies**
9//! (x/z centrados, y en la base). La colisión se resuelve eje por eje
10//! (move-and-resolve), el método clásico y robusto para voxels.
11//!
12//! La mirada (`yaw`/`pitch`) la lleva la app (compartida con la cámara
13//! `orbit`/`fly`); aquí viven sólo las helpers puras de base
14//! ([`forward_h`]/[`right_h`]/[`look_dir`]) para derivar el vector de avance.
15
16use llimphi_3d::glam::Vec3;
17use llimphi_3d::VoxelGrid;
18
19/// Semi-ancho del jugador en X/Z (la caja mide `2·HALF_W` de lado).
20const HALF_W: f32 = 0.3;
21/// Altura total de la caja (de los pies a la coronilla).
22const HEIGHT: f32 = 1.7;
23/// Altura del ojo sobre los pies (la cámara va aquí).
24const EYE: f32 = 1.55;
25/// Aceleración de gravedad (voxels/s²).
26const GRAVITY: f32 = 30.0;
27/// Velocidad horizontal de caminata (voxels/s).
28const MOVE_SPEED: f32 = 8.0;
29/// Velocidad vertical inicial del salto (voxels/s).
30const JUMP_SPEED: f32 = 9.0;
31/// Margen para no “tocar” el voxel del plano siguiente por error de redondeo.
32const EPS: f32 = 1e-3;
33
34/// Avance horizontal unitario para `yaw` (en grilla): `yaw=0` mira a `+Z`.
35pub fn forward_h(yaw: f32) -> Vec3 {
36    let (s, c) = yaw.sin_cos();
37    Vec3::new(s, 0.0, c)
38}
39
40/// "Derecha" horizontal unitaria para `yaw` (perpendicular a [`forward_h`],
41/// con la convención de mano derecha del motor: a `yaw=0`, derecha = `+X`).
42pub fn right_h(yaw: f32) -> Vec3 {
43    let (s, c) = yaw.sin_cos();
44    Vec3::new(c, 0.0, -s)
45}
46
47/// Dirección de mirada completa (incluye `pitch`), misma convención que
48/// [`Camera3d::fly`](llimphi_3d::Camera3d::fly). Útil como `dir` del raycast.
49pub fn look_dir(yaw: f32, pitch: f32) -> Vec3 {
50    let (sy, cy) = yaw.sin_cos();
51    let (sp, cp) = pitch.sin_cos();
52    Vec3::new(cp * sy, sp, cp * cy)
53}
54
55/// Cuerpo físico AABB con velocidad, parado sobre el terreno. Lo usa tanto el
56/// jugador (input directo) como los agentes ([`Critter`](crate::Critter), wander
57/// automático) — misma física, distinta voluntad.
58#[derive(Debug, Clone, Copy)]
59pub struct Player {
60    /// Centro de los pies (espacio de grilla).
61    pub pos: Vec3,
62    /// Velocidad actual (voxels/s).
63    pub vel: Vec3,
64    /// `true` si el último paso terminó apoyado en suelo (habilita el salto).
65    pub on_ground: bool,
66    /// Velocidad de caminata horizontal (voxels/s). Editable por cuerpo: el
67    /// jugador anda rápido, un bicho puede pastar lento.
68    pub speed: f32,
69}
70
71impl Player {
72    /// Jugador parado con los pies en `feet` (centro de los pies, grilla).
73    pub fn new(feet: Vec3) -> Self {
74        Self {
75            pos: feet,
76            vel: Vec3::ZERO,
77            on_ground: false,
78            speed: MOVE_SPEED,
79        }
80    }
81
82    /// Crea un jugador posado sobre la columna `(x, z)` del grid: pies un voxel
83    /// por encima del suelo más alto (o sobre el piso `y=0` si la columna está
84    /// vacía). Centrado en el voxel (`+0.5`).
85    pub fn spawn_on(grid: &VoxelGrid, x: u32, z: u32) -> Self {
86        let top = grid.height_at(x, z).map(|y| y as f32 + 1.0).unwrap_or(0.0);
87        Self::new(Vec3::new(x as f32 + 0.5, top + EPS, z as f32 + 0.5))
88    }
89
90    /// Posición del ojo (cámara) en espacio de grilla.
91    pub fn eye(&self) -> Vec3 {
92        self.pos + Vec3::new(0.0, EYE, 0.0)
93    }
94
95    /// Avanza la física un paso `dt` segundos. `wish` es la dirección de
96    /// caminata **horizontal** deseada (no necesita estar normalizada; se usa
97    /// su dirección × [`MOVE_SPEED`]); `jump` solicita un salto (sólo prende si
98    /// `on_ground`). Resuelve colisión contra `grid` eje por eje.
99    pub fn step(&mut self, grid: &VoxelGrid, wish: Vec3, jump: bool, dt: f32) {
100        let dt = dt.clamp(0.0, 0.05); // nunca un salto de física monstruoso
101
102        // Velocidad horizontal: directa desde el deseo (sin inercia, simple).
103        let flat = Vec3::new(wish.x, 0.0, wish.z);
104        let h = if flat.length_squared() > 1e-6 {
105            flat.normalize() * self.speed
106        } else {
107            Vec3::ZERO
108        };
109        self.vel.x = h.x;
110        self.vel.z = h.z;
111
112        // Gravedad + salto.
113        self.vel.y -= GRAVITY * dt;
114        if jump && self.on_ground {
115            self.vel.y = JUMP_SPEED;
116        }
117
118        // Mover y resolver eje por eje. on_ground se re-evalúa cada paso.
119        self.on_ground = false;
120        self.move_axis(grid, 0, self.vel.x * dt);
121        self.move_axis(grid, 1, self.vel.y * dt);
122        self.move_axis(grid, 2, self.vel.z * dt);
123    }
124
125    /// VUELO libre (modo creativo): sin gravedad ni colisión. La cámara se mueve
126    /// en la dirección deseada `wish` —incluida la VERTICAL (`wish.y` sube/baja)—
127    /// para explorar el mundo desde cualquier ángulo y dictar sucesos. `wish` no
128    /// necesita normalizarse; vuela algo más rápido que la caminata.
129    pub fn volar(&mut self, wish: Vec3, dt: f32) {
130        self.volar_veloz(wish, dt, 1.0);
131    }
132
133    /// Como [`volar`](Self::volar) pero con un **multiplicador de velocidad** `boost`
134    /// (p. ej. escalado por la altitud: más alto → más rápido).
135    pub fn volar_veloz(&mut self, wish: Vec3, dt: f32, boost: f32) {
136        let dt = dt.clamp(0.0, 0.05);
137        self.vel = Vec3::ZERO;
138        self.on_ground = false;
139        if wish.length_squared() > 1e-6 {
140            self.pos += wish.normalize() * self.speed * 1.8 * boost.max(0.1) * dt;
141        }
142    }
143
144    /// AABB `[min, max]` del jugador en `pos`.
145    fn aabb_at(&self, pos: Vec3) -> (Vec3, Vec3) {
146        (
147            Vec3::new(pos.x - HALF_W, pos.y, pos.z - HALF_W),
148            Vec3::new(pos.x + HALF_W, pos.y + HEIGHT, pos.z + HALF_W),
149        )
150    }
151
152    /// Intenta desplazar `amount` en `axis` (0=X,1=Y,2=Z); si la caja chocaría
153    /// con un sólido, cancela el movimiento en ese eje y anula la velocidad
154    /// (marcando `on_ground` si fue al bajar).
155    fn move_axis(&mut self, grid: &VoxelGrid, axis: usize, amount: f32) {
156        if amount == 0.0 {
157            return;
158        }
159        let mut np = self.pos;
160        np[axis] += amount;
161        let (min, max) = self.aabb_at(np);
162        if aabb_hits_solid(grid, min, max) {
163            if axis == 1 && amount < 0.0 {
164                self.on_ground = true;
165            }
166            self.vel[axis] = 0.0;
167        } else {
168            self.pos = np;
169        }
170    }
171}
172
173/// `true` si la caja `[min, max]` solapa algún voxel sólido del grid. Recorre
174/// las celdas enteras que toca la caja (cada voxel `(i,j,k)` ocupa
175/// `[i,i+1)³`).
176fn aabb_hits_solid(grid: &VoxelGrid, min: Vec3, max: Vec3) -> bool {
177    let x0 = min.x.floor() as i32;
178    let y0 = min.y.floor() as i32;
179    let z0 = min.z.floor() as i32;
180    // `-EPS` para que tocar exacto el plano del voxel siguiente no lo cuente.
181    let x1 = (max.x - EPS).floor() as i32;
182    let y1 = (max.y - EPS).floor() as i32;
183    let z1 = (max.z - EPS).floor() as i32;
184    for z in z0..=z1 {
185        for y in y0..=y1 {
186            for x in x0..=x1 {
187                if grid.is_solid(x, y, z) {
188                    return true;
189                }
190            }
191        }
192    }
193    false
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    /// Piso sólido en `y=0` (una capa) sobre un grid 8³.
201    fn grid_con_piso() -> VoxelGrid {
202        let mut g = VoxelGrid::new([8, 8, 8]);
203        for z in 0..8 {
204            for x in 0..8 {
205                g.set(x, 0, z, [100, 100, 100]);
206            }
207        }
208        g
209    }
210
211    #[test]
212    fn cae_y_se_apoya_en_el_piso() {
213        let g = grid_con_piso();
214        // Arranca flotando bien arriba.
215        let mut p = Player::new(Vec3::new(4.5, 5.0, 4.5));
216        for _ in 0..240 {
217            p.step(&g, Vec3::ZERO, false, 1.0 / 60.0);
218        }
219        // El piso ocupa [0,1); los pies deben quedar apoyados a y≈1.
220        assert!(p.on_ground, "debería terminar en el suelo");
221        assert!((p.pos.y - 1.0).abs() < 0.05, "pies en y≈1, dio {}", p.pos.y);
222    }
223
224    #[test]
225    fn volar_sube_sin_gravedad_ni_colision() {
226        let _g = grid_con_piso();
227        let mut p = Player::new(Vec3::new(4.5, 5.0, 4.5));
228        let y0 = p.pos.y;
229        // Volar hacia arriba: sube y NO cae (sin gravedad).
230        for _ in 0..30 {
231            p.volar(Vec3::Y, 1.0 / 30.0);
232        }
233        assert!(p.pos.y > y0 + 1.0, "voló hacia arriba: {} → {}", y0, p.pos.y);
234        assert!(!p.on_ground);
235        // Sin deseo, se queda quieto (no cae).
236        let quieto = p.pos;
237        for _ in 0..30 {
238            p.volar(Vec3::ZERO, 1.0 / 30.0);
239        }
240        assert!((p.pos - quieto).length() < 1e-4, "sin wish, flota quieto");
241        // Atraviesa el suelo si vuela hacia abajo (sin colisión).
242        for _ in 0..600 {
243            p.volar(-Vec3::Y, 1.0 / 30.0);
244        }
245        assert!(p.pos.y < 0.0, "el vuelo ignora la colisión: {}", p.pos.y);
246    }
247
248    #[test]
249    fn una_pared_frena_el_avance() {
250        let mut g = grid_con_piso();
251        // Muro en x=6, toda la altura.
252        for y in 0..8 {
253            for z in 0..8 {
254                g.set(6, y, z, [80, 80, 80]);
255            }
256        }
257        let mut p = Player::new(Vec3::new(4.5, 1.0, 4.5));
258        // Camina hacia +X contra el muro un buen rato.
259        for _ in 0..120 {
260            p.step(&g, Vec3::X, false, 1.0 / 60.0);
261        }
262        // No puede atravesar: su borde derecho (pos.x + HALF_W) queda en x<6.
263        assert!(p.pos.x + HALF_W <= 6.0 + EPS, "atravesó el muro: x={}", p.pos.x);
264        assert!(p.pos.x > 4.5, "debería haber avanzado algo hasta el muro");
265    }
266
267    #[test]
268    fn salta_solo_desde_el_suelo() {
269        let g = grid_con_piso();
270        let mut p = Player::new(Vec3::new(4.5, 1.0, 4.5));
271        // Un paso para asentar on_ground.
272        p.step(&g, Vec3::ZERO, false, 1.0 / 60.0);
273        assert!(p.on_ground);
274        // Salta: la velocidad vertical se vuelve positiva y despega.
275        p.step(&g, Vec3::ZERO, true, 1.0 / 60.0);
276        assert!(p.vel.y > 0.0, "el salto debe dar velocidad ascendente");
277        assert!(p.pos.y > 1.0, "debe despegar del piso");
278        // En el aire, un segundo intento de salto no reinyecta velocidad.
279        let antes = p.vel.y;
280        p.step(&g, Vec3::ZERO, true, 1.0 / 60.0);
281        assert!(p.vel.y < antes, "no debe re-saltar en el aire");
282    }
283}