tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 333dca1d5fb473825c9128d402d4cc4e2a5176fc
llimphi-voxel/src/player.rs
Historia
Quién tocó qué
crudo
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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}