Usaremos el ejemplo de github como referencia aunque hay algunas partes desactualizadas que no corresponden a la nueva versión de Rust. También usaremos como referencia lo que hicimos en la entrada anterior "hello_ggez03", para esta entrada haremos "cargo new hello_ggez04" y pondremos el archivo TOML similar a los ejemplos anteriores:
1 2 3 4 5 6 7 8 | [package] name = "hello_ggez04" version = "0.1.0" authors = ["MiUsuario <micorreo@correo.com>"] edition = "2018" [dependencies] ggez = "0.5.0-rc.1" |
Iremos por partes agregando funcionalidad que tiene el ejemplo de github. Para empezar agregaremos 4 cosas al juego:
- Crearemos el sistema de coordenadas y le pondremos un tamaño a la ventana.
- Dibujaremos un color de fondo en la ventana. Este será nuestro fondo del juego.
- Dibujaremos un rectángulo en la ventana, en realidad un cuadrado. Este será nuestro jugador. Por ahora no podremos nada de control. Sólo haremos que el rectángulo se mueva a la derecha en cada actualización.
- Limitaremos el número de actualizaciones por segundo. Esto para que podamos ver el movimiento del rectángulo a la derecha.
Para cada uno de los puntos anteriores lo podemos relacionar a comandos de ggez y bloques de código:
- Las funciones para dibujar usan píxeles para las posiciones. En un juego de Snake manejamos coordinadas que son diferentes a los píxeles, las celdas a las que corresponde. Así que debe de haber una forma de convertir entre ambos, esta conversión depende del tamaño de la ventana, número de celdas y tamaño de las celdas. En Rust podemos definir variables constantes ("const") y usarlas como parámetros. Así que usaremos:
-
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, );
- Lo anterior define una ventana de 320 x 320 píxeles, correspondiente a un tablero de 10 x 10 celdas y donde cada celda ocupa 32 x 32 píxeles. Para usar estos valores en la ventana simplemente los colocamos en nuestro objeto de "configuracion". También le podemos poner un título "Snake" a la ventana:
-
configuracion.window_setup = conf::WindowSetup::default() .title("Snake"); configuracion.window_mode = conf::WindowMode::default() .dimensions(DIMENSION_DE_VENTANA.0, DIMENSION_DE_VENTANA.1);
- Para dibujar el color de la ventana pondremos como primer comando en nuestra función "draw" el siguiente comando que limpia la pantalla y pone el color de fondo:
-
graphics::clear(contexto, [0.0, 1.0, 0.0, 1.0].into());
- La función anterior toma la variable de contexto "contexto" y el color como un valor "RGBA" ("red, green, blue, alpha" o "rojo, verde, azul, alfa"). En este caso [0.0, 1.0, 0.0, 1.0] corresponde a 100% verde con 100% de alfa (visible). Esencialmente cada ciclo de dibujo ("draw") pondremos el fondo verde.
- Para dibujar el rectángulo (el jugador) necesitaremos poner su posición en coordenadas de celdas en el "struct" del EstadoDelJuego. También en el "struct" del EstadoDelJuego guardaremos el momento de la última actualización, para usar esta información para limitar el número de actualizaciones por segundo. Para dibujarlo creamos un rectángulo con las coordenadas convertidas a píxeles (multiplicaros por 32, las dimensiones) y luego lo dibujaremos con la función "draw". También en el ejemplo de github utilizan las funciones "present()" y "yield_now()" para mandar los dibujos al buffer y ceder al sistema al final de nuestro método "draw". El estado del juego ahora es:
-
struct EstadoDelJuego { jugador_x: i16, jugador_y: i16, ultima_actualizacion: std::time::Instant }
- Y inicalizamos el EstadoDelJuego usando los tipos correspondientes. Pondremos la serpiente un cuarto (1/4) de la coordenada "x" y un medio (1/2) en la coordenada "y":
-
let estado_del_juego = &mut EstadoDelJuego { jugador_x: DIMENSION_DEL_TABLERO.0 / 4, jugador_y: DIMENSION_DEL_TABLERO.1 / 2, direccion: Direccion::Derecha, ultima_actualizacion: Instant::now() };
- El método "draw" será:
-
graphics::clear(contexto, [0.0, 1.0, 0.0, 1.0].into()); let rectangulo = 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_rectangulo = graphics::Mesh::new_rectangle( contexto, graphics::DrawMode::fill(), rectangulo, [0.3, 0.3, 0.0, 1.0].into())?; graphics::draw(contexto, &grafico_de_rectangulo, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; graphics::present(contexto)?; ggez::timer::yield_now(); Ok(())
- Al crear el rectángulo le pasamos la coordenada x, la coordenada y, su ancho y su largo. Y para la función de gráficos le pasamos el contexto, si queremos que lo rellene ("fill"), el objeto rectángulo que creamos y el color como valores "RGBA". En este caso el color lo pondremos 30% rojo, 30% verde y 100% de alfa (totalmente visible). La función "graphic::draw" puede realizar transformaciones y rotaciones simples a nuestro gráfico, pero en este caso la llamaremos directamente de la forma default con "(ggez::mint::Point2 { x: 0.0, y: 0.0 },)" para que no haga ninguna transformación de traslación o rotación.
- Finalmente para limitar el número de actualizaciones usaremos "const" y una condición dentro de nuestra función "update". También para hacer que el rectángulo se mueva a la derecha hay que sumarle 1 en cada actualización "update". Los "const" son los siguientes:
-
const ACTUALIZACIONES_POR_SEGUNDO: f32 = 8.0; const MILISEG_POR_ACTUALIZACION: u64 = ( (1.0 / ACTUALIZACIONES_POR_SEGUNDO) * 1000.0) as u64;
- Es decir queremos 8 actualizaciones por segundo. Convertirmos esto a milisegundos dividiendo 1 segundo entre actualizaciones por segundo y multiplicando por mil para que sea en milisegundos. Para considerar lo anterior nuestra función "update" ahora se ve de la siguiente forma:
-
if Instant::now() - self.ultima_actualizacion >= Duration::from_millis(MILISEG_POR_ACTUALIZACION) { self.jugador_x = self.jugador_x + 1; self.ultima_actualizacion = Instant::now(); } Ok(())
- Es decir calculamos la diferencia entre la última actualización y este momento. Si la diferencia excede nuestro valor configurado en milisegundos entonces actualizamos y actualizamos el valor de la última actualización. De otra forma no hacemos nada.
Todo el código completo se ve de la siguiente forma:
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 | use ggez::{conf, ContextBuilder, Context, event, graphics, GameResult}; use std::time::{Duration, Instant}; 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; struct EstadoDelJuego { jugador_x: i16, jugador_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.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()); let rectangulo = 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_rectangulo = graphics::Mesh::new_rectangle( contexto, graphics::DrawMode::fill(), rectangulo, [0.3, 0.3, 0.0, 1.0].into())?; graphics::draw(contexto, &grafico_de_rectangulo, (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?; graphics::present(contexto)?; ggez::timer::yield_now(); Ok(()) } } fn main() { let estado_del_juego = &mut EstadoDelJuego { jugador_x: DIMENSION_DEL_TABLERO.0 / 4, jugador_y: DIMENSION_DEL_TABLERO.1 / 2, 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 las siguientes entradas creceremos este ejemplo según el ejemplo Snake de github.
Navegación:
Primera parte
Siguiente parte
Parte anterior
En las siguientes entradas creceremos este ejemplo según el ejemplo Snake de github.
Navegación:
Primera parte
Siguiente parte
Parte anterior
Fuentes (inglés):
- 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