domingo, 14 de abril de 2019

Creando videojuegos en Rust lang (Parte 20)

Ahora continuaremos con el ejemplo "HelloGgez" de la página de Github de ggez: https://github.com/ggez/ggez/blob/master/docs/guides/HelloGgez.md El ejemplo simplemente imprime un ciclo/bucle que imprime "Hello ggez!" y un contador de intervalo de tiempo. La página de Github nos guía en los pasos. Y en la parte 4 avanzamos un poco con este ejemplo. Para resumir lo que hicimos en la parte anterior que trabajamos con ggez:

1. Creamos un proyecto llamado "hello_ggez02" con "cargo new hello_ggez02"
2. Cambiamos el archivo toml para poner la dependencia a ggez:

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

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

3. Compilamos el proyecto para que se descarguen y compilen las dependencias de ggez. Pero en realidad no cambiamos nada del código, es decir, seguía siendo el "Hola mundo" que genera "cargo new" de forma predeterminada.

Ahora modificaremos el código. La página de github guía la creación del archivo mediante modificaciones secuenciales. En vez de hacer eso, mostraré el código terminado.

Primero un poco de teoría sobre ggez:
  • ggez require de un "struct" que tenga todo el estado del juego. Es decir, una representación de todo el juego en forma de datos. Es algo que representa todo el juego como datos en memoria. 
  • Vimos en los ejemplos anteriores como usar Result, en realidad ggez usa una variante llamada "GameResult" pero funciona de forma similar.
  • Los juegos de vídeo son en cierto sentido un ciclo o bucle infinito donde se disparan eventos, actualizamos datos y dibujamos datos. Esta parte se llama el "game loop" o bucle de juego. Usando ggez este se encargará de configurar y administrar el ciclo y ggez sólo llamará nuestro código. En este caso ggez proporciona una interfaz ("trait") llamado EventHandler, esta nos guía para implementar lo que queramos que ggez llame durante el bucle del juego. Como requisito mínimo hay que implementar "update" y "draw", para actualizar los datos y dibujar el juego respectivamente. Estas dos funciones "update" y "draw" deben de regresar un "GameResult".
  • ggez requiere que construyamos una variable que contiene las configuraciones. Podemos generar las opciones predeterminadas con "conf::Conf::new()".
  • Usando las configuraciones anteriores, el nombre el programa (string) y el nombre del autor (string) podemos construir una variable de contexto y un bucle de juego. La función que usamos en ggez es "ContextBuilder::new("nombre_del_juego", "nombre_del_autor").conf(configuracion).build().unwrap()" y esta función regresa una tupla de referencias. Tenemos que hacer que la tupla encaje un patrón para descomponerla así que la asignamos a "(ref mut contexto, ref mut bucle_de_juego)" el "ref" es simplemente similar a usar "&" sólo que se usa "ref" cuando estamos buscando algo que encaje un patrón (lado izquierdo de la igualdad) en lugar de cuando lo usamos en el lado derecho de la igualdad. Es decir "let ref mut x = y" es lo mismo que "let x = &mut y". 
  • Finalmente le decimos a ggez que corra nuestro contexto, bucle de juego y estado de juego (struct). Esto lo hacemos usando "event::run" y le pasamos nuestro contexto, bucle_de_juego,  y estado_del_juego.
Considerando lo anterior estos son los cambios importantes que se hicieron al "main.rs":
  • Agregar "use ggez::*;" al inicio del archivo. Esto es para que Rust cargue las variables y interfaces que vienen en ggez.
  • Agregar un struct que contenga el estado del juego llamado "EstadoDelJuego". Es algo que representa todo el juego como datos en memoria. En este caso sólo tendrá una variable donde guardaremos una duración "dt" usando "std::time::Duration".
  • Agregaremos una implementación de ggez::event:EventHandler al struct de EstadoDelJuego. EventHandler es una interfaz ("trait") que nos proporciona ggez. Dentro de esta interfaz nos pide implementar mínimamente los métodos "update" y "draw".
    • El método "update" sirve para actualizar el EstadoDelJuego y toma como entrada el estado del juego ("self") y una variable de contexto que creamos con ggez. Dentro de la función "update" calculamos el "dt" usando "timer::delta(contexto)" que nos proporciona ggez. Esta función debe regresar un GameResult que es parecido a un Result, en este caso regresamos "Ok(())", que es Ok con la unidad (tupla vacía) adentro.
    • El método "draw"sirve para dibujar en la pantalla el EstadoDelJuego y también toma como entrada el estado del juego ("self") y una variable de contexto que creamos con ggez. Dentro de esta función imprimimos nuestro valor de "dt" en nanosegundos. Esta función debe regresar un GameResult que es parecido a un Result, en este caso regresamos "Ok(())", que es Ok con la unidad (tupla vacía) adentro.
  • En el método main hacemos las siguientes operaciones en orden:
    • Inicializamos el estado del juego como una variable mutable y ponemos 0.0 en el valor de dt.
    • Usamos "conf::Conf::new()" para crear una configuración predeterminada.
    • Usamos "ContextBuilder::new" para crear la variable de contexto y la variable de bucle de juego a partir de nuestro nombre de autor (string), nuestro nombre del juego (string) y la variable de configuración. Llamamos el método ".build()" y luego ".unwrap()".
    • Usamos  "event::run" para decirle a ggez que empiece el bucle del juego con nuestra variable de contexto, nuestra variable de bucle de juego y nuestro estado del juego. De forma similar llamamos el método ".unwrap()".
Básicamente la estructura de la ejecución del juego es la siguiente donde el bucle del juego es administrado por ggez:


Todo lo anterior y los detalles de los métodos se pueden ver en el siguiente código.


 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
use ggez::*;

struct EstadoDelJuego {
    dt: std::time::Duration
}

impl ggez::event::EventHandler for EstadoDelJuego {
    fn update(&mut self, 
            contexto: &mut Context) -> GameResult<()> {
        self.dt = timer::delta(contexto);
        Ok(())
    }
    fn draw(&mut self, 
            _contexto: &mut Context) -> GameResult<()> {
        println!("Hola ggez! dt = {}ns", 
                self.dt.subsec_nanos());
        Ok(())
    }
}

fn main() {
    let estado_del_juego = &mut EstadoDelJuego {
        dt: std::time::Duration::new(0,0) };
    let configuracion = conf::Conf::new();
    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();
}

Notar como en el código anterior estamos usando ".unwrap()" para desenvolver todas las salidas. En este caso no hay problema porque el ejemplo es muy fácil así que no esperamos que el programa entre en pánico por un Err en lugar de un Ok.

Al correr el código se abre una ventana en blanco y la consola empieza a imprimir mensajes. De hecho como entrará en un ciclo infinito debemos detenerlo cerrando la ventana blanca que se abre.

Este es el resultado de correr el código con "cargo run":

Y este es el resultado de correrlo usando "cargo run --release":


Al ejecutarlo con "cargo run --release" el compilador hace optimizaciones que tardan más tiempo en compilar pero el programa corre más rápido y por lo tanto el juego corre más rápido.

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

sábado, 13 de abril de 2019

Creando videojuegos en Rust lang (Parte 19)

Programa 13. "pre_hello_ggez13" Manejando excepciones y salidas de funciones: "Result"
Al usar librerías o funciones tenemos situaciones donde puede haber un error interno en la librería. En estas situaciones sería ideal que nos regresen un error describiendo la naturaleza de la falla. Podríamos usar "Option" como se vio en la sección pasada, sin embargo Option sólo nos deja definir un tipo de dato. Esta limitación se vuelve un problema si esperamos que el error sea un String pero el valor normal (sin error) es otro tipo de dato. Por ejemplo si esperamos que el valor de retorno es un entero mientras que el mensaje de error es un String. Para estas situaciones usamos el enum "Result" que incluye Rust de forma predeterminada.

El enum "Result" de Rust tiene la siguiente definición que permite dos tipos de datos genéricos <T, E>:

1
2
3
4
enum Result<T, E> {
   Ok(T),
   Err(E),
}

Como se puede observar puede tomar como entrada dos tipos diferentes de datos "T" y "E". La "E" es para el tipo de datos del error. Y la "T" es para el tipo de dato del resultado correcto, parecido a Option. Y los nombres de los contenedores de T y E son "Ok" y "Err". Result es usado por muchas funciones de la librería estándar "std" de Rust.

¿Cómo extraemos los datos dentro de un Result? De forma similar a cuando usamos Option, hay varias opciones para extraer los valores internos de un Result:
  • Usar "match". Similar a como se manejan los "enum".
  • Usar el método de "Result" llamado ".unwrap()". Funciona de forma similar a su uso con  Option. Si el valor es Ok entonces regresa el valor interno T. Si el valor es Err entonces entra en pánico y detiene la ejecución del programa. 
  • Usar el método de "Result" lamado ".unwrap_or(default)". Funciona de forma similar a su uso con Option. Si el valor es Ok entonces regresa el valor interno T. Si el valor es Err entonces regresa el valor "default" que tiene que ser del mismo tipo T. 
  • Usar el método de "Result" llamado ".expect("mensaje de error")". Funciona de forma similar a su uso con Option. Si el valor es Ok entonces regresa el valor interno T. Si el valor es Err entonces entra en pánico y regresa el "mensaje de error" y intenta imprimir el tipo E después del mensaje de error. 
  • Si estamos dentro de una función que regresa "Result" y tenemos múltiples llamadas que regresan "Result" podemos usar el operador "?" para simplificar la lógica de regresar Err. Si ponemos "?" al final de una línea esta regresará el valor de Ok desenvuelto si todo salió bien. Por otra parte si hubo un error "Err" como salida entonces iniciará una salida prematura en nuestra función. Es decir, nos ahorra hacer múltiples bloques "match" o bloques "if" para cada llamada y nos permite regresar Error "Err" inmediatamente. Es por esta razón que este operador "?" sólo funciona dentro de una función que regresa Result.
También tenemos unwrap_err() and expect_err() que tienen el comportamiento opuesto a unwrap() y expect(). Es decir, intentan regresar E o en otro caso entran en pánico con Ok y regresan el valor interno de Ok como parte del mensaje de error. Otra opción es usar los métodos "ok()" o "err()", estos métodos de "Result" sirven para convertir el Result a un Option. Finalmente, si lo necesitamos, hay un método de "Result" llamado ".is_ok()" que regresa un booleano (verdadero o falso) que nos dice si es Ok o no. De forma contraria hay otro método de "Result" llamado ".is_err()" y este valor es un booleano (verdadero o falso) que nos dice si es Err o no.

Todo esto se muestra a continuación:


 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
fn checar_division(dividendo: i32, divisor: i32) -> Result<i32,String> {
    if divisor == 0 {
        Err(String::from("División entre cero"))
    } else {
        Ok(dividendo/divisor)
    }
}

fn checar_resultado(entrada: &Result<i32,String>) {
    match entrada {
        Result::Err(texto) => println!("Error, mensaje: {}", texto),
        Result::Ok(var) => println!("Resultado: {}", var)
    }
}

fn procesar_div(dividendo: i32, 
                divisor: i32) -> Result<i32,String> {
    println!("Primera división: {}/{}", dividendo, divisor);
    let cociente1 = checar_division(dividendo, divisor)?;
    println!("Segunda división: {}/{}", cociente1, divisor-1);
    let cociente2 = checar_division(cociente1, divisor-1)?;
    println!("Tercera división: {}/{}", cociente2, divisor-2);
    let cociente3 = checar_division(cociente2, divisor-2)?;
    Result::Ok(cociente3)
}

fn main() {
    let x: Result<i32, String> = checar_division(6,3);
    let y: Result<i32, String> = checar_division(0,0);
    println!("x.is_ok() = {}", x.is_ok());
    println!("y.is_ok() = {}", y.is_ok());
    println!("x.is_err() = {}", x.is_err());
    println!("y.is_err() = {}", y.is_err());
    checar_resultado(&x);
    checar_resultado(&y);
    println!("procesar_div(6,0) = {:?}",procesar_div(6, 0));
    println!("procesar_div(6,1) = {:?}",procesar_div(6, 1));
    println!("procesar_div(6,2) = {:?}",procesar_div(6, 2));
    println!("procesar_div(6,3) = {:?}",procesar_div(6, 3));
    println!("x.unwrap() = {}", x.unwrap());
    println!("No podemos usar y.unwrap porque detendría la ejecución");
    // Reasignando los resultados ya que unwrap() mueve el valor
    let x: Result<i32, String> = checar_division(6,3);
    println!("x.unwrap_or(0) = {}", x.unwrap_or(0));
    println!("y.unwrap_or(0) = {}", y.unwrap_or(0));
    // Reasignando los resultados ya que unwrap_or() mueve el valor
    let x: Result<i32, String> = checar_division(6,3);
    let y: Result<i32, String> = checar_division(0,0);
    println!("x.ok() = {:?}", x.ok());
    println!("y.ok() = {:?}", y.ok());
    // Reasignando los resultados ya que ok() mueve el valor
    let x: Result<i32, String> = checar_division(6,3);
    let y: Result<i32, String> = checar_division(0,0);
    println!("x.err() = {:?}", x.err());
    println!("y.err() = {:?}", y.err());
    // Reasignando los resultados ya que err() mueve el valor
    let x: Result<i32, String> = checar_division(6,3);
    let y: Result<i32, String> = checar_division(0,0);
    println!("Sin x.unwrap_err ya que detiene la ejecución");
    println!("y.unwrap_err() = {:?}", y.unwrap_err());
    // Reasignando 'y' ya que unwrap_err mueve el valor
    let y: Result<i32, String> = checar_division(0,0);
    println!("Sin x.expect_err ya que detiene la ejecución, y da el valor");
    println!("y.expect_err() = {:?}", y.expect_err("Esperando error"));
    // Reasignando 'y' ya que expect_err mueve el valor
    let y: Result<i32, String> = checar_division(0,0);
    println!("x.expect(mensaje) = {:?}", x.expect("Intentando dividir x"));
    // La siguiente línea interrumpe la ejecución ya que es un Err:
    println!("y.expect(mensaje) = {:?}", y.expect("Intentando dividir y"));
}

Este es el resultado de correr el código:


Notamos como la última línea interrumpe la ejecución por ser un .expect() llamado a un Err. Y vemos en la captura como nos muestra el mensaje de nuestro expect "Intentando dividir y" además que el mensaje de error interno "División entre cero".

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

viernes, 12 de abril de 2019

Creando videojuegos en Rust lang (Parte 18)

Programa 12. "pre_hello_ggez12" Manejando variables que pueden ser vacías: "Option"
En lenguajes de programación como C, C++ o Java tenemos situaciones donde se anula una referencia o no hay valor que podamos regresar así que regresamos "null" (un valor nulo). En cambio en Rust no existe "null" y en su lugar se sugiere usar un enum llamado "Option". Este enum de "Option" viene incluido en la librería predeterminada de Rust así que lo podemos usar sin definirlo.

El enum "Option" hace uso de valores genéricos. Los genéricos también funcionan con "enum", de forma similar a los "structs". La definición de Option es la siguiente, hay que notar que usa un valor genérico "<T>" :

1
2
3
4
enum Option<T> {
    None,
    Some(T)
}

La definición anterior nos permite asignar "None" en el caso que no haya valor o resultado. De otra forma asignamos "Some" y ponemos entre el paréntesis la variable que obtenemos. Así podemos manejar datos que pueden no tener valor. Simplemente hay que envolverlos en una Option.

Para obtener el valor interno podemos considerar 4 opciones:
  • Usar una instrucción "match" y obtener de ahí el valor que necesitamos. De esta forma también podemos manejar el comportamiento cuando el valor este vacío ("None").
  • Usar el método de Option llamado "unwrap()". Unwrap ("desenvolver" en inglés) hace lo que su nombre dice, regresa el valor sin en envoltorio de "Option". Su desventaja es que si no hay valor entonces entra en pánico y detiene la ejecución del programa.
  • Usar el método de Option llamado "unwrap_or(default)". Similar a unwrap, con la diferencia de que si es None entonces regresa el valor especificado como el "default. Así se evita que detenga la ejecución ya que proporciona el valor default en caso que este como "None".
  • Usar el método de Option llamado "expect("mi mensaje de error")". El método "expect" regresa el valor interno del Option si lo hay y si no lo hay detiene la ejecución y muestra el mensaje personalizado de configuraste, es decir en el caso anterior mostraría "mi mensaje de error".
Las manipulaciones descritas anteriormente se muestran en el código siguiente donde usamos la división entre cero como ejemplo de una situación donde podemos usar "Option" y manejarlo:


 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
fn checar_division(dividendo: i32, divisor: i32) -> Option<i32> {
    if divisor == 0 {
        None
    } else {
        Some(dividendo/divisor)
    }
}

fn checar_opcion(entrada: Option<String>) {
    match entrada {
        Option::None => println!("Esta vacío"),
        Option::Some(value) => println!("Tiene un valor, y el valor es {}",
                                        value)
    }
}

fn checar_cociente(input: Option<i32>) {
    match input {
        Option::None => println!("Falló la divisón"),
        Option::Some(cociente) => println!("Calculado, el cociente es {}",
                                            cociente)
    }
}

fn main() {
    let op1: Option<String> = Option::None;
    let op2: Option<String> = Option::Some(String::from("Mi texto"));
    checar_opcion(op1);
    checar_opcion(op2);
    let op3 = checar_division(6, 3);
    let op4 = checar_division(4, 0);
    println!("op3: {:?}", op3);
    println!("op4: {:?}", op4);
    println!("op3.unwrap(): {:?}", op3.unwrap());
    // No ejecutamos op4.unwrap() porque detendría la ejecución
    checar_cociente(op3);
    checar_cociente(op4);
    println!("op3.unwrap_or(0): {:?}", op3.unwrap_or(0));
    println!("op4.unwrap_or(0): {:?}", op4.unwrap_or(0));
    println!("op3 usando expect: {:?}", op3.expect("Division entre cero"));
    println!("op4 usando expect: {:?}", op4.expect("Division entre cero"));
}

El resultado de correr el programa es el siguiente:

Podemos ver que se detuvo la ejecución en el "expect" y que imprimió nuestro mensaje personalizado.

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

jueves, 11 de abril de 2019

Creando videojuegos en Rust lang (Parte 17)

Programa 11. "pre_hello_ggez11" Tipos de dato genéricos: "generics" o "<...>"
En algunas situaciones queremos crear varios structs o enums similares en forma pero con diferentes tipos de datos (eg. ( ), i32, i64, f32, f64, String, etc.) En estos casos esto llevaría a mucho código repetido, ya que tendríamos que hacer una copia para cada tipo de dato. Para evitar el código repetido Rust tiene un sistema para definir datos genéricos ("generics") en donde definimos la estructura pero dejamos libre el tipo de dato que puede contener. Luego al usarlos podemos asignar un tipo de dato concreto.

Para definir las variables genéricas usaremos los símbolos de mayor que y menor que. En medio de los símbolos podemos un identificador (palabra) en CamelCase que representa nuestra variable genérica. Por convención usaremos "T" que viene de "tipo" escrito en CamelCase. Para variables adicionales usamos las letras siguientes: "U", "V", etc.

Los tipos de datos genéricos también nos ayudaran a definir funciones genéricas y facilitan el uso de librerías/crates ya que podemos usar structs genéricos como contenedores de nuestros structs concretos. De la misma forma aunque tengamos structs genéricos podemos definir implementaciones ("impl") específicas para cada tipo de dato. No entraremos mucho a detalle a este último uso con los traits y sólo mostraré ejemplos de lo siguiente:
  • Usando Vec<T>. Vec es un objeto que viene incluido en las librerías predeterminadas de Rust. Este objeto es básicamente un vector y se le pueden aplicar las operaciones usuales de un vector. Se puede crear un Vec de diferentes tipos y podemos usar la macro "vec!" para crear vectores a partir de arreglos. Otros objetos definidos por paquetes también usan valores genéricos para exponer su funcionalidad a varios usuarios.
  • Usando structs genéricos. Podemos usar T, U, V, etc. para definir específicamente si esperamos que sean del mismo tipo o si permiten que sean tipos diferentes.
  • Usando funciones genéricas. Similar al caso anterior podemos definir lo que esperamos y el compilador no nos causará problemas al llamar la función siempre y cuando se emparejen con la definición genérica.
El código es el siguiente:


 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
#[derive(Debug)]
struct StructGenerico<T> {
    x: T,
    y: Vec<T>
}

#[derive(Debug)]
struct StructGenerico2<T, U> {
    x: T,
    y: U
}

fn fn_generica1<T>(_x: T) { 
    println!("Procesando un tipo");
}

fn fn_generica2<T>(_x: T, _y: T) { 
    println!("Procesando dos tipos iguales");
}

fn fn_generica3<T, U>(_x: T, _y: U) { 
    println!("Procesando dos tipos que pueden ser diferentes");
}

fn main() {
    let vec1 : Vec<i32> = vec![5,4,3,2,1,0];
    let vec2 : Vec<f32> = vec![1.01, 2.34, 5.67];
    let vec3 : Vec<()> = vec![(), ()];
    println!("vec1.len() = {:?} y contenido {:?}", vec1.len(), vec1);
    println!("vec2.len() = {:?} y contenido {:?}", vec2.len(), vec2);
    println!("vec3.len() = {:?} y contenido {:?}", vec3.len(), vec3);
    let mi_generico1: StructGenerico<i32> = StructGenerico::<i32>{
        x: 10, y: vec![1,2,3,4]};
    let mi_generico2: StructGenerico<f64> = StructGenerico::<f64>{
        x: 3.14, y: vec![1.1,2.2,3.3]};
    let mi_generico3: StructGenerico<()> = StructGenerico::<()>{
        x: (), y: vec![(),(),(),()]};
    println!("Entero mi_generico1 = {:?}", mi_generico1);
    println!("Double mi_generico2 = {:?}", mi_generico2);
    println!("Unidad mi_generico3 = {:?}", mi_generico3);
    let mi_generico4 : StructGenerico2<i32, String> = StructGenerico2{
        x: 10, y: String::from("hola")};
    let mi_generico5 : StructGenerico2<(), f32> = StructGenerico2{
        x: (), y: 5.43};
    println!("mi_generico4 = {:?}", mi_generico4);
    println!("mi_generico5 = {:?}", mi_generico5);
    fn_generica1(7);
    fn_generica1(3.1416);
    fn_generica2(1,3);
    fn_generica2("string1","string2");
    fn_generica3(7,"hola");
    fn_generica3(vec![1,2],6.3982);
}

Y el resultado de correr el código es el siguiente:

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

martes, 9 de abril de 2019

Creando videojuegos en Rust lang (Parte 16)

Programa 10. "pre_hello_ggez10" Implementando una interfaz ("traits") en Rust: "impl"
En esta entrada usaremos el mismo archivo de la entrada anterior. Vamos a implementar los métodos del "trait" OpsVector2d que creamos en la entrada pasada. Para imprimir los valores ponemos "#[derive(Debug)]" en los lugares correspondientes en struct Vector2d y en el enum ModuloDeVector.

El elemento más importante a agregar es un bloque denotado con la clave "impl". Dentro de este bloque tenemos que escribir la firma de la función y escribir el código de la función correspondiente. La función debe regresar el tipo de dato requerido por el "trait". El bloque impl define la implementación trait para un struct y la sintaxis es "impl <trait> for <struct>". Imprimimos los valores de las funciones con diferentes entradas para probarlas.


  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
#[derive(Debug)]
struct Vector2d {
    x: i32,
    y: i32
}

#[derive(Debug)]
enum ModuloDeVector {
    MayorAUno,
    ExactamenteUno,
    MenorAUno
}

trait OpsVector2d {
    fn modulo_comparado_con_uno(&self) -> ModuloDeVector;
    fn igualdad(&self, v2: &Vector2d) -> bool;
    fn producto_punto(&self, v2: &Vector2d) -> i32;
    fn suma(&self, v2: &Vector2d) -> Vector2d;
    fn producto_escalar(&self, escalar: i32) -> Vector2d;
}

fn producto_punto_y_luego_escalar(
        entrada1: &impl OpsVector2d, 
        entrada2: &Vector2d) -> Vector2d {
    let resultado1 = entrada1.producto_punto(entrada2);
    entrada1.producto_escalar(resultado1)
}

impl OpsVector2d for Vector2d {
    fn modulo_comparado_con_uno(&self) -> ModuloDeVector {
        let modulo_al_cuadrado = (self.x * self.x) + 
                                 (self.y * self.y);
        if modulo_al_cuadrado > 1 {
            ModuloDeVector::MayorAUno
        } else if modulo_al_cuadrado < 1 {
            ModuloDeVector::MenorAUno
        } else {
            ModuloDeVector::ExactamenteUno
        }
    }
    fn igualdad(&self, v2: &Vector2d) -> bool {
        if self.x == v2.x && self.y == v2.y {
            true
        } else {
            false
        }
    }
    fn producto_punto(&self, v2: &Vector2d) -> i32 {
        (self.x * v2.x) + (self.y * v2.y)
    }
    fn suma(&self, v2: &Vector2d) -> Vector2d {
        Vector2d { x:self.x + v2.x,
                   y:self.y + v2.y }
    }
    fn producto_escalar(&self, escalar: i32) -> Vector2d {
        Vector2d { x:self.x * escalar, 
                   y:self.y * escalar }
    }
}

fn main() {
    let mi_vector1: Vector2d = Vector2d { x: 2, y: 3};
    let mi_vector2: Vector2d = Vector2d { x: 1, y: 0};
    let mi_vector3: Vector2d = Vector2d { x: 0, y: 0};
    println!("modulo_comparado_con_uno1 : {:?}",
        mi_vector1.modulo_comparado_con_uno());
    println!("modulo_comparado_con_uno2 : {:?}", 
        mi_vector2.modulo_comparado_con_uno());
    println!("modulo_comparado_con_uno3 : {:?}", 
        mi_vector3.modulo_comparado_con_uno());
    println!("igualdad1 : {:?}", 
        mi_vector1.igualdad(&mi_vector1));
    println!("igualdad2 : {:?}", 
        mi_vector1.igualdad(&mi_vector2));
    println!("suma1 : {:?}", 
        mi_vector1.suma(&mi_vector2));
    println!("suma2 : {:?}", 
        mi_vector2.suma(&mi_vector3));
    println!("suma3 : {:?}", 
        mi_vector1.suma(&mi_vector3));
    println!("producto_escalar1 : {:?}", 
        mi_vector1.producto_escalar(2));
    println!("producto_escalar2 : {:?}", 
        mi_vector2.producto_escalar(-1));
    println!("producto_escalar3 : {:?}", 
        mi_vector1.producto_escalar(3));
    println!("producto_escalar4 : {:?}", 
        mi_vector1.producto_escalar(-4));
    println!("producto_punto1 : {:?}", 
        mi_vector1.producto_punto(&mi_vector2));
    println!("producto_punto2 : {:?}", 
        mi_vector1.producto_punto(&mi_vector1));
    println!("producto_punto_y_luego_escalar1 : {:?}", 
        producto_punto_y_luego_escalar(&mi_vector1, &mi_vector2));
    println!("producto_punto_y_luego_escalar2 : {:?}", 
        producto_punto_y_luego_escalar(&mi_vector2, &mi_vector1));
    println!("producto_punto_y_luego_escalar3 : {:?}", 
        producto_punto_y_luego_escalar(&mi_vector1, &mi_vector3));
    println!("'Trait' definido, implementado y usado");
}

El resultado del código es el siguiente:


Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

lunes, 8 de abril de 2019

Creando videojuegos en Rust lang (Parte 15)

Programa 9. "pre_hello_ggez9" Definiendo el equivalente a una interfaz en Rust: "traits"
La primera parte de la definición de una función la podemos llamar la "firma" de una función. La "firma" de la función es el nombre de la función, las entradas de la función y las salidas de la función. Si no nos interesa la implementación (el código) podemos sólo prestar atención en el nombre de la función. De esta forma si le pasamos a la función la(s) entrada(s) necesaria(s) podemos confiar que la salida sea correcta.

En este sentido podemos construir código que confíe en que la implementación es correcta. El requisito será que necesitaremos una función que siga la "firma" requerida por nuestro código. En Java o C# lo anterior lo logramos con el concepto de Interfaz o Interfaces. En Rust tenemos algo parecido que lo llamamos "traits". En Rust en realidad no tenemos "objetos" como los tenemos en Java o C# o en cualquier otro lenguaje orientado a objetos. Así que las "traits" las implementamos dentro de "structs".

En Rust las "traits" son una forma de definir las firmas de funciones/métodos que queremos que tenga implementados un struct. Por ahora sólo crearemos "traits". En la siguiente entrada veremos como "implementar" un "trait" en un struct. Con los "traits" podemos escribir código que confíe en obtener la salida correcta al llamar estos métodos y así ahorrarnos escribir el código.

En programación orientada a objectos podemos usar las variables definidas dentro del objeto. De la misma forma en estas funciones implementadas dentro de un "struct" podemos usar el struct. Ya sea como una referencia de lectura a si mismo ("&self") o una referencia mutable a sí mismo
("&mut self"). En donde usaremos "self" para acceder a los elementos del struct.

En el siguiente ejemplo definimos un struct para tener datos como  un vector 2d (x,y) y un enum para guardar si el módulo (o norma) del vector 2d es igual a 1, mayor a 1 o menor a 1. Para simplificar este caso usaremos puros números enteros. Definimos un "trait" (interfaz) que queremos que tenga un objeto para poder hacer operaciones de vectores 2d. En este ejemplo debe poder:

  • Dado la estructura contenedora (self), obtener el enum que corresponde con el módulo del vector. La función "modulo_comparado_con_uno". En este caso nosotros nos aseguraremos manualmente que "&self" siempre sea de tipo Vector2d ya que en una entrada futura sólo implementaremos este "trait" dentro de la estructura Vector2d.
  • Dado la estructura contenedora (self) y un struct de tipo vector2d determinar si son iguales y regresar un booleano correspondiente al resultado.
  • Dado la estructura contenedora (self) y un struct de tipo vector2d determinar el producto punto entre estos dos y regresar el entero resultante.
  • Dado la estructura contenedora (self) y un struct de tipo vector2d determinar la suma de los dos vectores y regresar el Vector2d resultante.
  • Dado la estructura contenedora (self) y un escalar (entero) calcular el producto escalar y regresar el Vector2d resultante.
Para ver la facilidades que proporciona definir un "trait" también agregué una función llamada "producto_punto_y_luego_escalar". Esta función toma una entrada1 una variable de tipo "&impl OpsVector2d" y otra de tipo "&Vector2d". Lo que hace la definición "&impl OpsVector2d" es que acepte como válidas todo tipo de dato que tenga implementado ("impl") el trait OpsVector2d, así sólo podemos dejar pasar estructuras que tengan OpsVector2d implementado. De la misma forma al definirlo de forma genérica usando "&impl" podemos tomar varios tipos diferentes ya que nunca mencionamos ningún tipo específico, nuestro único requisito es que implemente OpsVector2d. Por otra parte la segunda entrada la definimos como "&Vector2d" así que sólo variables de tipo Vector2d pueden entrar.


Este es el código que muestra lo anterior con algunas variables sin uso (con "_" como prefijo) para evitar warnings:


 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
struct Vector2d {
    x: i32,
    y: i32
}

enum ModuloDeVector {
    MayorAUno,
    ExactamenteUno,
    MenorAUno
}

trait OpsVector2d {
    fn modulo_comparado_con_uno(&self) -> ModuloDeVector;
    fn igualdad(&self, v2: &Vector2d) -> bool;
    fn producto_punto(&self, v2: &Vector2d) -> i32;
    fn suma(&self, v2: &Vector2d) -> Vector2d;
    fn producto_escalar(&self, escalar: i32) -> Vector2d;
}

fn producto_punto_y_luego_escalar(
        entrada1: &impl OpsVector2d, 
        entrada2: &Vector2d) -> Vector2d {
    let resultado1 = entrada1.producto_punto(entrada2);
    entrada1.producto_escalar(resultado1)
}

fn main() {
    let _enum_modulo_op1 = ModuloDeVector::MayorAUno;
    let _enum_modulo_op2 = ModuloDeVector::MenorAUno;
    let _enum_modulo_op3 = ModuloDeVector::ExactamenteUno;
    let _mi_vector1: Vector2d = Vector2d { x: 2, y: 3};
    let _mi_vector2: Vector2d = Vector2d { x: 1, y: 0};
    let _mi_vector3: Vector2d = Vector2d { x: 0, y: 0};
    println!("¡El 'trait' está definido fin del programa!");
}

El resultado de correr el código anterior es:

Como podemos ver no evitamos algunos warnings por código sin utilizar, pero no hay problema, en la próxima entrada intentaremos implementar el trait "OpsVector2d" en el struct "Vector2d" y probaremos nuestro código.

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

domingo, 7 de abril de 2019

Creando videojuegos en Rust lang (Parte 14)

Programa 8. "pre_hello_ggez8" Definiendo funciones y llamando funciones: "fn" y "->"
En Rust también podemos definir funciones y llamarlas. Al definir funciones lo hacemos con la palabra clave "fn" seguido por un espacio y el nombre de la función, luego en paréntesis ponemos las variables de entrada, y finalmente ponemos "->" seguido por el tipo de dato que queremos en las salidas. Las funciones las encerramos entre llaves "{ ... }" entre las cuales hacemos las operaciones que queramos dentro de estas. 

Hablando sobre las entradas de funciones. Las funciones que hagamos pueden tomar un número variado de entradas o argumentos: 0, 1, 2, etc. Para no tomar entradas basta con dejar los paréntesis vacíos "mi_funcion()" y para poner entradas hay que definir el nombre y los tipos de variables que vamos a usar.

Hablando sobre las salidas de las funciones. Las funciones pueden regresar un número variado de argumentos o salidas, pero sólo podemos regresar una variable. Es decir para mandar varias salidas debemos de agruparlos en una tupla o otro objecto y así podremos regresar varias salidas. Por otra parte cuando no hay salida la función regresará "()", la unidad o tupla vacía. En Rust todas las líneas son expresiones así que si ponemos punto y coma ";" al final entonces la función intentará regresar la unidad "()", es decir la tupla vacía. Así que para que regrese la última expresión tenemos que dejar esa última línea sin punto y coma.

Los siguientes ejemplos muestran diferentes funciones:
  • Función "holamundo" que no tiene entradas ni salidas (regresa "()" implícitamente). Imprime "Hola mundo". Notar como pusimos un punto y coma ";" en la última línea de la función.
  • Función "sumar" que suma dos números enteros y regresa un número entero. Notar como en este caso no pusimos un punto y coma ";" en la última línea de la función.
  • Función "mensaje" que toma como entrada un "enum" y usa un "match" para imprimirlo.
  • Función "calcular" que toma como entrada una tupla de 3 elementos y regresa una tupla de 3 elementos. Es decir tiene 3 salidas que luego podemos asignar a variables diferentes.
  • Función "crear_estado_del_juego" que toma como entradas diferentes tipos de variables, construye un "struct" de EstadoDelJuego y lo regresa. 
  • Función "imprimir_estado_del_juego" que imprime una estructura EstadoDelJuego. Recordemos que no podemos imprimirla directamente  sin derivar "Debug" y usar "{:?}". Estes es un ejemplo de como podemos generar una función para imprimir una estructura de datos.
Este es el código:

 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
fn holamundo() {
    println!("Hola mundo");
}

fn sumar(numero1: i64, numero2: i64) -> i64 {
    numero1 + numero2
}

enum Direccion {
    Avanzar,
    Detenido(char),
    Click { x: i64, y: i64 }
}

fn mensaje(direccion_entrada: Direccion) {
    match direccion_entrada {
        Direccion::Avanzar => println!("¡Avanzar!"),
        Direccion::Detenido(mi_char) => println!("¡{}!", mi_char),
        Direccion::Click { x, y } => println!("¡x={} y={}!", x, y)
    };
}

fn calcular(entrada: (i32, i32, i32)) -> (i32, i32, i32) {
    let salida1 = entrada.0 + entrada.1 + entrada.2;
    let salida2 = entrada.0 - entrada.1 - entrada.2;
    let salida3 = entrada.0 * entrada.1 * entrada.2;
    (salida1, salida2, salida3)
}

struct EstadoDelJuego {
    id_jugador: u32,
    nombre_jugador: String,
    x: i32,
    y: i32,
    id_char: char
}

fn crear_estado_del_juego(id: u32, 
    nombre: String, v_x: i32, v_y:i32, 
    v_char: char) -> EstadoDelJuego {
    EstadoDelJuego{id_jugador: id,
        nombre_jugador: nombre,
        x: v_x, y: v_y, id_char: v_char}
}

fn imprimir_estado_del_juego(estado: EstadoDelJuego) {
    println!("J{} {} ({},{}) id {}", 
        estado.id_jugador, estado.nombre_jugador,
        estado.x, estado.y, estado.id_char);
}

fn main() {
    holamundo();
    holamundo();
    holamundo();
    println!("Suma 1: {}", sumar(9,6));
    println!("Suma 2: {}", sumar(3,5));
    println!("Suma 3: {}", sumar(4,2));
    let direccion_j1 = Direccion::Avanzar;
    let direccion_j2 = Direccion::Click{x: 6, y: 3};
    let direccion_j3 = Direccion::Detenido('p');
    mensaje(direccion_j1);
    mensaje(direccion_j2);
    mensaje(direccion_j3);
    let t1 = (1,2,1);
    let (suma, resta, multi) = calcular(t1);
    println!("suma = {}",suma);
    println!("resta = {}",resta);
    println!("multiplicacion = {}",multi);
    let mi_juego = crear_estado_del_juego(1,
        String::from("Jugador 1"),
        10, 5, 'a');
    imprimir_estado_del_juego(mi_juego);
    println!("¡Fin del juego!");
}

Y este es el resultado de correrlo:

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente (inglés):

sábado, 6 de abril de 2019

Creando videojuegos en Rust lang (Parte 13)

Programa 7. "pre_hello_ggez7" El equivalente a un "switch" en Rust: "match"
Si queremos dirigir el flujo de nuestro programa usando "enums" lo podemos hacer usando "match". En Rust "match" es muy parecido a un "switch" en C. Simplemente enlistas todos los casos a manejar y pones "=>" y una coma "," al final de ese caso. De hecho como los enums pueden ser "structs" o "tuple structs" podemos asignaros a variables ("tuple structs") o usar los nombres directamente ("structs").

 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
enum Direccion {
    Avanzar,
    Detenido(char),
    Click { x: i64, y: i64 }
}

fn main() {
    let direccion_jugador1 = Direccion::Avanzar;
    match direccion_jugador1 {
        Direccion::Avanzar => println!("¡Avanzar!"),
        Direccion::Detenido(input_char) => println!("¡{}!", input_char),
        Direccion::Click { x, y } => println!("¡x={} y y={}!", x, y)
    }
    let direccion_jugador2 = Direccion::Click {x: 6, y: 3};
    match direccion_jugador2 {
        Direccion::Avanzar => println!("¡Avanzar!"),
        Direccion::Detenido(input_char) => println!("¡{}!", input_char),
        Direccion::Click { x, y } => println!("¡x={} y y={}!", x, y)
    }
    let direccion_jugador3 = Direccion::Detenido('p');
    match direccion_jugador3 {
        Direccion::Avanzar => println!("¡Avanzar!"),
        Direccion::Detenido(input_char) => println!("¡{}!", input_char),
        Direccion::Click { x, y } => println!("¡x={} y y={}!", x, y)
    }
    println!("¡Fin del juego!");
}

El resultado de correr el código es el siguiente:
También hay funciones más avanzadas de "match". Podemos usar "|" para apuntar múltiples condiciones a un sólo grupo de código "a | b | c =>", así podemos replicar el comportamiento de "switch" en C/C++. También podemos usar "_" para manejar el resto de los casos similar a "default" en C/C++. Es decir podemos usar " _ => " para manejar los casos que no caen en los mencionados directamente.

Navegación:
Primera parte
Siguiente parte
Parte anterior

Fuente: