También agregaremos la lógica para colocar la comida en el tablero en un lugar aleatorio al inicio del juego. Y para poder ver la comida también agregaremos la lógica para dibujar la comida.
Construiremos el archivo usando como base la entrada anterior "hello_ggez06". En esta entrada trabajaremos con "cargo new hello_ggez07". Como es de esperar pondremos el archivo TOML similar a los ejemplos anteriores, lo único que agregaremos será una nueva dependencia al paquete "rand" que usan en el ejemplo de Snake usando ggez en github. La línea que debemos de agregar la podemos ver en crates.io (https://crates.io/crates/rand) y es "rand = "0.6.5"" al final :
[package] name = "hello_ggez07" version = "0.1.0" authors = ["MiUsuario <micorreo@correo.com>"] edition = "2018" [dependencies] ggez = "0.5.0-rc.1" rand = "0.6.5"
La documentación de "rand" también la podemos encontrarla al darle click en "Documentation" en la página de crates.io: https://rust-random.github.io/rand/rand/index.html
Y ahora para agregar la funcionalidad de limitar el movimiento de la serpiente y dibujar la comida en una posición aleatoria agregamos los siguientes bloques de código:
- Al inicio en donde ponemos nuestros declaraciones "use" agregamos "use rand::prelude::*;" para poder usar las funciones principales de "rand" para manejar la creación de números aleatorios que usaremos para posicionar la comida:
-
use rand::prelude::*;
- Derivamos la propriedad "PartialEq" para nuestro "struct" de dirección "Direccion". "PartialEq" es "Partial Equality" es decir "igualdad parcial". Esta propiedad ya ha habíamos usado con nuestro struct de "Estado". Como vimos en entradas anteriores esta propriedad nos permite usar los operadores de igualdad con los enums, sin requerir un "match". Para poder usarlo fuera de "match" y usar el operador "==" debemos de derivar la propiedad
-
#[derive(PartialEq)] enum Direccion { Arriba, Abajo, Izquierda, Derecha, }
- Para limitar el movimiento de la serpiente simplemente agregamos condicionales en donde ignore el botón que presionamos si es la dirección contraria a la dirección que viajamos. Es decir, ignorar "derecha" si vamos a la "izquierda", ignorar "arriba" si vamos hacia "abajo", ignorar "izquierda" si vamos a la "derecha" e ignorar "abajo" si nos dirigimos hacía "arriba". Entonces esto es lo que escribimos en la función "key_down_event" donde actualizamos la dirección:
-
match keycode { ggez::event::KeyCode::Up => { if self.direccion != Direccion::Abajo { self.direccion = Direccion::Arriba } }, ggez::event::KeyCode::Down => { if self.direccion != Direccion::Arriba { self.direccion = Direccion::Abajo } }, ggez::event::KeyCode::Left => { if self.direccion != Direccion::Derecha { self.direccion = Direccion::Izquierda } }, ggez::event::KeyCode::Right => { if self.direccion != Direccion::Izquierda { self.direccion = Direccion::Derecha } }, _ => (), } ()
- Para guardar la posición de la comida debemos de agregar la variable en el struct del EstadoDelJuego.
- En la definición el EstadoDelJuego termina de la siguiente forma:
-
struct EstadoDelJuego { jugador_x: i16, jugador_y: i16, direccion: Direccion, estado: Estado, comida_x: i16, comida_y: i16, ultima_actualizacion: std::time::Instant }
- Como cambiamos el struct de EstadoDeJuego ahora tenemos que cambiar el bloque de código donde asignamos el estado inicial para asignar la posición de la comida. En este caso también debemos se usar las funciones de números aleatorios "rand" y un "loop" para checar que no sean las mismas coordenadas de inicio de la serpiente. Las funciones de "rand" fueron copiadas del ejemplo de Snake usando ggez de la página de github. Un bloque "loop" es usado en Rust para ciclos infinitos donde usamos "break;" para terminar la ejecución si las coordenadas difieren con las coordenadas de inicio:
-
let mut rng = rand::thread_rng(); let mut ini_comida_x = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.0); let mut ini_comida_y = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.1); loop { if DIMENSION_DEL_TABLERO.0 / 4 != ini_comida_x && DIMENSION_DEL_TABLERO.1 / 2 != ini_comida_y { break; } ini_comida_x = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.0); ini_comida_y = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.1); } let estado_del_juego = &mut EstadoDelJuego { jugador_x: DIMENSION_DEL_TABLERO.0 / 4, jugador_y: DIMENSION_DEL_TABLERO.1 / 2, direccion: Direccion::Derecha, estado: Estado::Jugando, comida_x: ini_comida_x, comida_y: ini_comida_y, ultima_actualizacion: Instant::now() };
- Las función "rand::thread_rng()" crea un struct de donde podemos llamar las funciones para obtener números aleatorios, en este caso lo asignamos a la variable "rng". Luego usamos "rng.gen_range::<i16,i16,i16>(min, max)" para generar un rango de valores entre el "min" y el "max", la forma en que regresa los valores es que no incluye el valor del "max", regresa "max - 1" como valor máximo. En este caso significa que genera un valor entre 0 y "dimensiones del tablero - 1". Hacemos esto para "x" y luego para "y".
- En la función "draw" seguiremos dibujando el fondo, al jugador y agregaremos la parte donde dibujamos la comida usando los datos del struct EstadoDelJuego. Dibujaremos la comida con el color azul que corresponde a "[0.0, 0.0, 1.0, 1.0]" en código RGBA. Agregaré comentarios y cambiaré el nombre de algunas variables para hacerlo más claro. En resumen el interior de nuestra función "draw" ahora se ve de la siguiente forma:
-
graphics::clear(contexto, [0.0, 1.0, 0.0, 1.0].into()); // Dibujar el jugador let rectangulo_jugador = ggez::graphics::Rect::new( (self.jugador_x * DIMENSION_DE_CELDAS_DEL_TABLERO.0) as f32, (self.jugador_y * DIMENSION_DE_CELDAS_DEL_TABLERO.1) as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.0 as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.1 as f32); let grafico_de_jugador = graphics::Mesh::new_rectangle( contexto, graphics::DrawMode::fill(), rectangulo_jugador, [0.3, 0.3, 0.0, 1.0].into())?; graphics::draw(contexto, &grafico_de_jugador, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; // Dibujar la comida let rectangulo_comida = ggez::graphics::Rect::new( (self.comida_x * DIMENSION_DE_CELDAS_DEL_TABLERO.0) as f32, (self.comida_y * DIMENSION_DE_CELDAS_DEL_TABLERO.1) as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.0 as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.1 as f32); let grafico_de_comida = graphics::Mesh::new_rectangle( contexto, graphics::DrawMode::fill(), rectangulo_comida, [0.0, 0.0, 1.0, 1.0].into())?; graphics::draw(contexto, &grafico_de_comida, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; // Mandando al generador de gráficos graphics::present(contexto)?; ggez::timer::yield_now(); Ok(())
Y todo el código completo se ve de la siguiente forma en su estado actual:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | use ggez::{conf, ContextBuilder, Context, event, graphics, GameResult}; use std::time::{Duration, Instant}; use rand::prelude::*; const DIMENSION_DEL_TABLERO: (i16, i16) = (10, 10); const DIMENSION_DE_CELDAS_DEL_TABLERO: (i16, i16) = (32, 32); const DIMENSION_DE_VENTANA: (f32, f32) = ( DIMENSION_DEL_TABLERO.0 as f32 * DIMENSION_DE_CELDAS_DEL_TABLERO.0 as f32, DIMENSION_DEL_TABLERO.1 as f32 * DIMENSION_DE_CELDAS_DEL_TABLERO.1 as f32, ); const ACTUALIZACIONES_POR_SEGUNDO: f32 = 8.0; const MILISEG_POR_ACTUALIZACION: u64 = ( (1.0 / ACTUALIZACIONES_POR_SEGUNDO) * 1000.0) as u64; #[derive(PartialEq)] enum Direccion { Arriba, Abajo, Izquierda, Derecha, } #[derive(PartialEq)] enum Estado { Jugando, GameOver } struct EstadoDelJuego { jugador_x: i16, jugador_y: i16, direccion: Direccion, estado: Estado, comida_x: i16, comida_y: i16, ultima_actualizacion: std::time::Instant } impl ggez::event::EventHandler for EstadoDelJuego { fn update(&mut self, _contexto: &mut Context) -> GameResult<()> { if (Instant::now() - self.ultima_actualizacion >= Duration::from_millis(MILISEG_POR_ACTUALIZACION)) & (self.estado == Estado::Jugando) { match self.direccion { Direccion::Arriba => { self.jugador_y = self.jugador_y - 1; if self.jugador_y < 0 { self.estado = Estado::GameOver; self.jugador_y = self.jugador_y + 1; }}, Direccion::Abajo => { self.jugador_y = self.jugador_y + 1; if self.jugador_y > DIMENSION_DEL_TABLERO.1 - 1 { self.estado = Estado::GameOver; self.jugador_y = self.jugador_y - 1; }}, Direccion::Izquierda => { self.jugador_x = self.jugador_x - 1; if self.jugador_x < 0 { self.estado = Estado::GameOver; self.jugador_x = self.jugador_x + 1; }}, Direccion::Derecha => { self.jugador_x = self.jugador_x + 1; if self.jugador_x > DIMENSION_DEL_TABLERO.0 - 1 { self.estado = Estado::GameOver; self.jugador_x = self.jugador_x - 1; }} } self.ultima_actualizacion = Instant::now(); } Ok(()) } fn draw(&mut self, contexto: &mut Context) -> GameResult<()> { graphics::clear(contexto, [0.0, 1.0, 0.0, 1.0].into()); // Dibujar el jugador let rectangulo_jugador = ggez::graphics::Rect::new( (self.jugador_x * DIMENSION_DE_CELDAS_DEL_TABLERO.0) as f32, (self.jugador_y * DIMENSION_DE_CELDAS_DEL_TABLERO.1) as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.0 as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.1 as f32); let grafico_de_jugador = graphics::Mesh::new_rectangle( contexto, graphics::DrawMode::fill(), rectangulo_jugador, [0.3, 0.3, 0.0, 1.0].into())?; graphics::draw(contexto, &grafico_de_jugador, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; // Dibujar la comida let rectangulo_comida = ggez::graphics::Rect::new( (self.comida_x * DIMENSION_DE_CELDAS_DEL_TABLERO.0) as f32, (self.comida_y * DIMENSION_DE_CELDAS_DEL_TABLERO.1) as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.0 as f32, DIMENSION_DE_CELDAS_DEL_TABLERO.1 as f32); let grafico_de_comida = graphics::Mesh::new_rectangle( contexto, graphics::DrawMode::fill(), rectangulo_comida, [0.0, 0.0, 1.0, 1.0].into())?; graphics::draw(contexto, &grafico_de_comida, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; // Mandando al generador de gráficos graphics::present(contexto)?; ggez::timer::yield_now(); Ok(()) } fn key_down_event( &mut self, _ctx: &mut Context, keycode: ggez::event::KeyCode, _keymod: ggez::event::KeyMods, _repeat: bool, ) { match keycode { ggez::event::KeyCode::Up => { if self.direccion != Direccion::Abajo { self.direccion = Direccion::Arriba } }, ggez::event::KeyCode::Down => { if self.direccion != Direccion::Arriba { self.direccion = Direccion::Abajo } }, ggez::event::KeyCode::Left => { if self.direccion != Direccion::Derecha { self.direccion = Direccion::Izquierda } }, ggez::event::KeyCode::Right => { if self.direccion != Direccion::Izquierda { self.direccion = Direccion::Derecha } }, _ => (), } () } } fn main() { let mut rng = rand::thread_rng(); let mut ini_comida_x = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.0); let mut ini_comida_y = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.1); loop { if DIMENSION_DEL_TABLERO.0 / 4 != ini_comida_x && DIMENSION_DEL_TABLERO.1 / 2 != ini_comida_y { break; } ini_comida_x = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.0); ini_comida_y = rng.gen_range::<i16, i16, i16>(0, DIMENSION_DEL_TABLERO.1); } let estado_del_juego = &mut EstadoDelJuego { jugador_x: DIMENSION_DEL_TABLERO.0 / 4, jugador_y: DIMENSION_DEL_TABLERO.1 / 2, direccion: Direccion::Derecha, estado: Estado::Jugando, comida_x: ini_comida_x, comida_y: ini_comida_y, ultima_actualizacion: Instant::now() }; let mut configuracion = conf::Conf::new(); configuracion.window_setup = conf::WindowSetup::default() .title("Snake"); configuracion.window_mode = conf::WindowMode::default() .dimensions(DIMENSION_DE_VENTANA.0, DIMENSION_DE_VENTANA.1); let (ref mut contexto, ref mut bucle_de_juego) = ContextBuilder::new( "hello_ggez", "autor") .conf(configuracion).build().unwrap(); event::run(contexto, bucle_de_juego, estado_del_juego).unwrap(); } |
Si todo salió bien cuando lo corremos usando "cargo run" podemos ver el cuadro moviéndose a la derecha en un inicio y si presionamos a la izquierda mientras nos movemos no podremos cambiar su dirección. Sólo podemos ir a la izquierda si vamos a arriba o abajo primero. También deberíamos de ver en un lugar aleatorio un cuadro azul que representa la comida de la serpiente:
En las siguientes entradas veremos como agregar las funcionalidades que faltan para que se parezca más al ejemplo de github de Snake usando ggez.
Fuentes (inglés):
- Página de crates.io de "rand": https://crates.io/crates/rand
- Documentación de "rand": https://rust-random.github.io/rand/rand/index.html
- Página de Github de ggez con ejemplo usando Snake https://github.com/ggez/ggez/blob/master/examples/04_snake.rs
- Herramienta de formato del código para estas entradas (Rust): http://hilite.me/
No hay comentarios.:
Publicar un comentario