gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc llimphi-voxel/src/player.rs Rust · 240 líneas · 8.7 KB Historia Quién tocó qué crudo

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · 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`); acá 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 acá).
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    /// AABB `[min, max]` del jugador en `pos`.
126    fn aabb_at(&self, pos: Vec3) -> (Vec3, Vec3) {
127        (
128            Vec3::new(pos.x - HALF_W, pos.y, pos.z - HALF_W),
129            Vec3::new(pos.x + HALF_W, pos.y + HEIGHT, pos.z + HALF_W),
130        )
131    }
132
133    /// Intenta desplazar `amount` en `axis` (0=X,1=Y,2=Z); si la caja chocaría
134    /// con un sólido, cancela el movimiento en ese eje y anula la velocidad
135    /// (marcando `on_ground` si fue al bajar).
136    fn move_axis(&mut self, grid: &VoxelGrid, axis: usize, amount: f32) {
137        if amount == 0.0 {
138            return;
139        }
140        let mut np = self.pos;
141        np[axis] += amount;
142        let (min, max) = self.aabb_at(np);
143        if aabb_hits_solid(grid, min, max) {
144            if axis == 1 && amount < 0.0 {
145                self.on_ground = true;
146            }
147            self.vel[axis] = 0.0;
148        } else {
149            self.pos = np;
150        }
151    }
152}
153
154/// `true` si la caja `[min, max]` solapa algún voxel sólido del grid. Recorre
155/// las celdas enteras que toca la caja (cada voxel `(i,j,k)` ocupa
156/// `[i,i+1)³`).
157fn aabb_hits_solid(grid: &VoxelGrid, min: Vec3, max: Vec3) -> bool {
158    let x0 = min.x.floor() as i32;
159    let y0 = min.y.floor() as i32;
160    let z0 = min.z.floor() as i32;
161    // `-EPS` para que tocar exacto el plano del voxel siguiente no lo cuente.
162    let x1 = (max.x - EPS).floor() as i32;
163    let y1 = (max.y - EPS).floor() as i32;
164    let z1 = (max.z - EPS).floor() as i32;
165    for z in z0..=z1 {
166        for y in y0..=y1 {
167            for x in x0..=x1 {
168                if grid.is_solid(x, y, z) {
169                    return true;
170                }
171            }
172        }
173    }
174    false
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    /// Piso sólido en `y=0` (una capa) sobre un grid 8³.
182    fn grid_con_piso() -> VoxelGrid {
183        let mut g = VoxelGrid::new([8, 8, 8]);
184        for z in 0..8 {
185            for x in 0..8 {
186                g.set(x, 0, z, [100, 100, 100]);
187            }
188        }
189        g
190    }
191
192    #[test]
193    fn cae_y_se_apoya_en_el_piso() {
194        let g = grid_con_piso();
195        // Arranca flotando bien arriba.
196        let mut p = Player::new(Vec3::new(4.5, 5.0, 4.5));
197        for _ in 0..240 {
198            p.step(&g, Vec3::ZERO, false, 1.0 / 60.0);
199        }
200        // El piso ocupa [0,1); los pies deben quedar apoyados a y≈1.
201        assert!(p.on_ground, "debería terminar en el suelo");
202        assert!((p.pos.y - 1.0).abs() < 0.05, "pies en y≈1, dio {}", p.pos.y);
203    }
204
205    #[test]
206    fn una_pared_frena_el_avance() {
207        let mut g = grid_con_piso();
208        // Muro en x=6, toda la altura.
209        for y in 0..8 {
210            for z in 0..8 {
211                g.set(6, y, z, [80, 80, 80]);
212            }
213        }
214        let mut p = Player::new(Vec3::new(4.5, 1.0, 4.5));
215        // Camina hacia +X contra el muro un buen rato.
216        for _ in 0..120 {
217            p.step(&g, Vec3::X, false, 1.0 / 60.0);
218        }
219        // No puede atravesar: su borde derecho (pos.x + HALF_W) queda en x<6.
220        assert!(p.pos.x + HALF_W <= 6.0 + EPS, "atravesó el muro: x={}", p.pos.x);
221        assert!(p.pos.x > 4.5, "debería haber avanzado algo hasta el muro");
222    }
223
224    #[test]
225    fn salta_solo_desde_el_suelo() {
226        let g = grid_con_piso();
227        let mut p = Player::new(Vec3::new(4.5, 1.0, 4.5));
228        // Un paso para asentar on_ground.
229        p.step(&g, Vec3::ZERO, false, 1.0 / 60.0);
230        assert!(p.on_ground);
231        // Salta: la velocidad vertical se vuelve positiva y despega.
232        p.step(&g, Vec3::ZERO, true, 1.0 / 60.0);
233        assert!(p.vel.y > 0.0, "el salto debe dar velocidad ascendente");
234        assert!(p.pos.y > 1.0, "debe despegar del piso");
235        // En el aire, un segundo intento de salto no reinyecta velocidad.
236        let antes = p.vel.y;
237        p.step(&g, Vec3::ZERO, true, 1.0 / 60.0);
238        assert!(p.vel.y < antes, "no debe re-saltar en el aire");
239    }
240}