
In questo articolo condividerò la mia esperienza nello sviluppo di un piccolo gioco su Rust. Ci sono volute circa 24 ore per creare una versione funzionante (principalmente ho lavorato la sera o nel fine settimana). Il gioco è ancora lontano dal completamento, ma credo che l'esperienza sarà utile. Condividerò ciò che ho imparato e alcune osservazioni fatte durante la costruzione del gioco da zero.
Skillbox consiglia: Corso pratico di due anni .
Ricordiamo: per tutti i lettori di «Habr» — sconto di 10.000 rubli per l'iscrizione a qualsiasi corso Skillbox con il codice promozionale «Habr».
Perché Rust?
Ho scelto questo linguaggio perché ho sentito molte cose positive al riguardo e vedo che diventa sempre più popolare nel settore dello sviluppo di giochi. Prima di scrivere il gioco, avevo una leggera esperienza nello sviluppo di semplici applicazioni su Rust. Questo era sufficiente per sentire una certa libertà durante la scrittura del gioco.
Perché proprio un gioco e che gioco?
Creare giochi è divertente! Vorrei che ci fossero più motivi, ma per i progetti «domestici» scelgo temi che non sono troppo strettamente legati al mio lavoro abituale. Che gioco? Volevo creare qualcosa simile a un simulatore di tennis, dove si combinano Cities Skylines, Zoo Tycoon, Prison Architect e, appunto, il tennis. In sostanza, è venuto fuori un gioco su un'accademia di tennis, dove le persone possono giocare.
Preparazione tecnica
Volevo utilizzare Rust, ma non sapevo esattamente da quanto «zero» sarebbe stato necessario iniziare a lavorare. Non volevo scrivere shader pixel e utilizzare drag-n-drop, quindi cercavo le soluzioni più flessibili.
Ho trovato delle risorse utili, che condividerò con voi:
- — un elenco di elementi necessari per lo sviluppo di giochi in Rust;
Ho esaminato diversi motori di gioco Rust, scegliendo infine Piston e ggez. Li avevo già incontrati lavorando al mio progetto precedente. Alla fine ho scelto ggez, poiché sembrava più adatto per realizzare un piccolo gioco 2D. La struttura modulare di Piston è troppo complessa per uno sviluppatore alle prime armi (o per chi lavora su Rust per la prima volta).
Struttura del gioco
Ho dedicato un po' di tempo a riflettere sull'architettura del progetto. Il primo passo è realizzare la «terra», le persone e i campi da tennis. Le persone devono muoversi sui campi e aspettare. I giocatori devono avere abilità che si perfezionano col tempo. Inoltre, deve esserci un editor che consenta di aggiungere nuove persone e campi, ma questo non è gratuito.
Dopo aver pensato a tutto, ho iniziato a lavorare.
Creazione di un gioco
Inizio: cerchi e astrazioni
Ho preso un esempio da ggez e ho ottenuto un cerchio sullo schermo. Straordinario! Ora un po' di astrazioni. Ho pensato che sarebbe utile astrarre l'idea di oggetto di gioco. Ogni oggetto deve essere renderizzato e aggiornato come indicato qui:
// 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(())
}
}Questo pezzo di codice mi ha permesso di ottenere un'ottima lista di oggetti che posso aggiornare e rendere in un ciclo altrettanto ottimo.
mpl event::EventHandler per MainState {
fn update(&mut self, context: &mut Context) -> GameResult {
// Aggiorna tutti gli oggetti
for object in self.objects.iter_mut() {
object.update(context)?;
}
Ok(())
}
fn draw(&mut self, context: &mut Context) -> GameResult {
graphics::clear(context);
// Disegna tutti gli oggetti
for object in self.objects.iter_mut() {
object.draw(context)?;
}
graphics::present(context);
Ok(())
}
} main.rs è necessario perché contiene tutte le righe di codice. Ho dedicato un po' di tempo a separare i file e ottimizzare la struttura delle directory. Ecco come è diventato dopo:
resources -> qui ci sono tutte le risorse (immagini)
src
— entità
— game_object.rs
— circle.rs
— main.rs -> ciclo principale
Persone, campi e immagini
La fase successiva è creare l'oggetto di gioco Person e caricare le immagini. Tutto deve essere costruito su base di mattonelle delle dimensioni 32*32.

Campi da tennis
Dopo aver studiato com'erano fatti i campi da tennis, ho deciso di farli con mattonelle 4*2. Inizialmente si poteva creare un'immagine di tale dimensione oppure comporre insieme 8 mattonelle separate. Ma poi ho capito che servivano solo due mattonelle uniche, ecco perché.
In totale abbiamo due mattonelle: 1 e 2.
Ogni sezione del campo consiste di un mattonella 1 o mattonella 2. Possono essere disposte come al solito o essere capovolte di 180 gradi.

Modalità principale di costruzione (assemblaggio)
Dopo aver reso visibili i campi, le persone e le mappe, ho capito che era necessaria anche una modalità di assemblaggio di base. L'ho implementata così: quando il pulsante è premuto, l'oggetto è selezionato e il clic lo posiziona nel luogo desiderato. Così, il pulsante 1 permette di selezionare il campo, mentre il pulsante 2 consente di selezionare un giocatore.
Ma è anche necessario ricordare cosa significano 1 e 2, quindi ho aggiunto un wireframe per far capire quale oggetto è selezionato. Ecco come appare.

Domande sull'architettura e il refactoring
Ora ho diversi oggetti di gioco: persone, campi e piani. Ma affinché i wireframe funzionino, è necessario comunicare a ciascuna entità dell'oggetto se gli oggetti stessi sono in modalità dimostrativa o se è semplicemente disegnata una cornice. Non è molto comodo.
Ho pensato che fosse necessario ripensare l'architettura in modo che emergessero alcune limitazioni:
- la presenza di un'entità che si visualizza e si aggiorna da sola è un problema, poiché quest'entità non sarà in grado di 'sapere' cosa deve renderizzare: l'immagine e il wireframe;
- la mancanza di uno strumento per lo scambio di proprietà e comportamenti tra entità separate (ad esempio, la proprietà is_build_mode o il rendering del comportamento). Si potrebbe utilizzare l'ereditarietà, anche se in Rust non c'è un modo normale per realizzarla. Ciò di cui avevo davvero bisogno era la composizione;
- uno strumento per interagire tra le entità era necessario per assegnare persone ai campi;
- le stesse entità rappresentavano una miscela di dati e logica, il che usciva rapidamente fuori controllo.
Ho condotto un'ulteriore ricerca e ho scoperto l'architettura , che è comunemente usata nei giochi. Ecco i vantaggi dell'ECS:
- i dati sono separati dalla logica;
- composizione invece di ereditarietà;
- architettura orientata ai dati.
Per l'ECS ci sono tre concetti di base:
- entità — tipo di oggetto a cui si riferisce un identificatore (questo può essere un giocatore, una pallina o altro);
- componenti — da cui sono composte le entità. Esempio — componente di rendering, posizione e altri. Questi sono contenitori di dati;
- sistemi — utilizzano sia oggetti che componenti, più contengono comportamento e logica basati su questi dati. Esempio — sistema di rendering che esamina tutte le entità con componenti per il rendering e si occupa del disegno.
Dopo lo studio è diventato chiaro che l'ECS risolve i seguenti problemi:
- applicazione della composizione invece dell'ereditarietà per l'organizzazione sistemica delle entità;
- eliminazione del disordine del codice attraverso sistemi di gestione;
- uso di metodi come is_build_mode per mantenere la logica del wireframe nello stesso luogo — nel sistema di rendering.
Ecco cosa è risultato dopo l'implementazione dell'ECS.
resources -> qui ci sono tutte le risorse (immagini)
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 -> funzioni della fabbrica mondiale
— main.rs -> ciclo principale
Assegniamo persone ai campi
ECS ha semplificato la vita. Adesso avevo un metodo sistematico per aggiungere dati alle entità e integrare logica basata su questi dati. Questo, a sua volta, ha permesso di organizzare la distribuzione delle persone sui campi.
Cosa ho fatto:
- ho aggiunto dati sui campi assegnati a Persone;
- ho aggiunto dati sulle persone distribuite a TennisCourt;
- ho aggiunto CourtChoosingSystem, che consente di analizzare le persone e i campi, scoprire i campi disponibili e distribuire i giocatori su di essi;
- ho aggiunto il sistema PersonMovementSystem, che cerca le persone assegnate ai campi e, se non sono lì, le invia dove necessario.

Riepiloghiamo
Mi è piaciuto molto lavorare a questo semplice gioco. Inoltre, sono soddisfatta di aver utilizzato Rust per scriverlo, perché:
- Rust ti offre ciò di cui hai bisogno;
- ha una documentazione eccellente, Rust è molto elegante;
- la costanza è fantastica;
- non è necessario ricorrere a clonazioni, copie o azioni simili, cosa che facevo spesso in C++;
- Options è molto comodo da usare, gestisce anche bene gli errori;
- se il progetto è riuscito a compilarsi, allora nel 99% dei casi funziona esattamente come dovrebbe. I messaggi di errore del compilatore sono, a mio avviso, i migliori che abbia mai visto.
Lo sviluppo di giochi in Rust è solo agli inizi. Ma c'è già una comunità stabile e piuttosto ampia che lavora per rendere Rust accessibile a tutti. Quindi guardo al futuro del linguaggio con ottimismo, ansiosa di vedere i risultati del nostro lavoro comune.
Skillbox consiglia:
- Corso online .
- Corso pratico .
- Corso pratico annuale .
Fonte: habr.com
