Mentre Web e marketplace si riempiono di videogiochi di ogni foggia: nuovi giochi, cloni di vecchi classici, cloni dei nuovi migliorati (o peggiorati) e si fanno tentativi più o meno riusciti di trovare il gioco preferito dalla gente, molti sviluppatori si chiedono come meglio investire il proprio tempo per creare questi prodotti.
Dal mio modesto punto di vista iniziare creando browser-game è un buon punto di partenza. Perché?
- Perché anzitutto abbiamo a disposizione un'immensità di materiale su JavaScript
- C'è una vasta letteratura anche su HTML5 Canvas, WebGL, SVG etc.
- Ci sono numerosissimi Framework che ci permettono di sfruttare senza troppa fatica le tecnologie che abbiamo appena citato.
- E soprattutto ci sono i browser moderni con tutto il necessario per lavorare, testare, provare, debuggare etc. Un esempio per tutti Internet Explorer 11.
Una volta realizzato, il nostro browser game (o HTML5 game che dir si voglia) potrà essere un buon biglietto da visita e attirare visite dei giocatori sul nostro sito. Inoltre abbiamo un trampolino pronto per creare app native per tutte le piattaforme, utilizzando le stesse tecniche... ma questa è un'altra storia.
In questo articolo esaminiamo alcuni punti chiave utili per creare giochi e lo facciamo prendendo spunto da questo articolo di Kai Jäger.
1. Iniziare sempre dalle basi...
Nel primo punto Kai suggerisce subito l'utilizzo di un framework e il suggerimento è da considerarsi assolutamente valido, tuttavia secondo me vale la pena spendere un po' di tempo per apprendere o consolidare le basi di JavaScript, soprattutto la gestione degli oggetti, e l'API di HTML5 Canvas.
Con questo non intendo dire che bisogna studiare valanghe di libri, ma almeno passare un paio d'ore a farsi un'idea su come affrontare alcuni aspetti:
- creare una pagina HTML minima che ospiti il nostro gioco
- disegnare gli oggetti su una scena, basta anche mettere su una semplice forma come un rectFill o caricare una png
- rappresentare gli oggetti, e qui possiamo sfruttare le proprietà di JavaScript
- gestire il loop principale del gioco, per questo possiamo anche sfruttare il classico setInterval.
- gestire l'input dell'utente, per questo basta cercare un po' di documentazione su eventi come keypressed, sulla gestione del mouse o sui pointer events per il touch.
Per capirci ecco un breve esempio (non completo) di come si può iniziare a lavorare a un clone di PONG.
Un progetto da zero... a zero punto uno
Mettiamo su un file HTML con il minimo necessario per il rendering del canvas:
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script type="text/javascript" src="game.js"></script>
<style>
html,
body
{
margin: 0px; padding: 0px;
background-color: black;
overflow: hidden;
cursor: none;
color:#fff; /* da cancellare */
}
canvas
{
position: absolute;
width: 100%; height: 100%;
top: 0%; left: 0%;
}
</style>
<title>Canvas Game</title>
</head><body>
<canvas id="gamecanvas" />
</body></html>
Nel file game.js abbiamo tutta la logica del gioco, vediamone alcune parti.
Oggetti sullo schermo
Ecco una possibile rappresentazione delle barrette (qui c'è quella sinistra) e della pallina:
var leftbar = {
speed: 5, // velocità in px/frame
x: 0,
y: 0,
width: 10,
height: 20,
draw: function(ctx) {
// disegnamo la barra sinistra
ctx.fillStyle = "#fff";
ctx.fillRect(this.x, this.y, this.width , this.height);
}
};
var ball = {
speed: 5,
x:0, y:0,
radius:5,
// direzione, inizialmente casuale
angle: Math.random() * Math.PI * 2,
draw: function(ctx) {
// disegnamo la barra sinistra
ctx.fillStyle = "#fff";
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, false);
ctx.closePath();
ctx.fill();
},
move: function(ctx) {
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
}
};
Ecco invece come poter sbrigativamente effettuare qualche definizione inizale, il loop e le funzioni di rendering.
// inizializzazione
document.addEventListener("DOMContentLoaded", function() {
console.log("Documento caricato!"); // debugmsg
// Recupero del contesto 2D dell'elemento canvas
var canvas = document.getElementById("gamecanvas"),
ctx = canvas.getContext("2d");
console.log("contesto 2d acquisito: " + ctx);
// altezza e larghezza della finestra
var W = window.innerWidth,
H = window.innerHeight;
canvas.width = W;
canvas.height = H;
// posizionamento delle barrette
leftbar.x = Math.ceil(W/100*2);
rightbar.x = Math.ceil(W/100*98);
leftbar.y = rightbar.y = Math.ceil(H/2 - leftbar.height/2);
leftbar.width = rightbar.width = Math.ceil(W/100*1); // barretta larga il 2% dello schermo
leftbar.height = rightbar.height = Math.ceil(H/100*15); // barretta alta il 10% dello schermo
// posizionamento iniziale della palla
ball.x = Math.ceil(W/2);
ball.y = Math.ceil(H/2);
ball.radius = Math.ceil(W/100*1);
function render() {
// puliamo lo sfondo
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, W, H);
console.log("puliamo lo sfondo");
// disegnamo gli oggetti
leftbar.draw(ctx);
rightbar.draw(ctx);
// controlla le collisioni con i bordi
if ((ball.y-ball.radius < 0) || (ball.y+ball.radius > H))
ball.angle = 2*Math.PI - ball.angle;
// aggiorna la posizione della palla
ball.move(ctx);
// if (ball.collide(leftbar)) console.log('collisione con leftbar');
// if (ball.collide(rightbar)) console.log('collisione con rightbar');
ball.draw(ctx);
}
// primo disegno della scena
render();
// elaborazione degli eventi della tastiera
window.onkeydown = function(event) {
switch (event.which) {
case 38:
leftbar.y -= leftbar.speed;
break;
case 40:
leftbar.y += leftbar.speed;
break;
default:
return; // evita di restituire false
}
return false;
};
// impostazione del loop
var then = Date.now(); // timestamp iniziale
setInterval(mainLoop, 50);
// la scelta dei 50 ms è per non intasare troppo il processore nel test
// si può anche mettere 1ms
function mainLoop()
{
// timestamp in ms (tempo passato da1l' 1/1/1970 alle 00:00)
var now = Date.now();
// differenza tra il momento attuale e il ciclo appena precedente
var delta = now - then;
render();
then = now;
}
});
Si può fare molto meglio, ma è un esercizio utile per prendere contatto con l'ambiente e i pattern basilari utilizzati da tutti i framework: loop, rendering della scena, controlli ed eventi, componenti.
2 ... ma poi scegliere un framework!
Oltre a questo come ricorda anche Kai però, bisognerebbe lavorare su:
- precaricamenti delle immagini e dei suoni, perché risultino sincronizzati
- gestione degli sprite
- gestione della velocità di esecuzione
- etc.
Fortunatamente abbiamo numerosi Framework che faranno questo lavoro per noi. Ecco subito un elenco di progetti che semplificano la vita ai game developers:
Framework | Descrizione |
---|---|
Create JS | Suite gratuita e completa, supportata da Microsoft e Adobe e ottima anche per chi viene dal mondo Flash/Actionscript |
Construct 2 | Disponibilie anche in versione gratuita, offre anche la possibilità di esportare progetti per diverse piattaforme come Windows Store |
LimeJS | Anche questo semplice e gratuito |
Impact JS | È quello suggerito anche da Kai, molto valido ma a pagamento |
CraftyJS | Gratis, consente di lavorare sia su Canvas sia su DOM |
MelonJS | Leggero e opensource |
Three JS | Ultima ma non ultima va segnalata questa libreria per realizzare giochi 3D che sfruttino le WebGL, ormai largamente supportate |
Quelli elencati sono solo i framework più noti, ma ne escono sempre di nuovi e anche quando abbiamo fatto la nostra scelta, meglio tenersi un po' di tempo per continuare a sperimentare.
Jager caldeggia l'utilizzo di ImpactJS perché è molto completo e la spesa di 99$ per la licenza è certamente un buon affare. In ogni caso è bene provare diversi progetti per trovare quello che meglio si adatta alle nostre esigenze. Considerando giustamente che ciascuno adotta una sua logica, anche per la programmazione.
Vediamo uno spezzone di codice in ImpactJS:
ig.module(
'monster'
)
.requires(
'impact.game',
)
.defines(function(){
Monster = ig.Entity.extend({
eyes: 42
});
});
Con questo snippet, Kai Jager mostra come ImpactJS non solo fornisca un'astrazione per il rendering grafico e l'audio, ma introduca un particolare object model per la creazione degli oggetti e la gestione dell'ereditarietà.
Infine per le app HTML5 c'è anche l'abitudine di utilizzare librerie "scorciatoia" come jQuery o Zepto. Ottimi framework e molto utili anche, ma valutate bene se e quando utilizzarli. Scoprirete che forse non sempre ne avete bisogno. Vi suggerisco di leggere questo articolo in proposito.
3. Considerare le versioni per schermi piccoli e touch
Ormai siamo abituati a sentir parlare di contenuti adattabili ad una molteplicità di dimensioni, per il Web si parla di siti responsive ad esempio. Ebbene anche i nostri giochi dovrebbero essere adattabili per poter essere giocati anche su smartphone e tablet.
Il tentativo di essere compliant con diversi schermi introduce una difficoltà perché le differenze di dimensioni possono essere anche molto grandi, in più cambia l'aspect-ratio.
Jager indica come soluzione quella di assicurarci che i giochi supportino risoluzioni multiple oppure che l'area di gioco non ecceda gli 800x480 pixel (WVGA).
Inoltre bisogna stare attenti agli zoom previsti dai sistemi mobile. Possiamo disattivare questi comportamenti impostanto il meta tag viewport. Ecco un esempio in cui indichiamo alla viewport del gioco di occupare tutta la larghezza disponibile:
<meta name="Viewport" content="width=device-width; user-scaleable=no; initial-scale=1.0" />
Impostando il parametro user-scaleable
a no
indichiamo al browser mobile di disabilitare la gesture dello zoom (pinch-zoom), che oltretutto potrebbe andare in conflitto con con i controlli touch del gioco stesso.
Possiamo anche fare delle valutazioni sull'orientamento dello schermo e implementare delle visualizzazioni alternative sfruttando gli eventi di DeviceOrientation supportati anche da Internet Explorer 11.
Fatto questo dobbiamo pensare all'interfaccia utente: le tastiere virtuali ad esempio tendono ad occupare molto spazio sullo schermo dei device e potrebbe rivelarsi necessaria la creazione di una mini-tastiera o un controller per soddisfare le necessità di input del gioco (pensiamo ai tasti con le frecce ad esempio).
Meglio ancora potrebbe essere introdurre modalità di input alternative, basate sul tocco. Kai cita l'esempio di Spy Chase, in cui si può guidare semplcemente con un dito.
4. Tenersi stretti gli utenti: salvare i progessi del giocatore
In giro c'è una moltitudine di giochi e in qualche maniera dobbiamo pensare a coccolare i nostri utenti, per evitare di annoiarli e dirottarli involontariamente verso un'altro gioco. Per questo bisogna avere alcune accortezze, ad esempio non fare giochi troppo complessi per non scoraggiare, ma nemmeno troppo semplici per mantenere alta l'attenzione sulla sfida.
Una delle cose più odiose è quella di costringere il giocatore a ricominciare da capo solo perché ha accidentalmente cambiato pagina o perché la batteria del device è a terra. Inoltre un gioco che si ricorda di noi quando lo riprendiamo e che conserva i nostri progressi (e magari ci aiuta a condividerli sui social) mantiene certamente il suo appeal nel tempo.
Salvare i progressi del giocatore è dunque importantissimo. Kai Jager suggerisce di effettuare salvataggi automatici con regolarità e ci mostra alcune possibilità per farlo.
Le strade più classiche sono quella di scrivere su server o su un servizio cloud i risultati ottenuti, il ché richiede un minimo di registrazione perché l'utente sia riconoscibile, ma soprattutto ci costringe a continue richieste HTTP verso lo storage.
Bisogna anche considerare una massima importantissima: non sempre i dispositivi sono connessi con continuità ed è noioso non poter continuare a giocare perché l'applicazione non riesce più a comunicare col server.
C'è l'opzione di impostare un cookie, ma c'è una discreta possibilità di perdere i dati, inoltre possiamo memorizzare poche informazioni.
HTML5 offre una serie di opzioni in più per memorizzare i dati come lo standard Web Storage, chiamato anche Dom Storage o Local Storage. L'implementazione prevede un oggetto localStorage che permette il salvataggio dei dati direttamente su file system con una capacità di diversi megabytes per ogni sito.
Nel file i dati vengono organizzati come coppie chiave-valore, il ché in teoria ci impedirebbe di salvare gli oggetti complessi dei nostri videogame. Il trucchetto che utilizza Kai è quello di serializzare gli oggetti in JSON e di salvare ciò che vogliamo.
Ecco due semplici funzioni che possono aiutare a salvare e a recuperare lo stato dei nostri giochi:
function saveState(state) {
window.localStorage.setItem("gameState", JSON.stringify(state));
}
function restoreState() {
var state = window.localStorage.getItem("gameState");
if (state) {
return JSON.parse(state);
} else {
return null;
}
}
Attenzione! Con Internet Explorer 9-10-11 non funziona localStorage in locale (scusate il giro di parole), ovvero se il sito non sta girando su un web server. In alternativa possiamo utilizzare l'oggetto Storage, (window.Storage
), che espone gli stessi metodi getItem
e setItem
.
5. Utilizzare gli strumenti di sviluppo del Browser, ottimizzare con un profiler.
La prima cosa che va detta è che possiamo sviluppare HTML5 utilizzando veramente pochi strumenti, anche se molto potenti. Se ci pensate potremmo portare i nostri progetti dentro una chiavetta USB che contenga:
- Un editor di testi (suggerisco Notepad++ o SublimeText) in versione portable
- La cartella del nostro progetto (con librerie e framework annessi)
Tutto il resto è browser, tanto per parafrasare una vecchia canzone. Infatti i browser moderni come IE11 ci consentono di effettuare il debug step-by-step del codice, verificare i tempi di caricamento delle risorse, e profilare le nostre applicazioni per vedere dove è possibile snellirle per rendere i nostri giochi più fluidi.
Come ricorda anche Kai, la sfida più importante è quella di verificare il mantenimento di un frame-rate alto mano mano che aggiungiamo nuove features. I più moderni JavaScript engine supportano la gestione multi-core e l'accelerazione hardware (è il caso di Internet Explorer, ad esempio), ciò garantisce una piattaforma già performante... ma non basta, bisogna farne buon uso.
Occorre sempre verificare la presenza di colli di bottiglia delle nostre applicazioni, anche se all'apparenza sono fluide, dobbiamo pensare che potrebbero essere eseguite su sistemi meno performanti o su piattaforme diverse dalla nostra.
Per ottenere un gioco che lavori a 60 fps, ricorda il buon Kai, abbiamo non più di 16 millisecondi per effettuare il rendering di ciascun frame, all'incirca 6 frame per ogni battito di ciglia. Ma non bisogna farsi spaventare, anche se per alcuni giochi complessi l'impresa può non essere banale.
Per scongiurare al massimo eventuali rallentamenti, sfruttiamo le caratteristiche per sviluppatori del browser. Vediamo ad esempio come utilizzare il JavaScript profiler di Internet Explorer 11. Per provarlo non bisogna far altro che: lanciare il nostro gioco, premere F12, selezionare l'icona "Profiler" e cliccare su "Start Profiling".
Dopo aver atteso una trentina di secondi possiamo cliccare su "Stop profiling" e leggere i dati sui tempi di esecuzione delle singole chiamate.
Possiamo esaminare ora le performance e le risorse impiegate da ciascuna funzione del gioco. Nella maggior parte dei casi, verificheremo che un gruppo di funzioni avrà utilizzato la maggior parte del tempo complessivo di esecuzione. È da queste che dobbiamo partire per tentare di ottimizzare il nostro codice. Scendendo in dettaglio possiamo scovare e modificare quelle che sono le routine più lente.
Il consiglio è quello di effettuare spesso profilazioni del codice, soprattutto dopo aver effettuato cambiamenti significativi o aggiunto nuove caratteristiche, che potrebbero avere effetti inattesi sulle performance generali del gioco.
Conclusioni
Sviluppare videogame non è un giochetto ma può essere molto appassionante e divertente. il quinto punto di Kai Jäger parla di creatività ed ha ancora ragione. Aggiungerei la curiosità di scoprire nuove cose e sperimentare.
Inoltre è utile segnalare che un target molto succulento per i nostri HTML5 game è Facebook e che in ogni caso può essere interessante permettere agli utenti di condividere i propri successi nel gioco sui social. Ma anche di questo parleremo ancora in futuro.