martes, 21 de mayo de 2019

Creando videojuegos en Rust lang (Parte 25)

En esta entrada limitaremos el movimiento de la serpiente. En un juego de serpiente la serpiente no puede "regresarse" en dirección contraria en la que viaja.

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):

No hay comentarios.:

Publicar un comentario