Giocando responsabilmente con le canvas API

03/01/2015 00:00 Pubblicato da
Share on FacebookTweet about this on TwitterGoogle+Share on Reddit

canvas_header_html5_game_tutorial

Al tempo degli Dei dell’Olimpo, dei signori della guerra e dei re che spadroneggiavano su una terra in tumulto, il genere umano invocava il soccorso di un eroe per riconquistare la libertà. Finalmente arrivò HTML5, l’invincibile tecnologia web guerriera forgiata dal fuoco di mille battaglie. La lotta per il potere, le sfrenate passioni, gli intrighi, i tradimenti furono affrontati con indomito coraggio da coloro che, soli, poteva cambiare il mondo Web, ovvero dal WHATWG e dal W3C.

  Demo   Github   Download

* Il codice presente su Github potrebbe presentare alcune differenze rispetto a quello proposto in questo articolo.
** Nella demo utilizzare il pulsante sinistro del mouse per spostare la navicella.

Indice

  1. Introduzione
  2. Il progetto: A Space Odyssey
    1. Cosa andremo a realizzare?
    2. Tecnologie
    3. Il codice
      1. index.html
      2. utils.js
      3. game.js
        1. Il Create event
        2. Lo Step event
        3. Il Draw event
      4. global.css
  3. Conclusioni

Introduzione

Il termine HTML5 viene spesso utilizzato come buzzword per riferirsi alle tecnologie Web moderne. Tra le varie novità introdotte, troviamo l’aggiunta di elementi specifici per il controllo di contenuti multimediali come i tag video, audio, la geolocalizzazione ed infine i canvas. In questo articolo verranno trattati in particolare i canvas con un occhio di riguardo alle performance.

Il canvas di HTML5, che è nato come un esperimento della Apple, è lo standard più supportato per la grafica 2D sul web. Un canvas consiste in una tela bitmap (grafica a griglia) che può essere utilizzata per il rendering rapido di grafici, di interfacce grafiche per i videogiochi e molto altro ancora. Una tela (in riferimento ai canvas) è un rettangolo nella pagina su cui è possibile disegnare utilizzando il linguaggio JavaScript.
Le funzionalità base per i canvas sono supportate a partire dalle seguenti versioni dei browser più noti:

  • IE 9
  • Firefox 2
  • Chrome 4
  • Safari 3.1
  • Opera 9.6
  • iOS Safari 3.2
  • Opera Mini 8 (supportate parzialmente)
  • Android Browser 3
  • Opera Mobile 10
  • BlackBerry Browser 7
  • Chrome per Android 39
  • Firefox per Android 33
  • IE Mobile 10
  • UC Browser per Android 9.9

Per ulteriori informazioni sulla compatibilità fare riferimento qui.

A livello di codice HTML, definiamo un canvas nel seguente modo:

Da un punto di vista grafico un canvas è un elemento trasparente avente una lunghezza ed una larghezza predefinita. L’elemento canvas è accessibile come qualsiasi altro elemento del DOM, ossia tramite ID. La trasparenza ha un costo elevato a livello di rendering. Attualmente solo Firefox permette impostare il canvas opaco (ottimizzando così le performance di pittura) attraverso l’aggiunta dell’attributo moz-opaque oppure settando la proprietà mozOpaque su true dell’oggetto canvas.
Andando a consultare la documentazione di MDN, scopriamo che il tag canvas è molto simile al tag img, ad eccezione del fatto che i suoi attributi sono solo width e height (per Firefox anche moz-opaque) che non sono obbligatori. Infatti, se non viene definita l’altezza e la larghezza, il canvas assume di default 150px per l’attributo height e 300px per il width. Da precisare che entrambi i tag, quello di apertura e quello di chiusura sono obbligatori. Al fine di informare l’utente che le canvas API non sono supportate dal proprio browser si può racchiudere del testo tra i tag canvas.
Un canvas può essere visto come un piano cartesiano la cui origine si trova in alto a sinistra.
canvas
Sfruttando la trasparenza, possiamo sovrapporre più canvas e lavorare su più “layer”. Ciò comporterà un leggero aumento delle prestazioni quando si dovranno rappresentare delle scene di una certa complessità.
multi-layer

Cosa andremo a realizzare?

Al fine di poter esporre meglio le potenzialità dei canvas, nonostante io non sia un grande appassionato di videogiochi, verrà mostrato allo sviluppatore come creare un prototipo di un videogioco in HTML5.
In una prospettiva implementativa, bisognerà creare e sovrapporre due canvas. Il primo, denominato background, sarà responsabile dello sfondo del videogioco e della rappresentazione dello stato del player, mentre il secondo, chiamato foreground, si occuperà della rappresentazione della navicella.
Il prototipo cartaceo dell’applicazione, denominata A Space Odyssey, è il seguente:
HTML5 canvas game prototype

Tecnologie

In questa guida si farà uso di HTML, JavaScript (e HTML5 API), CSS e delle icone vettoriali scalabili presenti nel pacchetto Font Awesome.

index.html

In primis andiamo a definire la nostra pagina HTML index.html. In questo file non adoperiamo il tag canvas poiché lo andremo a definire da codice. Quello che è necessario fare, è includere il file utils.js, che farà da contenitore per il codice di supporto della nostra applicazione, ed aggiungere anche un altro file, il game.js, il quale conterrà la logica del videogioco. Utilizzeremo il file di stile global.css per realizzare il multilayering, o meglio la sovrapposizione dei due canvas che andranno a costituire la room del videogioco.

utils.js

Il passo successivo consiste nella definizione del file utils.js che conterrà alcune funzionalità che verranno utilizzate dal file game.js.
Per prima cosa andiamo a verificare, ed eventualmente definiamo, il metodo requestAnimationFrame() che consente di riferire al browser che si vuole effettuare un’animazione e di richiedere di chiamare una determinata funzione che aggiorni l’animazione prima del repaint successivo. Se il browser in questione non è dotato di tale funzionalità, verrà utilizzata la funzione setTimeout(). La requestAnimationFrame() accetta in input una funzione di callback la quale sarà responsabile dell’aggiornamento della nostra animazione per il prossimo repaint. In altre parole la funzione di callback verrà richiamata circa sessanta volte al secondo.
Perché usare la requestAnimationFrame() invece di setTimeout() oppure della setInterval()? La risposta è molto semplice: per questioni di performance. Infatti, per superare i problemi di efficienza, Mozilla, i creatori di Firefox, hanno proposto la funzione requestAnimationFrame() che in un secondo tempo è stata adottata ed implementata dai browser Chrome e Safari. A differenza delle altre due funzioni che forzano il browser ad eseguire certe operazioni dopo un determinato intervallo di tempo, la requestAnimationFrame() indica al browser di rappresentare graficamente la nostra animazione il prima possibile (nel caso migliore ogni sessantesimo di secondo) e non ad un intervallo predeterminato. In questo modo i browser possono ottimizzare le performance secondo il carico di lavoro, il livello della batteria del dispositivo e in base alla visibilità o meno dell’elemento su cui viene eseguita l’operazione di disegno (ad esempio quando si passa su un altro Tab, la requestAnimationFrame() non viene più eseguita). Pertanto, utilizzando questa funzione ad hoc le animazioni diventerebbero più dolci, sincronizzate con la GPU e stresserebbero meno la CPU.

Adesso andiamo a definire l’oggetto utils che per comodità verrà associato al namespace globale window.

La seguente funzione sarà utile per calcolare la distanza tra due punti passati in input.

isNotCanvasSupported() ci consente di determinare se il browser supporta le canvas API. Esso restituisce true se non si possono utilizzare le canvas.

Con createAppendCanvas() possiamo creare un nuovo elemento canvas avente come id, larghezza e lunghezza pari a quelli passati in input. All’oggetto canvas verrà associato l’oggetto context (2d) che offre i metodi e le proprietà per disegnare e manipolare immagini su un elemento di tipo canvas. Successivamente, il canvas verrà appeso all’oggetto father passato in input e verrà ritornato l’elemento appena creato.

Il seguente metodo ha la stessa logica di quello precedente ad eccezione del fatto che serve per creare un qualsiasi tipo di elemento e appenderlo all’elemento padre(father) passato in input. Il parametro type è di tipo stringa e può essere un qualsiasi elemento del DOM (es “div”, “span”, “p”, etc.).

La seguente funzione va chiamata una sola volta e prende come parametro l’elemento su cui si vuole aggiungere il listener (nel nostro caso si tratterà di un canvas). captureMouse() restituisce un oggetto cursor che conterrà un riferimento all’oggetto event e le coordinate del cursore del mouse rispetto all’elemento passato in input. Naturalmente le proprietà x e y saranno aggiornate costantemente, quindi il nostro oggetto cursor conterrà sempre la posizione aggiornata del cursore.

game.js

Per accertarci che la pagina sia stata caricata completamente prima dell’esecuzione del videogioco dovremo associare all’evento onload una funzione che verrà eseguita una volta generato l’evento load.
Con l’aumentare della complessità del videogioco si potrebbero avere più assegnamenti di funzione all’evento precedentemente citato, indi, per gestire tale situazione in maniera elegante andremo a definire le seguenti righe di codice:

Da come si può notare inizialmente viene effettuato un controllo sul tipo del valore assegnato a window.onload, se non si tratta di una function significa che possiamo assegnare la funzione aSpaceOdyssey(). In caso contrario, bisognerà assegnare una nuova funzione a window.onload che contenga il vecchio window.onload assieme alla nostra funzione.

Per semplicità possiamo suddividere il blocco di codice della funzione aSpaceOdyssey() in tre parti, denominate eventi (se si ha un po’ di dimestichezza con Game Maker probabilmente i nomi di questi eventi risulteranno molto famigliari):

  1. Create event – eseguito una sola volta, definisce gli oggetti e prepara l’ambiente
  2. Step event – si ripete con costanza, aggiorna lo stato del videogioco
  3. Draw event – reiterato continuamente, aggiorna la veste grafica del videogioco

Il Create event

Nel Create event andremmo a preparare il terreno per gli altri due eventi, lo Step event ed il Draw event, ossia dovremmo definire le classi, creare i canvas ed implementare qualche funzione di supporto.
Nelle prime due righe di codice dichiariamo due variabili che verranno utilizzate per impostare la larghezza e l’altezza dei canvas. Subito dopo abbiamo la classe SpaceShip che rappresenta la navicella spaziale.

La navicella ha due proprietà che conservano ognuna una direzione: la prima rappresenta la direzione dell’oggetto navicella (this.direction) mentre la seconda la direzione dell’animazione (this.rotation). Lo scaleX e lo scaleY invece potrebbero tornare utili per creare delle animazioni di breve durata in fase di creazione e di distruzione della navicella (aumentando e riducendo la scala dell’immagine). La proprietà fuel indica la quantità di carburante disponibile, il suo valore può decrementare di fuelDec unità ed il valore massimo che può assumere fuel è pari a fuelMax. Il numero delle vite è memorizzato nella variabile lives e sarà rappresentato graficamente con l’ausilio della stringa livesString. La proprietà flame ci permetterà di capire se l’oggetto è in movimento o meno.

Alla classe SpaceShip associamo un canvas (riga 1). Questo canvas ha la larghezza e l’altezza della width e della height passati in input e ci tornerà utile quando dovremmo fare il pre-rendering. Il pre-rendering consiste nel rendering temporaneo di immagini su un canvas non rappresentato graficamente che sarà aggiunto in un secondo tempo sul canvas visibile all’utente (l’utilità sta nell’ottenere delle buone performance in fase di rendering). In altri termini mi preparo una piccola tela non visibile all’utente su cui rappresento la mia navicella tenendo conto della direzione, della scala e altro, una volta che ho pre-renderizzato, prendo la mia tela e la unisco a quella visibile all’utente, che nel nostro caso potrebbe essere il canvas denominato foreground oppure il canvas background.

Ritornando al codice, per poter accedere alle funzionalità di disegno bidimensionale del canvas spaceShipCanvas dobbiamo prendere il suo contesto attraverso this.spaceShipCanvas.getContext("2d").

Ottenuto il contesto, andiamo ad istanziare un oggetto di tipo Image. Associamo all’immagine una sorgente dalla quale prendere il file .png (con l’istruzione spaceShipSprite.src = "...").

Andiamo a definire alcuni metodi per la classe SpaceShip. Il primo metodo, updateLivesString(), aggiorna la stringa che rappresenta il numero delle vite in base alla variabile lives dell’istanza SpaceShip (\uf004 rappresenta un carattere del package FontAwesome). Il metodo rotate() aggiorna il valore di rotation dell’istanza SpaceShip secondo la posizione del cursore e la posizione della navicella. Math.atan2() restituisce l’angolo la cui tangente è il quoziente della differenza tra la posizione del cursore e la posizione dell’oggetto sul piano xy. A differenza di rotate(), il metodo updateDirection() aggiorna la variabile direction solo se la variabile flame è settata su true, così, la nostra spaceship si muove nella direzione del cursore solo se l’utente, come vedremo dopo, ha cliccato sul pulsante sinistro del mouse e quindi ha invocato il metodo go().

Nel metodo updatePosition() andiamo a verificare la distanza tra la posizione del cursore del mouse e quella della navicella, e se questa è superiore della metà della larghezza della navicella allora aggiorna la posizione della SpaceShip in base alla velocità e alla direzione. Come comportamento predefinito, la navicella trasla sull’asse delle y di game.enviroment.gravity unità e se incontra un qualsiasi bordo della nostra room smette di muoversi nella direzione di quel particolare bordo.

Passiamo alla rappresentazione grafica della navicella. Il metodo draw() prende in input un oggetto di tipo context e attraverso il metodo drawImage() unisce alla tela passata in input lo spaceShipCanvas su cui è raffigurata l’immagine della navicella. drawImage() prende in input come primo parametro un’immagine oppure un canvas, come secondo e terzo parametro invece le coordinate x e y.

Precedentemente, avevo parlato del pre-rendering. Ebbene, è arrivato il momento di realizzarlo. Andiamo a definire un metodo, il preRenderSpaceShip(), che verrà richiamato nello Step event (dunque verrà invocato spesso). Tale metodo si occuperà della rappresentazione della navicella in funzione dei valori delle sue variabili.

La prima istruzione (this.spaceShipContext.save()) salva lo stato del canvas. Lo stato di un canvas consta in uno snapshot di tutti li stilli e le trasformazioni che sono state applicate precedentemente. Quando si invoca il metodo save() lo stato viene salvato su uno stack, mentre quando si invoca il metodo restore() si ripristina l’ultimo stato salvato.
L’istruzione successiva (this.spaceShipContext.clearRect(0, 0, this.width, this.height)) provvede a cancellare una determinate area del nostro canvas, in questo caso si tratta dell’intero canvas. Il metodo clearRect(x, y, width, height) rede tutti i pixel del rettangolo definito in input trasparenti.
Con this.spaceShipContext.scale(this.scaleX, this.scaleY) possiamo aumentare o ridurre le dimensioni dell’immagine.
La nostra spaceship deve essere posizionata al centro e ruotata di this.rotation unità intorno al suo centro, quindi per prima cosa facciamo traslare il nostro piano 2d al centro con l’istruzione this.spaceShipContext.translate(this.width / 2, this.height / 2), ruotiamo l’immagine di this.rotation radianti ed infine trasliamo nuovamente il piano nella posizione iniziale. Procediamo con il rendering dell’immagine sul canvas nella posizione (this.width - this.spaceShipSprite.width) / 2 e (this.height - this.spaceShipSprite.height) / 2).

Se this.flame == true (ovvero se l’utente ha cliccato sul pulsante sinistro del mouse) e se l’immagine flame.png è stata recuperata con successo (cioè se this.flameImage.complete == true) allora bisogna rappresentare graficamente anche la fiamma:

Come abbiamo fatto prima, trasliamo e poi invochiamo il metodo drawImage(). In questo caso abbiamo invocato una sola volta il metodo translate(), la seconda traslazione avviene in drawImage() (facendo -this.width / 2 e -this.spaceShipSprite.height / 2).
Con l’istruzione [-1, 1][Math.floor(Math.random() * 10) % 2] scegliamo un numero in modo pseudocasuale dall’array [-1,1], che ci consente di ottenere un effetto oscillatorio dello sprite flame.png.
I due canvas verranno incapsulati all’interno di un elemento DIV che verrà centrato orizzontalmente all’interno della pagina (tramite istruzioni CSS). L’id del wrapper DIV sarà canvas-container e la sua larghezza sarà paria 800px mentre la sua altezza 640px (che sono anche le dimensioni dei due canvas).

Adesso andiamo a definire un object initializer che conterrà alcune proprietà del videogioco, tra cui il riferimento al background canvas, situato al livello 0 ed il riferimento al foreground cavas posizionato sopra il background canvas. Abbiamo un ulteriore canvas, il panelCanvas, che verrà unito al backgroundCanvas. L’enviroment invece contiene alcune proprietà dell’ambiente del videogioco, come la gravità (gravity) che indica di quanto dovrà traslare la navicella sull’asse delle y.
Con window.game.backgroundCanvas.mozOpaque impostato su true indichiamo che il backgroundCanvas dovrà essere opaco, in questo modo la GPU verrà stressata di meno. L’istruzione è compatibile solo con il browser Firefox.

Al namespace window.game.cursor aggiungiamo l’oggetto cursor, restituito dal metodo captureMouse(), il quale contiene la posizione del cursore del mouse. Istanziamo un oggetto di tipo SpaceShip che verrà posizionato al centro della room e avrà una dimensione pari a 128×128 px. Successivamente aggiungiamo gli event handler per gli eventi mouseup e mousedown. Memorizziamo i context di ogni canvas con game.backgroundContext = game.backgroundCanvas.context, game.foregroundContext = game.foregroundCanvas.context e game.panelContext = game.panelCanvas.context (la proprietà contex è stata aggiunta precedentemente dal metodo window.utils.createAppendCanvas()).

Invochiamo il metodo updateLivesString() della navicella che aggiorna la stringa da stampare a video.
Procediamo con la definizione del background del videogame riempendolo con un insieme di puntini bianchi che rappresentano il nostro cielo stellato. Lo sfondo dovrà muoversi verso il basso per dare l’impressione di movimento. Associamo al namespace game.stars un oggetto che gestisca il background. L’insieme delle stelle è contenuto in un array che ha dimensione pari al valore della proprietà units (è consigliabile tenerlo inferiore o uguale a 100 se non si vuole avere un calo del numero degli FPS). La traslazione verso il basso avviene alla velocità impostata nella proprietà speed.
Ogni volta che una stella supera il bordo inferiore del canvas, le sue proprietà vengono “resettate” tramite l’invocazione del metodo resetStar().
resetStar() ridefinisce la posizione della stella sull’asse delle y (impostandola a -1 così non sarà visibile dall’utente) e sull’asse delle x (in questo caso generando un numero casuale).
Il metodo incipit() invece, invocato una volta creato un oggetto stella, provvede a posizionare la stella in modo pseudocasuale anche sull’asse delle y, così, una volta aperta la pagina, si avrà un background cosparso di stelle.
Il metodo drawCircle() rappresenta graficamente una stella. Il metodo disegna una circonferenza di raggio star.radius e opacità star.opacity nella posizione star.x e star.y sul context passato in input.
drawCicrle() potrebbe rivelarsi meno efficiente se venisse invocato in un ciclo for che agisce su un array di star i cui valori per le proprietà opacity e radius rimangono invariati. In quel caso la proprietà ctx.fillStyle assumerebbe lo stesso valore per ogni star dell’array, ovvero verrebbe reimpostata con lo stesso valore un numero di volte pari alla dimensione dell’array. Quindi dobbiamo implementare un altro metodo che ci consenta di renderizzare il nostro array di stelle in modo più efficiente e lo facciamo con drawCircles() il quale ha la stessa logica del metodo precedente a differenza del fatto che setta il valore di fillStyle una sola volta.
Quando dobbiamo rappresentare circonferenze o archi è necessario prima eseguire un altro metodo del contesto bidimensionale, il beginPath(), che indica al context che si vuole definire un percorso. Una volta concluso il path, ossia, nel nostro caso, dopo aver utilizzato il metodo arc(x, y, raggio, angoloDiInizio, angoloDiFine, inSensoAntiorario) è necessario riempire il percorso con un colore oppure semplicemente rappresentare i bordi della figura attraverso i metodi context.fill() e context.stroke() rispettivamente. Il colore di default per i due metodi è il nero (#000000). Possiamo ridefinire il colore impostando la strokeStyle = "#MioColore" prima di chiamare il metodo stroke(), oppure fillStyle = "# MioColore " prima di chiamare il metodo fill().
Quindi, se volessimo disegnare solo i bordi di una circonferenza oppure riempirla con un colore dovremo scrivere del codice simile a quello sottostante:
Le ultime istruzioni del Create event creano 100 oggetti di tipo star che vengono aggiunti all’array game.stars.array solo dopo aver richiamato il metodo incipit().

Lo Step event

Lo Step event consiste in una funzione che viene eseguita un numero di volte inferiore rispetto a quella dello Draw event (in particolare circa ogni trentesimo di secondo, mentre il Draw event ogni sessantesimo di secondo).
Nelle prime istruzione viene controllato se l’utente ha cliccato sul pulsante sinistro del mouse e se la velocità della spaceship è inferiore al valore massimo di velocità ammissibile. In caso affermativo il valore speed della navicella viene incrementato di speedInc unità. In caso negativo invece, la velocità viene decrementata. Successivamente vengono invocati i metodi updateDirection() che prende in input la posizione del cursore del mouse, updatePosition() che aggiorna la posizione della navicella sul canvas, rotate() che aggiorna il valore della proprietà rotation e per concludere viene richiamato il metodo preRenderSpaceShip() che aggiorna l’immagine graficamente sulla tela temporanea spaceShipCanvas.
Nel ciclo for vengono aggiornate le posizioni delle stelle. Se il loro valore y è maggiore dell’altezza del canvas, allora esso viene riportato a -1, in caso contrario viene solo incrementato di game.stars.speed.

Per mostrare all’utente la quantità di carburante a disposizione ed il numero di vite rimaste verrà utilizzato il panelContext. Con game.panelContext.clearRect(…) cancelliamo l’immagine precedente. Salviamo il contesto con save(), definiamo il colore del testo con game.panelContext.fillStyle = "#ffffff", specifichiamo il font con game.panelContext.font = "bold 10px Arial" ed infine rappresentiamo a schermo il testo “FUEL” con game.panelContext.fillText("FUEL", 5, 15) nella posizione x = 5 e y = 15. Il livello del carburante verrà rappresentato da un rettangolo che ha una larghezza pari al valore di fuel. Ad un livello inferiore invece si troverà un altro rettangolo che rappresenterà la quantità massima di carburante.
Nelle ultime righe di codice si provvede a decrementare il valore di fuel ed eventualmente si imposta il valore di flame su false se il livello del carburante è inferiore a 0. Se la navicella ha il valore di flame impostato su false, allora ad ogni step il valore di fuel verrà incrementato di game.SpaceShip.fuelDec / 10.

Il Draw event

La prima istruzione indica al browser che si vuole effettuare una richiesta di animazione. Subito dopo andiamo a cancellare le immagini precedentemente disegnate sul canvas attraverso il metodo clearRect().
Il metodo drawCircles() scorre l’interro array delle stelle e le rappresenta in base alla loro posizione sul canvas che avevamo chiamato backgroundCanvas. Sopra il cielo stellato (sempre usando il backgroundContext) andiamo a rappresentare il panelCanvas, sul quale abbiamo riprodotto il livello di carburante ed il numero di vite del player.
Infine, cancelliamo anche il foregroundCanvas utilizzando il metodo clearRect(…) ed invochiamo il metodo draw() dell’istanza della spaceship passandole come parametro il game.foregroundContext.

global.css

Il file css inizialmente fa un reset delle proprietà degli elementi della pagina HTML.

Nella seconda parte del file (GAME) andiamo a definire le proprietà degli elementi del videogioco. In particolare l’elemento identificato da #canvas-container dovrà essere posizionato al centro della finestra. Voglio ricordare che le sue dimensioni vengono impostate nel file game.js. I canvas identificati come #foreground e #background dovranno avere una position: absolute in questo modo si sovrappongono all’interno del #canvas-container. Il canvas #background inoltre ha uno sfondo nero.
Con @font-face andiamo a importare i font di FontAwesome situati nella directory fonts (da notare che non utilizziamo l’intero package di FontAwesome).

Se apriamo la pagina index.html in un browser web dovremmo ottenere la seguente schermata:
A Space Odyssey

Conclusioni

La bella notizia è che abbiamo ottenuto un prototipo di un videogioco che mantiene i 60 fotogrammi al secondo.

Prestazioni di A Space Odyssey su  Google Chrome 39

Prestazioni di A Space Odyssey su Google Chrome 39

Prestazioni di A Space Odyssey su Firefox

Prestazioni di A Space Odyssey su Firefox 34

Prestazioni di A Space Odyssey su Internet Explorer 11

Prestazioni di A Space Odyssey su Internet Explorer 11
* Il numero degli FPS dipendono molto anche dal carico di lavoro della macchina.
** Le immagini erano fluide su tutti i browser su cui sono stati fatti i test (Google Chrome, Firefox ed Internet Explorer).

La brutta invece è che potevamo pure risparmiarci tutto questo codice :)
HTML5 non è più una novità. Si possono trovare moltissimi framework che rendono la vita dello sviluppatore molto più gradevole. L’obiettivo primario di questo articolo non è quello di mostrare come creare un videogioco, ma quello di indicare come mantenere delle buone performance quando si opera con le canvas API.
In sintesi, giocare responsabilmente con le canvas API significa tener conto dei seguenti punti:

  • quando è possibile, effettuare il pre-rendering;
  • evitare l’utilizzo della proprietà shadowBlur (per maggiori informazioni dare un’occhiata qui);
  • utilizzare la window.requestAnimationFrame() per le animazioni (e non il setTimeout() o il setInterval());
  • se non si ha bisogno di un canvas trasparente, attivare l’opacità sul canvas;
  • per le scene di una certa complessità utilizzare canvas multipli sovrapposti;
  • per le coordinate evitare l’utilizzo di valori in virgola mobile(usare invece i numeri interi);
  • renderizzare ciclicamente solo le parti del canvas che cambiano. Ad esempio nel caso del panelCanvas, ad ogni step, possiamo invocare il metodo clearRect() solo per l’area in cui viene rappresentato il livello del carburante poiché il valore di game.SpaceShip.fuel cambia molto frequentemente. Ciò non vale invece per il testo stampato a video (come “FUEL” o “LIVES”) che può essere renderizzato sul canvas una sola volta. Nel caso della game.SpaeShip.livesString, si potrebbe cancellare solo l’area in cui essa viene stampata ogni volta che il valore del numero di vite cambia.
  • evitare di cambiare frequentemente lo stato del canvas. Si prenda come esempio un ciclo for in cui viene settata la proprietà fillStyle = "#ffffff" e renderizzato un oggetto punto appartenente all’array (attraverso il metodo arc()). La prima istruzione del ciclo viene eseguita inutilmente per ogni elemento dell’array. Ciò potrebbe incidere negativamente sul numero degli FPS soprattutto con l’aumentare del numero di proprietà di stato che vengono reimpostate. Per ovviare a questo problema, naturalmente, bisogna settare lo stato prima del ciclo for.
  • quando si vogliono rappresentare un insieme di rette, archi, curve quadratiche o curve di bezier è consigliabile creare un path contenente tutti li elementi per poi renderizzarli una sola volta.
    Si osservino i sottostanti frammenti di codice:

  Demo   Github   Download

* Il codice presente su Github potrebbe presentare alcune differenze rispetto a quello proposto in questo articolo.
** Nella demo utilizzare il pulsante sinistro del mouse per spostare la navicella.

21/07/2017 10:14

Comments are closed here.