domingo, 26 de mayo de 2019

Creando videojuegos en Rust lang (Parte 27)

En esta entrada agregaremos la primera parte de la lógica para que la serpiente crezca al comer. Con esto estaremos más cerca de terminar el juego de ejemplo en Rust usando ggez.

Como lo hemos venido haciendo en las entradas anteriores usaremos como base la entrada anterior "hello_ggez08". En esta entrada trabajaremos con "cargo new hello_ggez09". De la misma forma  usaremos el archivo TOML de la entrada anterior:

[package]
name = "hello_ggez09"
version = "0.1.0"
authors = ["MiUsuario <micorreo@correo.com>"]
edition = "2018"

[dependencies]
ggez = "0.5.0-rc.1"
rand = "0.6.5"

En el ejemplo de Snake usando ggez en la página de github utilizan una lista enlazada ("linked list") de la librería estándar de Rust: "std::collections::LinkedList;". Si buscamos la documentación de LinkedList (https://doc.rust-lang.org/std/collections/struct.LinkedList.html) nos recomienda que consideremos usar "Vec" o "VecDeque" en lugar de "LinkedList" para que tenga un mejor uso de memoria y mejor uso de procesador/CPU. En esta entrada usaremos "VecDeque". "VecDeque" es ideal para nuestro ejemplo de Snake ya que tiene algunas funciones para eliminar al final y agregar al inicio del VecDeque, estas son las funcionalidades que necesitamos para el cuerpo de la serpiente.

Para agregar las funcionalidades de crecer el cuerpo de la serpiente tenemos que agregar algunos bloques nuevos de código:
  • Primero agregamos la referencia para usar VecDeque al inicio del archivo:
    • use std::collections::VecDeque;
      
  • Luego agregamos el contenedor VecDeque del cuerpo en nuestro struct EstadoDelJuego para que podamos guardar el estado actual del cuerpo como un a serie de tuplas con las coordenadas (x,y) de la parte del cuerpo:
    • struct EstadoDelJuego {
          jugador_x: i16,
          jugador_y: i16,
          cuerpo: VecDeque<(i16, i16)>,
          direccion: Direccion,
          estado: Estado,
          comida_x: i16,
          comida_y: i16,
          ultima_actualizacion: std::time::Instant
      }
      
  • Actualizamos el código de inicio para que contenga el nuevo VecDeque vacío al inicio del juego cuando empezamos el programa. Para crear uno vacío usamos "VecDeque::new()":
    • let estado_del_juego = &mut EstadoDelJuego {
          jugador_x: DIMENSION_DEL_TABLERO.0 / 4,
          jugador_y: DIMENSION_DEL_TABLERO.1 / 2,
          cuerpo: VecDeque::new(),
          direccion: Direccion::Derecha,
          estado: Estado::Jugando,
          comida_x: ini_comida_x,
          comida_y: ini_comida_y,
          ultima_actualizacion: Instant::now()
      };
      
  • Agregamos el siguiente bloque de código para dibujar ("Draw") el cuerpo de la serpiente. Simplemente tenemos que iterar sobre cada parte del cuerpo y dibujarla. Para iterar usamos "for parte_del_cuerpo in self.cuerpo.iter() {...}" ya que se puede manejar de forma similar a como iteramos sobre un Vec y dentro de ese "for" copiamos y pegamos código similar para dibujar rectángulos. Para diferenciar los rectángulos utilizaremos un color diferente, en este caso usaremos el color rojo establecido por "[9.0, 0.1, 0.0, 1.0]". Cuidaremos dibujar el cuerpo, luego el jugador y luego la comida para que el jugador esté sobre el cuerpo. Para hacer esto simplemente ordenamos las rutinas dentro del "Draw". La rutina para dibujar el cuerpo que pondremos al inicio del método "Draw" será:
    • for parte_del_cuerpo in self.cuerpo.iter() {
          let rectangulo_cuerpo = ggez::graphics::Rect::new(
              (parte_del_cuerpo.0 *
               DIMENSION_DE_CELDAS_DEL_TABLERO.0) as f32, 
              (parte_del_cuerpo.1 *
               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_cuerpo = graphics::Mesh::new_rectangle(
              contexto,
              graphics::DrawMode::fill(),
              rectangulo_cuerpo,
              [9.0, 0.1, 0.0, 1.0].into())?;
          graphics::draw(contexto, 
              &grafico_de_cuerpo, 
              (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?;
      }
      
  • Agregamos el siguiente bloque de código para la rutina que se ejecuta cuando consigue la comida dentro de nuestro método "Update". El bloque nuevo de código lo colocaremos en el mismo lugar donde teníamos nuestra rutina para mover la comida a un nuevo lugar aleatorio. En este caso ahora la comida tiene que moverse a un lugar que esté disponible, es decir donde no esté el jugador ni el cuerpo de la serpiente. Como esta es la rutina cuando se consigue comida entonces también tenemos que hacer que la serpiente crezca. Para hacer que crezca haremos que se agregue un elemento al VecDeque. Vemos como usamos nuevamente el "for con iter()" y el método "push_front" de VecDeque:
    • if self.jugador_x == self.comida_x &&
         self.jugador_y == self.comida_y {
          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 self.jugador_x != ini_comida_x &&
                 self.jugador_y != ini_comida_y {
                  let mut ocupado = false;
                  for parte_de_cuerpo in self.cuerpo.iter() {
                      if parte_de_cuerpo.0 == ini_comida_x &&
                         parte_de_cuerpo.1 == ini_comida_y {
                          ocupado = true;
                          break;
                      }
                  }
                  if ocupado == false {
                      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);
          }
          self.comida_x = ini_comida_x;
          self.comida_y = ini_comida_y;
          self.cuerpo.push_front((self.jugador_x, self.jugador_y));
      }
      
Por ahora esto será suficiente para esta entrada. Por ahora no nos preocuparemos por mover las piezas del cuerpo. Es decir al correr nuestro código las piezas se quedarán donde conseguimos la comida. En resumen agregando todos los bloques anteriores 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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
use ggez::{conf, ContextBuilder, Context, 
           event, graphics, GameResult};
use std::time::{Duration, Instant};

use rand::prelude::*;

use std::collections::VecDeque;

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,
    cuerpo: VecDeque<(i16, 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;
                }}
            }
            if self.jugador_x == self.comida_x &&
               self.jugador_y == self.comida_y {
                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 self.jugador_x != ini_comida_x &&
                       self.jugador_y != ini_comida_y {
                        let mut ocupado = false;
                        for parte_de_cuerpo in self.cuerpo.iter() {
                            if parte_de_cuerpo.0 == ini_comida_x &&
                               parte_de_cuerpo.1 == ini_comida_y {
                                ocupado = true;
                                break;
                            }
                        }
                        if ocupado == false {
                            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);
                }
                self.comida_x = ini_comida_x;
                self.comida_y = ini_comida_y;
                self.cuerpo.push_front((self.jugador_x, self.jugador_y));
            }
            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 cuerpo de la serpiente
        for parte_del_cuerpo in self.cuerpo.iter() {
            let rectangulo_cuerpo = ggez::graphics::Rect::new(
                (parte_del_cuerpo.0 *
                 DIMENSION_DE_CELDAS_DEL_TABLERO.0) as f32, 
                (parte_del_cuerpo.1 *
                 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_cuerpo = graphics::Mesh::new_rectangle(
                contexto,
                graphics::DrawMode::fill(),
                rectangulo_cuerpo,
                [9.0, 0.1, 0.0, 1.0].into())?;
            graphics::draw(contexto, 
                &grafico_de_cuerpo, 
                (ggez::mint::Point2 { x: 0.0, y: 0.0 },))?;
        }
        // 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,
        cuerpo: VecDeque::new(),
        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 nuestro código es correcto y compila correctamente usando "cargo run" entonces podemos empezar a jugar nuestro juego e intentar llegar a la comida. En el momento que tocamos la comida aparece nuestro cuadro rojo en este lugar (y permanece fijo), la comida aparece en un lugar diferente que no ha sido ocupado:



En las siguiente entrada agregaremos las funcionalidades que faltan para lograr que la serpiente se mueva y que el juego termine si el jugador toca el cuerpo de la serpiente.
Fuentes (inglés):

No hay comentarios.:

Publicar un comentario