Articoli di programmazione

Un semplice videogame in javascript puro? Si può fare!

Prima parte: introduzione su come costruire un videogame utilizzando solo un canvas e la buona vecchia programmazione

photo-1562229125-6d6075419a22.jpeg
Già qualche anno fa mi ero appassionato con il videogame programming.
Avevo sviluppato in C# alcuni videogiochi, partendo da Snake, passando per Arkanoid, per arrivare fino a Tetris, Pac-Man e Space Invaders.
Mi ero costruito un framework partendo dai vettori, gli sprite e tutto il necessario per gestire la Collision Detection e la fisica del gioco.
Avevo quindi creato un gioco di biliardo, dove potevo regolare a mio piacimento l'attrito del tavolo, la massa delle palle e la forza applicata dal giocatore.
E' divertente.
E' appagante.
Si imparano tante cose e soprattutto ci si diverte.

Dopo qualche anno sono passato a Unity.
Lì è c'è già tutto il framework per fare ogni cosa: la fisica non devi scrivertela e la collision detection è talmente raffinata e semplice da impostare che sembra un sogno.
In Unity ho iniziato con lo sviluppare qualche gioco con le sfere, la gravità, le collisioni e le esplosioni: tanto per prendere confidenza.
Poi ho fatto un gioco di biliardo.
Poi una specie di Arkanoid 3D.
Un gioco 2D in cui un orco doveva prendere a mazzate le sfere che tentavano di ucciderlo.
Alla fine ho fatto anche un gioco di guerra: un simulatore di volo in cui con un caccia-bombardiere si devono distruggere i carri armati che ci sparano contro, mentre si plana su una città devastata dal conflitto.
Ero riuscito a farlo girare bene su Android, nonostante il numero cospicuo di attività eseguite per ogni frame: molti carri 3D ben dettagliati, che si accorgevano del passaggio del nostro aereo, ben dettagliato anch'esso, e provavano a sparargli cercando di indovinare la traiettoria: nei livelli più difficili erano diventati proverbiali cecchini e dovevi essere un manico per uscirne vivo!
Tutto molto bello. A un certo punto anche complicato, ma certamente il divertimento per chi ama questo tipo di attività, tocca vette altissime.

Poi il nulla.
Per qualche anno non ho più aperto un gioco se non per giocarci. Così tra qualche partita FIFA e l'immortale Starcraft, sono arrivato a 2 settimane fa, quando mi è salita la scimmia di rispolverare i vecchi videogames e renderli giocabili in HTML.
Così, sfruttando un semplice Canvas HTML e Javascript puro ho voluto creare nel tempo libero qualche gioco semplicissimo al quale pure mia nonna avrebbe potuto giocare.
Volevo tornare alle basi e fare solo qualche piccola e semplice schermata di gioco in 2D.

Dopo qualche chiacchiera inutile, partiamo a descrivere alcuni concetti chiave per poter implementare il nostro videogame.

Buongiorno vettori!
Ebbene, che si tratti giochi 2D o giochi 3D avrai a che fare con i cari vecchi vettori.
Ti serve una classe Vector, che riesca a memorizzare le informazioni del vettore, X e Y per quelli 2D come nel nostro esempio, e ti consenta di farci le operazioni necessario a muovere i tuoi oggetti sul canvas.
La classe Vector, oltre alle proprietà X e Y, contiene alcune funzioni che le permettono si normalizzare il vettore, di sommarlo, dividerlo, moltiplicarlo, etc.
Vediamo un estratto di un Vecor 2D:

var Vector = function (x, y) {
this.x = x || 0;
this.y = y || 0;
};
Vector.prototype.multiply = function (scalar) {
return new Vector(this.x * scalar, this.y * scalar);
};
Vector.prototype.multiplyBy = function (scalar) {
this.x *= scalar;
this.y *= scalar;
};
Vector.prototype.divide = function (scalar) {
return new Vector(this.x / scalar, this.y / scalar);
};
Vector.prototype.divideBy = function (scalar) {
this.x /= scalar;
this.y /= scalar;
};

Chiara, fresca, Sprite!
Una volta creata la classe Vector, si passa a creare la cellula costitutiva di ogni videogioco: lo Sprite.
Lo Sprite è una classe che memorizza le informazioni del singolo atomo visibile sullo schermo.
Nel caso più semplice lo Sprite è da solo il giocatore, il mattone o il nemico, oppure la palla.
Nel caso più complesso, più Sprite si combinano insieme per costruire elementi o personaggi articolati, che si muovono coordinati sullo schermo.
Uno sprite ha una determinata posizione rappresentata da un vettore.

Uno sprite con posizione x = 0, y = 0, è ad esempio fermo nel vertice in alto a sinistra del nostro schermo.
Lo sprite ha anche una certa velocità, espressa anch'essa con un vettore. Se la sua velocità è x = 1, y = 1, significa che in un determinato lasso di tempo la posizione diventerà x = 1, y = 1 e poi x = 2 e y = 2 e così via, finchè la velocità non cambierà.
Ecco quindi con 2 semplici vettori, che riusciamo a far muovere il nostro sprite sul video. Basta predisporre una funzione di rendering che chiameremo Draw, che gira ogni pochi millisecondi, che si occupi di aggiornare la posizione dello sprite in base alla sua velocità.
Lo sprite non ha solo questo: ha una sua massa, un suo attrito, un suo colore, una sua forma, ad esempio sfera o rettangolo, e moltissime altre proprietà che ci permettono di creare videogames completi.
Ad esempio lo sprite può avere un target: ovvero un altro sprite da tenere come bersaglio e la sua velocità potrà variare per tenersi ad una certa distanza da esso, oppure per avvicinarsi o allontanarsi.

Ora dobbiamo pensare a fare muovere lo sprite in base al nostro volere.
Quando premo il bottone "freccia a destra" voglio che lo sprite si muova a destra.
Aggancio un gestore di evento al KeyDown, verifico che il tasto premuto sia quello desiderato ed in tal caso, creo un vettore x = 1, y = 0: questa sarà la velocità del mio sprite per un certo lasso di tempo.
In verità ho 2 modi per far muovere il mio sprite:
- il primo consiste semplicemente nel far aggiornare il suo vettore posizione in base al nuovo vettore velocità tramite una semplice somma: la nuova posizione è la vecchia posizione più la velocità
- il secondo consiste nell'applicare una forza tramite il nuovo vettore che ho creato sulla pressione del bottone
Nel primo caso il movimento è istantaneo e prescinde dalla massa dello sprite.
Nel secondo caso il movimento risentirà di tutte le forza in gioco sullo sprite, dall'attrito a quella di gravità ed il movimento sarà decisamente più realistico, anche se meno istantaneo a causa dell'inerzia generata dalla massa dello sprite.
C'è da tenere presente che agendo più forze sullo sprite, la nostra spinta a destra può tradursi in un movimento diverso dallo spostamento a destra.
Lo sprite può per esempio muoversi a destra ma anche un pochino in basso in quanto agisce su di esso la gravità (se l'ho attivata e programmata). Se la forza applicata non sarà sufficiente, addirittura lo sprite non si muoverà.
Quale scegliere? Dipende da che tipo di gioco stiamo creando: su un arcade come pac-man sceglierei il primo, mentre su un simulatore o su un gioco di biliardo andrei sul secondo metodo.

Lo sprite deve disegnarsi.
Può caricare una bitmap e disegnarla sul canvas oppure più semplicemente disegnare un cerchio di un certo diametro, o magari un rettangolo con un certo lato.
Vediamo come disegnare sul canvas un semplice oggetto sprite; se il type contiene la parola "block" verrà disegnato come quadrato, altrimenti come cerchio:

this.context.beginPath();
if (this.type.indexOf("block") >= 0)
ctx.rect(this.position.x, this.position.y, this.size, this.size);
else
this.context.arc(this.position.x, this.position.y, this.size, 0, Math.PI * 2);
this.context.fillStyle = this.fill;
this.context.fill();
this.context.closePath();

E' molto pratico dare una proprietà Size allo sprite per questo, che tornerà molto utile anche per il collision detection: la bestia nera dei giochi fai da te!
Sì perchè accorgersi delle collisioni e soprattutto orchestrarne i risultati sui corpi in gioco non è affatto banale.
Va considerato che la funzione deve essere chiamata ogni render, su tutti gli oggetti sul video, quindi deve essere prestante oltre che corretta.
La base del ragionamento solitamente è: ho una collection di Sprite presenti sul video, in un loop controllo ciascuno sprite che posizione ha e la confronto con quella di tutti gli altri sprite per vedere se ce n'è uno che ha la posizione minore o uguale alla mia sommata della mia size.
Se ne trovo uno significa che questo sta collidendo con il mio sprite. Da lì si apre quindi il fronte del calcolo di ciò che deve accadere.
Prendo le velocità di entrambi, la loro massa e tramite qualche formula di fisica delle superiori mi ricavo i vettori risultanti dall'urto.

Oltre a questo il nostro Sprite dovrà controllare di non finire fuori dai bordi del canvas, e per questo sarà necessario scrivere una funzione apposita "CheckBoundaries" e potrebbe essere interessante definire delle propietà per stabilire se l'urto con i bordi farà rimbalzare l'oggetto oppure no.
Lo sprite può avere un Type che useremo cone identificativo e nei nostri Draw, valuteremo il da farsi per sprite di diverso type. Ad esempio possiamo inibire la collision detection tra certi tipi di sprite, per esempio perché non vogliamo che gli sprite delle nostre pallottole uccidano il nostro player.

In attesa degli altri articoli di questa serie, puoi guardare qualche semplicissimo videogame di esempio sviluppato con i concetti di questo articolo, in javascript e HTML.
E' sempre possibile usare la tastiera, mentre se sei su smartphone puoi usare la pressione del tuo dito sullo schermo per muovere il tuo giocatore (la palla andrà verso il punto premuto), oppure premere i bottoni colorati in basso allo schermo quando presenti:
Crazy Balls
Bally
Buntu
Balkanoid
Space intruders
Space hero
Super pengu

#vanillajs #videogame #programming #javascript #js