Adrianistán

El blog de Adrián Arroyo


Tutorial de Piston, programa juegos en Rust

- Adrián Arroyo Calle

Ya he hablado de Rust varias veces en este blog. La última vez fue en el tutorial de Iron, que os recomiendo ver si os interesa el tema del desarrollo web backend.

Hoy vamos a hablar de Piston. Piston es una de las librerías más antiguas del ecosistema Rust. Surgida cuando todavía no existía Cargo, esta librería está pensada para el desarrollo de juegos. No es la única que existe en Rust pero sí la más conocida. Piston es una librería que te enseñará Rust de la mejor forma. Y ahora quiero disculparme, porque Piston no es una librería, son un montón, pero eso lo veremos enseguida. En primer lugar creamos un proyecto nuevo con Cargo.
cargo new --bin ejemplo_piston
cd ejemplo_piston

Ahora abrimos el archivo Cargo.toml, vamos a añadir las dependencias necesarias. Las dependencias en Piston son un poco complicadas, veamos:

  • Existen las dependencias core, implementan la API fundamental pero no pueden usarse por separado, son window, input y event_loop. Se usan a través de piston.

  • Los backends de window, existen actualmente 3 backends: glutin, glfw, sdl2. Se importan manualmente.

  • Graphics, una API 2D, no presente en core, pero al igual que las dependencias core necesita un backend.

  • Los backends de graphics son varios: opengl, gfx y glium.

  • Existe una dependencia que nos deja todo montado, piston_window. Esta trae por defecto el core de Piston, glutin, graphics y gfx.

  • Luego existen dependencias extra, como por ejemplo para cargar texturas, estas las podremos ir añadiendo según las necesite el proyecto.


Para simplificar añadimos piston_window únicamente:

 


[package]
name = "piston_example"
version = "0.1.0"
authors = ["Adrián Arroyo Calle"]

[dependencies]
piston_window = "0.59.0"


 

Ahora abrimos el archivo main.rs. Añadimos la crate de piston_window y los módulos que vamos a usar.


extern crate piston_window;

use piston_window::*;
use std::path::Path;


 

Así mismo definimos un par de cosas para el resto del programa, la versión de OpenGL que usará Piston internamente y una estructura para guardar los movimientos de teclado.


const OPENGL: OpenGL = OpenGL::V3_1;

struct Movement{
up: bool,
down: bool,
left: bool,
right: bool
}


 

En la función main podemos crear la ventana, especificando título y tamaño. Más opciones como V-Sync, pantalla completa y demás también están disponibles.


fn main() {

let mut window: PistonWindow = WindowSettings::new("Piston - Adrianistan",[640,480])
.exit_on_esc(true)
.opengl(OPENGL)
.build()
.unwrap();


 

Ahora cargamos la tipografía Sinkin Sans, que vamos a usar para dibujar texto en pantalla. Como hay dos posibles localizaciones comprobamos esos dos lugares antes de salir del programa si no se consigue cargar la fuente.


let mut glyphs = Glyphs::new(Path::new("SinkinSans.ttf"),window.factory.clone()).unwrap_or_else(|_|{
let glyphs = Glyphs::new(Path::new("target/debug/SinkinSans.ttf"),window.factory.clone()).unwrap_or_else(|_|{
panic!("Failed to open the font file. Check that SinkinSans.tff is in the folder");
});
glyphs
});


 

Inicializamos la estructura de movimientos, generamos las dimensiones iniciales del rectángulo (que será un cuadrado en este caso), su color y la posición del ratón.


let mut mov = Movement{
up: false,
down: false,
left: false,
right: false
};

let mut dims = rectangle::square(50.0,50.0,100.0);
let mut rect_color = color::BLACK;

let mut mc: [f64; 2] = [0.0,0.0];


 

Ahora viene la parte importante, el bucle de eventos. El bucle va a funcionar infinitamente generando eventos por el camino (pueden ser eventos de inactividad también). Usamos la función draw_2d para dibujar en 2D. Hay dos maneras de dibujar un rectángulo, en primer lugar tenemos la forma abreviada y en segundo lugar una más completa que permite más opciones. Por último dibujamos el texto usando la fuente y realizando una transformación para que no quede el texto en la posición 0,0.


while let Some(e) = window.next() {
window.draw_2d(&e, |c, g| {
clear([0.5, 0.5, 0.5, 1.0], g);
rectangle([1.0, 0.0, 0.0, 1.0], // color rojo, rgba
[0.0, 0.0, 100.0, 100.0], // dimensiones
c.transform, g); // transormacion y donde se va a dibujar

let rect = Rectangle::new(rect_color);
rect.draw(dims,&c.draw_state,c.transform,g);
text(color::BLACK,18,"¡Saludos desde Piston!",&mut glyphs,c.transform.trans(100.0,200.0),g); // aplicamos una transormacion, movemos las X 100 y las Y 200
});


 

A continuación vamos a tratar cada evento de forma independiente, como todos los métodos devuelven Option, hemos de usar esta sintaxis con Some. En primer lugar tenemos un UpdateEvent, que básicamente nos informa del tiempo delta transcurrido. Recomiendo usar este evento para realizar los cambios en las geometrías, en este caso para mover el rectángulo.


if let Some(upd_args) = e.update_args() {
let dt = upd_args.dt;

if mov.right {
dims[0] += dt*100.0;
}
if mov.left {
dims[0] -= dt*100.0;
}
if mov.up {
dims[1] -= dt*100.0;
}
if mov.down {
dims[1] += dt*100.0;
}
}


Los siguientes dos eventos son opuestos, uno se activa cuando pulsamos una tecla y el otro cuando la soltamos. Comprobamos la tecla y modificamos la estructura movement en consecuencia.


if let Some(Button::Keyboard(key)) = e.press_args() {
if key == Key::W {
mov.up = true;
}
if key == Key::S {
mov.down = true;
}
if key == Key::A {
mov.left = true;
}
if key == Key::D {
mov.right = true;
}
};
if let Some(Button::Keyboard(key)) = e.release_args() {
if key == Key::W {
mov.up = false;
}
if key == Key::S {
mov.down = false;
}
if key == Key::A {
mov.left = false;
}
if key == Key::D {
mov.right = false;
}
};


Por último, si queremos comprobar clicks del ratón hacemos algo similar. He añadido código para que cambio el color del rectángulo si pulsamos sobre él.


if let Some(Button::Mouse(mb)) = e.release_args() {
if mb == MouseButton::Left {
let x = mc[0];
let y = mc[1];
if x > dims[0] && x < dims[0] + dims[2] { if y > dims[1] && y < dims[1] + dims[3] {
rect_color = if rect_color == [1.0,0.0,0.0,0.7]{
[0.0,1.0,0.0,0.7]
} else if rect_color == [0.0,1.0,0.0,0.7] {
[0.0,0.0,1.0,0.7]
} else{
[1.0,0.0,0.0,0.7]
}
}
}

}
}


A continuación un pequeño evento que guarda la última posición del ratón.


if let Some(mouse_cursor) = e.mouse_cursor_args() {
mc = mouse_cursor;
}
}
}


 

Y con esto ya tenemos hecho un ejemplo en Piston.



Si quieres tener un ejecutable para Windows sin que se muestre primero la consola debes compilar la versión que vas a distribuir con unos parámetros especiales. Si usas Rust con GCC usarás:
cargo rustc --release -- -Clink-args="-Wl,--subsystem,windows"

Si por el contrario usas Visual C++:
cargo rustc --release -- -Clink-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"

 

Piston todavía se encuentra en fuerte desarrollo, en la API estan documentados todos los métodos pero aun así muchas veces no se sabe como hacer ciertas cosas. Piston soporta además 3D, contando con una librería especializada en vóxels. Veremos como evoluciona esta librería.

Comentarios

nasciiboy
para programar con un enfoque de ejecucion eficiente que te parece mas molon c, go, rust o algun otro?
nasciiboy
como se compila esto en gnu? en mi sistema lanzo esta = note: /usr/bin/ld: unrecognized option '--subsystem' /usr/bin/ld: use the --help option for usage information collect2: error: ld returned 1 exit status
aarroyoc
Si estas en Linux no hace falta hacer nada, con <code>cargo build</code> se construye todo correctamente. Lo que he puesto es para Windows ya que en Windows las aplicaciones gráficas van en otro subsistema y hay que decírselo al linker (que puede ser GCC o MSVC).
aarroyoc
Interesante pregunta. Una ejecución eficiente suele implicar no tener recolector de basura, por lo que C#, Java, Nim, Go y D se van fuera. C++ y Rust son la respuesta pero cada uno tiene sus pros y sus contras. Ambos son lenguajes complejos, más de lo que me gustaría a mí, cada uno por sus propios méritos. Hay gente que en este debate han visto con buenos ojos el intento de sustituir a C++ por parte de Rust pero les ha aterrorizado la complejidad que añade en otros lugares. Jonathan Blow defiende esta postura y se encuentra desarrollando Jai, pero como todavía no tenemos compilador disponible todavía es pronto para juzgarlo.
nasciiboy
que es lo atractivo de rust? ha tardado bastante bajando y compilando dependencias, se parece a c++ con los incomodos let de lisp encima la carpeta del proyecto pesa la friolera de 68 MB tambien es necesaria la gestion de memoria a pelo como en C? digo, me gusta c, pero me gustaria que el comite añadiera tipos decentes al core del nucleo y no parches mediante librerias adicionales, el estandar dejase de ser poco especifico y se revisaran y añadieran nuevas librerias estandar, de momento estoy probando con go, parece un lenguje descente, pero hacer cast continuamente no es agradable, tampoco sus punteros de jugete.
nasciiboy
la compilacion termino con exito, pero la ejecucion falla, parace un problema con alguna libreria thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: DescriptorInit(PixelExport(0, None))', src/libcore/result.rs:799 note: Run with `RUST_BACKTRACE=1` for a backtrace. solo era una prueba de concepto, rust es muy raro...
aarroyoc
Debe de ser un error de la gráfica, a mi también me pasó. Tuve bastantes problemas con una gráfica integrada de Intel para hacer funcionar el código. Luego probé en un ordenador con gráfica AMD moderna y todo fue bien. Puedes cambiar la versión de OpenGL que se usa (desde 2.0 hasta 4.5 creo), así lo solucioné yo.
aarroyoc
Lo atractivo de Rust es precisamente que no hay gestión de memoria a pelo como en C o C++ sin sacrificar rendimiento por ello. Muchos lenguajes son seguros pero dependen de un recolector de basura o máquina virtual y los hace lentos e impredecibles. Rust mantiene el rendimiento de C pero gestionando la memoria de forma más segura. En Rust se te garantiza que NUNCA se cerrará el programa por un crasheo, salvo que hayas llamado manualmente a panic!, hayas hecho un unwrap sin seguridad o uses bloques de código unsafe. Por otro lado las dependencias en Rust sí que ocupan bastante porque por defecto todas se compilan con símbolos de depuración y pesa bastante. Pero si compilas en modo release no ocupa tanto. En realidad el código final de los ejecutables no lo genera Rust sino LLVM, igual que pasa en el compilador de C++ Clang. Otra de las ventajas de Rust es su concurrencia pero eso es algo más complejo para usarlo en muchos casos. Rust no es el lenguaje idóneo para prototipos pero es robusto para aplicaciones finales, el compilador de Rust es muy estricto y a la mínima no va a compilar si haces algo mal con la memoria.
nasciiboy
va, gracias por la explicacion. Seguire esperando el dia que los compiladores generen codigo eficiente, con una compilacion inmediata, que gestionen las dependencias automagicamente, eso, o esperar a un sucesor digno de C...

Añadir comentario

Todos los comentarios están sujetos a moderación