viernes, 10 de mayo de 2019

Creando videojuegos en Rust lang (Parte 23)

Continuaremos con nuestro ejemplo agregando control sobre la serpiente. En su estado anterior el rectángulo (o mejor dicho el cuadrado) representaba el jugador. Sin embargo en la versión anterior no se puede controlar. En esta entrada agregaremos la funcionalidad de controlar la serpiente, es decir, nuestro jugador/cuadrado.

Similar a la entrada anterior usaremos como referencia lo que hicimos anteriormente "hello_ggez04", para esta entrada haremos "cargo new hello_ggez05" y pondremos el archivo TOML similar a los ejemplos anteriores:

1
2
3
4
5
6
7
8
[package]
name = "hello_ggez05"
version = "0.1.0"
authors = ["MiUsuario <micorreo@correo.com>"]
edition = "2018"

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

Simplemente agregaremos la funcionalidad para controlar la serpiente y esto involucra agregaregar 4 cosas al juego:
  • Un "enum" para guardar las 4 direcciones: arriba, abajo, izquierda y derecha. O también podemos llamarlas norte, sur, oeste y este.
  • Agregar la información de la dirección del jugador al Estado del Juego.
  • Agregar lógica que actualice la posición de la serpiente según la dirección. Esta lógica usará un "switch" según la dirección. Este "switch" lo pondremos dentro de nuestra implementación de "update" que ya teníamos en la versión anterior. En la versión anterior movía a la derecha. Según la forma en la que definimos las celdas esta es la forma en la que actualizaremos la posición del jugador para las 4 direcciones:
    • Si la dirección es al norte hay que restar 1 celda en el eje y.
    • Si la dirección es al norte hay que sumar 1 celda en el eje y.
    • Si la dirección es al este hay que sumar 1 celda en el eje x. Esto es lo que hacíamos para que se mueva a la derecha.
    • Si la dirección es al oeste hay que restar 1 celda en el eje x. 
  • Agregar un nuevo evento en el bucle del juego (ciclo del juego) en donde leemos las teclas que fueron presionadas y actualizamos el Estado del Juego para que refleje el resultado de presionar las teclas en la memoria (EstadoDelJuego). En ggez esto lo hacemos agregando implementación de la función "key_down_event" que recibe un "keycode" con el cual podemos hacer un "match" y actualizar la dirección en el Estado del Juego según la tecla presionada. 
El bucle del juego donde registramos las teclas presionadas con "key_down_event" o cualquier manejo de la entrada (mouse, control, etc.) se ve de la siguiente forma:


Para cada uno de los puntos anteriores lo podemos relacionar a comandos de ggez y bloques de código:
  • Para la dirección:
    • enum Direccion {
          Arriba,
          Abajo,
          Izquierda,
          Derecha,
      }
      
  • En el struct de "EstadoDelJuego" agregamos la dirección anterior. Así podemos tener la información que presionó el jugador disponible y actualizar la posición de la serpiente según este valor:
    • struct EstadoDelJuego {
          jugador_x: i16,
          jugador_y: i16,
          direccion: Direccion,
          ultima_actualizacion: std::time::Instant
      }
      
    • También al inicializar el EstadoDelJuego tenemos que ponerle una dirección inicial. Escogeremos la "derecha" para que tenga el mismo comportamiento que en la entrada anterior:
    • 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() };
      
  • La función "update" seguirá haciendo operaciones sólo cuando sea tiempo de actualizar. Y lo que agregaremos será el "switch" en la función "update" para actualizar la posición x-y (celda)  según la dirección actual: 
    • if Instant::now() - 
              self.ultima_actualizacion >= 
              Duration::from_millis(MILISEG_POR_ACTUALIZACION) {
          match self.direccion {
              Direccion::Arriba => 
              self.jugador_y = self.jugador_y - 1,
              Direccion::Abajo => 
              self.jugador_y = self.jugador_y + 1,
              Direccion::Izquierda => 
              self.jugador_x = self.jugador_x - 1,
              Direccion::Derecha => 
              self.jugador_x = self.jugador_x + 1,
          }
          self.ultima_actualizacion = Instant::now();
      }
      Ok(())
      
  • Y esto es lo que agregaremos para el "key_down_event" que requiere ggez. Básicamente este método captura la entrada del usuario y nos la presenta como un "ggez::event::KeyCode que debemos de manejar. El "_ => ()" al final del "match" simplemente es para manejar el resto de los casos de los otros posibles ggez::event::KeyCode sin hacer nada.
    • 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 => 
              self.direccion = Direccion::Arriba,
              ggez::event::KeyCode::Down => 
              self.direccion = Direccion::Abajo,
              ggez::event::KeyCode::Left => 
              self.direccion = Direccion::Izquierda,
              ggez::event::KeyCode::Right => 
              self.direccion = Direccion::Derecha,
              _ => (),
          }
          ()
      }
      
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
 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
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;

enum Direccion {
    Arriba,
    Abajo,
    Izquierda,
    Derecha,
}

struct EstadoDelJuego {
    jugador_x: i16,
    jugador_y: i16,
    direccion: Direccion,
    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) {
            match self.direccion {
                Direccion::Arriba => 
                self.jugador_y = self.jugador_y - 1,
                Direccion::Abajo => 
                self.jugador_y = self.jugador_y + 1,
                Direccion::Izquierda => 
                self.jugador_x = self.jugador_x - 1,
                Direccion::Derecha => 
                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 key_down_event(
        &mut self,
        _ctx: &mut Context,
        keycode: ggez::event::KeyCode,
        _keymod: ggez::event::KeyMods,
        _repeat: bool,
    ) {
        match keycode {
            ggez::event::KeyCode::Up => 
            self.direccion = Direccion::Arriba,
            ggez::event::KeyCode::Down => 
            self.direccion = Direccion::Abajo,
            ggez::event::KeyCode::Left => 
            self.direccion = Direccion::Izquierda,
            ggez::event::KeyCode::Right => 
            self.direccion = Direccion::Derecha,
            _ => (),
        }
        ()
    }
}

fn main() {
    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() };
    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, pero si presionamos las teclas podemos cambiar su dirección. Todavía no ponemos lógica de fin del juego así que el cuadro/jugador puede seguir saliendose del tablero:


En las siguientes entradas seguiremos creciendo este ejemplo según el ejemplo Snake de github. Lo siguiente será poner la lógica de "game over" para que el jugador no se salga del tablero.

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuentes (inglés):

No hay comentarios.:

Publicar un comentario