Asteroids

Il Progetto
Asteroids è un videogioco 2D che ho sviluppato riprendendo il classico arcade Atari del 1979 omonimo.

Asteroids di Atari, Inc. — 1979
Ho svolto il progetto come parte integrante dell’esame di Fondamenti di Computer Graphics M, all’Alma Mater Studiorum, Università di Bologna. La traccia (il laboratorio 02 del corso) chiedeva di realizzare una demo di animazione digitale 2D interattiva in C/C++ con OpenGL, specificando esplicitamente che non sarebbero stati accettati elaborati limitati a banali modifiche del materiale fornito — così ho deciso di realizzare direttamente un piccolo gioco completo.
Tutto ciò che si vede a schermo è disegnato con primitive OpenGL — triangoli, linee e punti. Non ci sono texture né sprite: la navicella, gli asteroidi, le esplosioni e persino i caratteri del testo sono geometria, generata a runtime.

La schermata del menu
Struttura del Progetto
Il progetto è organizzato in moduli, ciascuno dei quali espone le proprie funzioni di init/update/draw:
02_main.cpp— entry point, funzioni di inizializzazione e game loop;commons.h— un unico punto in cui includere, in modo ordinato, tutte le librerie di sistema e personali;defs.h— costanti ed enumerativi globali;structs.h— strutture dati comuni;utils.h— macro e funzioni di utilità;- un modulo per ciascun elemento di gioco:
asteroids,bullets,colliders,explosions,figure,firetrail,input,spaceship,stars,text,ui.
Il rendering passa attraverso una semplice shader pass-through, caricata tramite la libreria ShaderMaker fornita dalla professoressa, con una proiezione glm::ortho su una finestra 1280x720. Finestra, callback di input e timer sono gestiti da GLUT.
Game Loop
Il game loop si basa sulla tecnica del delta timing, che permette di disaccoppiare la logica di gioco dagli FPS: ogni update è espresso in funzione della frazione di secondo trascorsa dal frame precedente, per cui il comportamento degli elementi di scena non cambia al variare del framerate.
static void update(int value)
{
// Wait until 16ms has elapsed since last frame
while (glutGet(GLUT_ELAPSED_TIME) - game.timeSinceStart <= 16);
// Delta time = time elapsed from last frame (in seconds)
game.deltaTime = (glutGet(GLUT_ELAPSED_TIME) - game.timeSinceStart) / 1000.0f;
// Clamp delta time so that it doesn't exceed ~60 fps
game.deltaTime = MIN(0.05f, game.deltaTime);
// Update elapsed time (for next frame)
game.timeSinceStart = glutGet(GLUT_ELAPSED_TIME);
// process input & update functions
glutTimerFunc(UPDATE_DELAY, update, 0);
}
Il gioco è inoltre suddiviso in stati — GAME_MENU, GAME_MENU_CONTROLS, GAME_RUNNING, GAME_PAUSED, GAME_STAGE_COMPLETED, GAME_NEXT_STAGE_STARTING, GAME_OVER — sia per navigare tra le varie schermate, sia per aggiornare solo i moduli effettivamente necessari in un dato momento.
Input
GLUT consegna gli eventi da tastiera uno alla volta, il che non è comodo quando serve sapere se un tasto è tenuto premuto. Ho quindi scritto una piccola libreria di input (input.h) che mantiene aggiornata una struttura Input tramite glutKeyboardFunc() e glutKeyboardUpFunc(): alla pressione di un tasto la chiave relativa viene messa a 1, al rilascio torna a 0. Ogni modulo può così limitarsi a controllare i tasti che gli interessano, ottenendo un input continuo e in tempo reale.
void inputSpaceship()
{
if (input.keyboard.keys['w'])
{
// move forward
}
}
Gameplay
Si controlla una navicella spaziale in mezzo a un campo di asteroidi, e l’obiettivo è distruggerli tutti senza perdere le tre vite a disposizione. Gli asteroidi hanno tre dimensioni: quando uno viene colpito da un proiettile si divide in due più piccoli, a meno che non sia già di tipo piccolo. Completare una stage porta a quella successiva, con più asteroidi e quindi più caos; la partita termina quando si esauriscono le vite.
Ogni asteroide distrutto assegna un punteggio che dipende dalla sua grandezza — i più piccoli valgono di più, dato che sono più difficili da colpire. All’inizio di ogni stage, e ogni volta che la navicella riappare dopo essere stata distrutta, si ottiene uno scudo che garantisce 3 secondi di invulnerabilità.
I comandi sono consultabili dal gioco (tasto H nel menu), e alcuni esistono solo per mostrare il rendering:
| Tasto | Azione |
|---|---|
W | muove la navicella in avanti |
A/D | ruota in senso antiorario/orario |
Space | spara un proiettile |
C | mostra/nasconde i collider |
L | alterna il rendering a triangoli e quello a linee |
B | rende grande/piccola la navicella (per vederne i dettagli) |
O | apre/chiude l’oblò della navicella |
P | mette in pausa |
Esc | torna al menu |


Grafica
Sfondo
Lo sfondo è una base nera — il vuoto dello spazio — su cui vengono disegnate 300 stelle, raggruppate in tre insiemi per dimensione e lontananza: 50 vicine, 100 intermedie e 150 lontane.
Navicella
La navicella è di gran lunga l’oggetto più complesso della scena, ed è composta da un bel po’ di primitive:
- punta: un triangolo;
- pinne laterali: un rombo ciascuna (due triangoli);
- pinna centrale: una linea;
- scafo: le curve dei lati sono disegnate tramite due funzioni periodiche (coseno), poi riempite con triangoli;
- propulsore: due trapezi isosceli di colori leggermente diversi, per rendere l’effetto della lucentezza del metallo;
- cabina: un cerchio pieno, che ospita l’astronauta;
- astronauta: visiera (due semicerchi e un rettangolo) e tuta (un semicerchio e un quadrato) — l’astronauta resta sempre dritto, perché la matrice di modellazione delle sue figure viene ruotata in senso opposto a quella della navicella;
- oblò: un bordo metallico (una corona) e il vetro, un cerchio pieno trasparente reso con
GL_BLENDe il canale alpha.


Asteroidi, Scudo, Proiettili e Testo
Gli asteroidi hanno tre forme diverse, i cui vertici ho inserito manualmente, prendendo spunto per sagome e colori da un’immagine di riferimento. Lo scudo è un cerchio trasparente che copre l’intera navicella, mentre un proiettile non è altro che un singolo punto rosso di dimensione 10. Le vite riutilizzano lo sfondo della cabina e la grafica dell’astronauta.


Un discorso a parte merita il testo: dato che non ci sono texture né font caricati, ho scritto una piccola libreria (text.h) che costruisce delle scritte a partire da un buffer di caratteri, usando punti e linee. I vertici di ogni carattere li ho scritti a mano, ed è il motivo per cui il font ha quell’aspetto — con delle curve di Bézier il risultato sarebbe stato migliore, ma sarebbe stata una tana del bianconiglio per un altro laboratorio. I caratteri non implementati vengono sostituiti da un hyphen.

Il font fatto a mano
Fisica e Collisioni
Tutti gli elementi del gameplay vengono wrappati all’interno della scena: quando un oggetto esce da un lato della viewport, ricompare sul lato opposto.
Le stelle sfruttano l’effetto parallasse: al muoversi della navicella, quelle vicine (grandi) si spostano più velocemente di quelle lontane (piccole), il che rende la scena molto più profonda di quanto in realtà sia.
Il movimento della navicella è vincolato dal propulsore: può solo andare avanti o ruotare, e sia la velocità lineare che quella angolare decelerano quando il giocatore rilascia i tasti, dando alla navicella la sua inerzia.
void updateSpaceship(float deltaTime)
{
if (spaceship.angularSpeed != 0.0f)
{
spaceship.heading += spaceship.angularSpeed * deltaTime;
if (spaceship.heading > 2 * PI)
spaceship.heading -= 2 * PI;
if (spaceship.heading < 0.0f)
spaceship.heading += 2 * PI;
}
if (spaceship.forwardSpeed != 0.0f)
{
spaceship.pos.x += cos(spaceship.heading) * spaceship.forwardSpeed * deltaTime;
spaceship.pos.y += sin(spaceship.heading) * spaceship.forwardSpeed * deltaTime;
// [...]
}
}
Gli asteroidi si muovono di moto rettilineo uniforme, con direzione e verso casuali; i proiettili fanno lo stesso, ma assumono la direzione della navicella nel momento in cui vengono sparati.
Le collisioni sono gestite con dei collider circolari: ogni oggetto che può collidere (navicella, asteroidi, proiettili) contiene un CircleCollider con centro e raggio, e due oggetti collidono quando la distanza tra i centri è minore della somma dei raggi. È il test più economico possibile e, per forme così tondeggianti, è più che sufficiente.
bool isCollidingCircle(CircleCollider collider1, CircleCollider collider2)
{
return distance(collider1.pos, collider2.pos) < collider1.radius + collider2.radius;
}

Collider visibili (tasto 'C')
Animazioni
Sistemi Particellari
La scia di fuoco viene generata quando la navicella accelera: le particelle vengono create in punti casuali all’interno di un’area circolare posta sotto il propulsore, e solo se la velocità supera una certa soglia, in numero dipendente dalla velocità corrente e dalle dimensioni della navicella.
// Spawn firetrail only if the speed is greater than a threshold
if (spaceship.forwardSpeed > 10.0f)
{
float xSpawnCenter, ySpawnCenter;
xSpawnCenter = spaceship.pos.x +
(spaceship.radius + spaceship.scale * 1.25f) * cos(spaceship.heading + PI);
ySpawnCenter = spaceship.pos.y +
(spaceship.radius + spaceship.scale * 1.25f) * sin(spaceship.heading + PI);
spawnFiretrailParticles(
{ xSpawnCenter, ySpawnCenter, 0.0f },
spaceship.forwardSpeed,
spaceship.heading - PI,
20.0f * (spaceship.forwardSpeed / SPACESHIP_MAX_FORWARD_SPEED) *
(spaceship.radius / spaceship.originalRadius),
5.0f
);
}
Le esplosioni seguono la stessa idea: quando la navicella priva di scudo viene colpita da un asteroide, esplode generando particelle dai colori di detriti, fuoco e fumo, mentre un asteroide distrutto emette particelle dai colori simili ai propri.



Esplosione di un asteroide
Testo e UI
Infine, un paio di animazioni rendono l’interfaccia meno statica: il testo lampeggiante, usato nel menu e nella schermata di game over per indicare quale tasto premere, e il testo che incrementa/decrementa, usato per il countdown dello scudo e per far salire il punteggio a fine stage.
Build ed Esecuzione
Le soluzioni dei laboratori sono progetti Visual Studio: la cartella 02es contiene l’intero gioco, e la guida al setup nella repository spiega come configurare le dipendenze OpenGL. Nelle release è disponibile una build precompilata per Windows x64, insieme a quelle degli altri laboratori.
Il gioco è uno dei sette laboratori del corso, che coprono anche curve di Bézier, mesh shading e lighting, raytracing, e modellazione e digital art in Blender: si trovano tutti, insieme ai relativi report, nella stessa repository.