
En este artículo, compartiré mi experiencia personal en el desarrollo de un pequeño juego en Rust. La creación de la versión funcional tomó aproximadamente 24 horas (principalmente trabajé por las noches o los fines de semana). El juego aún está lejos de estar terminado, pero creo que la experiencia será útil. Les contaré lo que aprendí y algunas observaciones hechas durante la construcción del juego desde cero.
Skillbox recomienda: Curso práctico de dos años .
Recordamos: para todos los lectores de «Habr» — un descuento de 10,000 rublos al registrarse en cualquier curso de Skillbox usando el código promocional «Habr».
¿Por qué Rust?
Elegí este lenguaje porque escuché muchas cosas buenas sobre él y veo que se está volviendo cada vez más popular en el campo del desarrollo de juegos. Antes de escribir el juego, tenía algo de experiencia en el desarrollo de aplicaciones simples en Rust. Eso fue suficiente para sentir una cierta libertad al escribir el juego.
¿Por qué un juego y qué tipo de juego?
¡Crear juegos es divertido! Me gustaría que hubiera más razones, pero para proyectos «domésticos», elijo temas que no están demasiado relacionados con mi trabajo habitual. ¿Qué tipo de juego? Quería hacer algo así como un simulador de tenis, donde se combinan Cities Skylines, Zoo Tycoon, Prison Architect y, por supuesto, tenis. En general, se trata de un juego sobre una academia de tenis, a la que la gente viene a jugar.
Preparación técnica
Quería usar Rust, pero no sabía exactamente cuán «desde cero» necesitaba comenzar a trabajar. No quería escribir shaders de píxeles ni usar drag-and-drop, así que busqué las soluciones más flexibles.
Encontré recursos útiles que comparto con ustedes:
- — una lista de elementos necesarios para el desarrollo de juegos en Rust;
Investigé varios motores de juego en Rust, eligiendo al final Piston y ggez. Me había encontrado con ellos mientras trabajaba en un proyecto anterior. Finalmente elegí ggez, ya que me pareció más adecuado para implementar un pequeño juego 2D. La estructura modular de Piston es demasiado compleja para un desarrollador principiante (o alguien que trabaja con Rust por primera vez).
Estructura del juego
Pasé un tiempo reflexionando sobre la arquitectura del proyecto. El primer paso es crear la «tierra», las personas y las canchas de tenis. Las personas deben moverse por las canchas y esperar. Los jugadores deben tener habilidades que mejoren con el tiempo. Además, debe haber un editor que permita agregar nuevas personas y canchas, pero eso ya no será gratuito.
Después de pensar en todo, comencé a trabajar.
Creación de un juego
Inicio: círculos y abstracciones
Tomé un ejemplo de ggez y obtuve un círculo en la pantalla. ¡Asombroso! Ahora un poco de abstracciones. Me pareció que sería bueno abstraer la idea del objeto del juego. Cada objeto debe ser renderizado y actualizado, como se indica aquí:
// the game object trait
trait GameObject {
fn update(&mut self, _ctx: &mut Context) -> GameResult<()>;
fn draw(&mut self, ctx: &mut Context) -> GameResult<()>;
}
// a specific game object - Circle
struct Circle {
position: Point2,
}
impl Circle {
fn new(position: Point2) -> Circle {
Circle { position }
}
}
impl GameObject for Circle {
fn update(&mut self, _ctx: &mut Context) -> GameResult<()> {
Ok(())
}
fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
let circle =
graphics::Mesh::new_circle(ctx, graphics::DrawMode::Fill, self.position, 100.0, 2.0)?;
graphics::draw(ctx, &circle, na::Point2::new(0.0, 0.0), 0.0)?;
Ok(())
}
}Este fragmento de código me permitió obtener una excelente lista de objetos que puedo actualizar y renderizar en un ciclo igualmente excelente.
mpl event::EventHandler para MainState {
fn update(&mut self, context: &mut Context) -> GameResult {
// Actualiza todos los objetos
for object in self.objects.iter_mut() {
object.update(context)?;
}
Ok(())
}
fn draw(&mut self, context: &mut Context) -> GameResult {
graphics::clear(context);
// Dibuja todos los objetos
for object in self.objects.iter_mut() {
object.draw(context)?;
}
graphics::present(context);
Ok(())
}
} main.rs es necesario porque contiene todas las líneas de código. Pasé un tiempo separando archivos y optimizando la estructura de directorios. Así es como todo se ve después de esto:
resources -> aquí es donde están todos los activos (imágenes)
src
— entidades
— game_object.rs
— circle.rs
— main.rs -> bucle principal
Personas, canchas y imágenes
El siguiente paso es crear el objeto de juego Person y cargar imágenes. Todo debe construirse sobre azulejos de 32*32.

Canchas de tenis
Estudiando cómo lucen las canchas de tenis, decidí hacerlas con azulejos de 4*2. Inicialmente se podía hacer una imagen de ese tamaño o juntar 8 azulejos individuales. Pero luego entendí que solo necesitaba dos azulejos únicos, y aquí está la razón.
En total tenemos dos de esos azulejos: 1 y 2.
Cada sección de la cancha consta de un azulejo 1 o un azulejo 2. Pueden estar dispuestos de manera normal o estar volteados 180 grados.

Modo principal de construcción (ensamblaje)
Después de lograr renderizar las canchas, las personas y los mapas, supe que también necesitaba un modo básico de ensamblaje. Lo implementé así: cuando se presiona un botón, se selecciona un objeto y un clic lo coloca en el lugar correcto. Así, el botón 1 permite seleccionar la cancha y el botón 2 permite seleccionar al jugador.
Pero también es necesario recordar lo que significa 1 y 2, así que añadí un wireframe para que fuera claro cuál objeto estaba seleccionado. Así es como se ve.

Preguntas sobre arquitectura y refactorización
Ahora tengo varios objetos de juego: personas, canchas y pisos. Pero para que los wireframes funcionen, es necesario informar a cada entidad del objeto si los propios objetos están en modo de demostración o si solo se dibuja un marco. Esto no es muy conveniente.
Me pareció que era necesario replantear la arquitectura para que aparecieran algunas limitaciones:
- la existencia de una entidad que se muestra y actualiza a sí misma es un problema, ya que esta entidad no podrá 'saber' qué debe renderizar: una imagen y un wireframe;
- la falta de una herramienta para intercambiar propiedades y comportamientos entre entidades individuales (un ejemplo sería la propiedad is_build_mode o el comportamiento de renderizado). Se podría haber utilizado herencia, aunque en Rust no hay una forma adecuada de implementarlo. Lo que realmente necesitaba era composición;
- una herramienta para la interacción entre entidades era necesaria para asignar personas a las canchas;
- las propias entidades representaban una mezcla de datos y lógica, lo que rápidamente se volvía incontrolable.
Realicé un estudio adicional y descubrí la arquitectura , que se utiliza comúnmente en los juegos. Aquí están las ventajas de ECS:
- los datos están separados de la lógica;
- composición en lugar de herencia;
- una arquitectura orientada a datos.
ECS se caracteriza por tres conceptos básicos:
- entidades — tipo de objeto al que se refiere un identificador (esto puede ser un jugador, una pelota o algo más);
- componentes — de los cuales están formadas las entidades. Ejemplo: componente de renderizado, ubicación y otros. Son depósitos de datos;
- sistemas — utilizan tanto objetos como componentes, además contienen comportamiento y lógica que se basa en esos datos. Ejemplo: sistema de renderizado que recorre todas las entidades con componentes para el renderizado y se encarga de la reproducción.
Después de estudiar, quedó claro que ECS aborda los siguientes problemas:
- aplicar composición en lugar de herencia para la organización sistemática de entidades;
- eliminar el desorden del código a través de sistemas de gestión;
- utilizar métodos como is_build_mode para mantener la lógica del wireframe en un solo lugar: en el sistema de renderizado.
Así quedó después de implementar ECS.
resources -> aquí es donde están todos los activos (imágenes)
src
— components
— position.rs
— person.rs
— tennis_court.rs
— floor.rs
— wireframe.rs
— mouse_tracked.rs
— resources
— mouse.rs
— systems
— rendering.rs
— constants.rs
— utils.rs
— world_factory.rs -> funciones de la fábrica mundial
— main.rs -> bucle principal
Asignamos personas a las canchas
ECS hizo la vida más fácil. Ahora tenía un camino sistemático para agregar datos a las entidades y agregar lógica basada en esos datos. Esto, a su vez, permitió organizar la distribución de personas en las canchas.
Lo que hice:
- agregué datos sobre las canchas asignadas en Person;
- agregué datos sobre las personas distribuidas en TennisCourt;
- agregué CourtChoosingSystem, que permite analizar personas y lugares, descubrir canchas disponibles y distribuir jugadores en ellas;
- agregué el sistema PersonMovementSystem, que busca personas asignadas a las canchas y, si no están allí, envía a las personas donde se necesita.

Resumen
Disfruté mucho trabajar en este juego simple. Además, estoy contenta de que utilicé Rust para escribirlo, ya que:
- Rust te da lo que necesitas;
- tiene una excelente documentación, Rust es bastante elegante;
- la inmutabilidad es genial;
- no necesito recurrir a clonar, copiar o acciones similares, algo que solía hacer con C++;
- Options son muy convenientes de manejar, también manejan errores de manera excelente;
- si el proyecto se compila, en un 99% funcionará, y lo hará exactamente como debe. Las mensajes de error del compilador, creo que son los mejores que he visto.
El desarrollo de juegos en Rust apenas está comenzando. Pero ya hay una comunidad estable y bastante grande trabajando para abrir Rust a todos. Por lo tanto, veo el futuro del lenguaje con optimismo, esperando con ansias los resultados de nuestro trabajo conjunto.
Skillbox recomienda:
- Curso en línea .
- Curso práctico .
- Curso práctico de un año .
Fuente: habr.com
