Introduzione
Quando abbiamo convertito il nostro gioco di parole crociate Wordico da Flash ad HTML5, la nostra prima attività è stata dimenticare tutto ciò che sapevamo sulla creazione di un'esperienza utente completa nel browser. Mentre Flash offriva un'API singola e completa per tutti gli aspetti dello sviluppo delle applicazioni, dal disegno vettoriale al rilevamento dei hit dei poligoni alla parsing XML, HTML5 offriva un insieme di specifiche con un supporto del browser variabile. Ci siamo anche chiesti se l'HTML, un linguaggio specifico per i documenti, e il CSS, un linguaggio incentrato sulle caselle, fossero adatti per creare un gioco. Il gioco verrebbe visualizzato in modo uniforme su tutti i browser, come in Flash, e avrà lo stesso aspetto e lo stesso comportamento? Per Wordico, la risposta è stata sì.
Qual è il tuo vettore, Victor?
Abbiamo sviluppato la versione originale di Wordico utilizzando solo grafica vettoriale: linee, curve, riempimenti e gradienti. Il risultato è stato un sistema altamente compatto e infinitamente scalabile:
Abbiamo anche sfruttato la sequenza temporale di Flash per creare oggetti con più stati. Ad esempio, abbiamo utilizzato nove keyframe denominati per l'oggetto Space
:
In HTML5, invece, utilizziamo uno sprite bitmap:
Per creare il tabellone di gioco 15 x 15 da singoli spazi, eseguiamo l'iterazione su una notazione di stringa di 225 caratteri in cui ogni spazio è rappresentato da un carattere diverso (ad esempio "t" per la tripla lettera e "T" per la tripla parola). Si trattava di un'operazione semplice in Flash: abbiamo semplicemente stampato gli spazi e li abbiamo disposti in una griglia:
var spaces:Array = new Array();
for (var i:int = 0; i < 225; i++) {
var space:Space = new Space(i, layout.charAt(i));
...
spaces.push(addChild(space));
}
LayoutUtil.grid(spaces, 15);
In HTML5, il processo è leggermente più complesso. Utilizziamo l'elemento <canvas>
, una superficie di disegno bitmap, per dipingere il tabellone un quadrato alla volta. Il primo passaggio consiste nel caricare lo sprite immagine. Una volta caricato, eseguiamo l'iterazione della notazione del layout, disegnando una parte diversa dell'immagine a ogni iterazione:
var x = 0; // x coordinate
var y = 0; // y coordinate
var w = 35; // width and height of a space
for (var i = 0; i < 225; i++) {
if (i && i % 15 == 0) {
x = 0;
y += w;
}
var imageX = "_dDFtTqQxm".indexOf(layout.charAt(i)) * 70;
canvas.drawImage("spaces.png", imageX, 0, 70, 70, x, y, w, w);
x += w;
}
Ecco il risultato nel browser web. Tieni presente che la tela stessa ha un'ombra esterna CSS:
La conversione dell'oggetto riquadro è stata un'operazione simile. In Flash, utilizzavamo campi di testo e forme vettoriali:
In HTML5, combiniamo tre sprite di immagini in un singolo elemento <canvas>
in fase di esecuzione:
Ora abbiamo 100 canvas (uno per ogni riquadro) più un canvas per la plancia di gioco. Ecco il markup per un riquadro "H":
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Ecco il CSS corrispondente:
.tile {
width: 35px;
height: 35px;
position: absolute;
cursor: pointer;
z-index: 1000;
}
.tile-drag {
-moz-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
-webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
-moz-transform: scale(1.10);
-webkit-transform: scale(1.10);
-webkit-box-reflect: 0px;
opacity: 0.85;
}
.tile-locked {
cursor: default;
}
.tile-racked {
-webkit-box-reflect: below 0px -webkit-gradient(linear, 0% 0%, 0% 100%,
from(transparent), color-stop(0.70, transparent), to(white));
}
Applichiamo gli effetti CSS3 quando la scheda viene trascinata (ombra, opacità e ridimensionamento) e quando è posizionata nella sezione (riflesso):
L'utilizzo di immagini raster presenta alcuni vantaggi evidenti. Innanzitutto, il risultato è preciso al pixel. In secondo luogo, queste immagini possono essere memorizzate nella cache dal browser. Terzo, con un po' di lavoro in più, possiamo sostituire le immagini per creare nuovi design di riquadri, ad esempio un riquadro in metallo, e questo lavoro di progettazione può essere svolto in Photoshop anziché in Flash.
Il lato negativo? Se utilizzi le immagini, rinunci all'accesso programmatico ai campi di testo. In Flash, era un'operazione semplice cambiare il colore o altre proprietà del tipo; in HTML5, queste proprietà sono incorporate nelle immagini stesse. Abbiamo provato il testo HTML, ma richiedeva molto markup e CSS aggiuntivi. Abbiamo provato anche il testo canvas, ma i risultati non erano coerenti tra i browser.
Logica fuzzy
Volevamo sfruttare al meglio la finestra del browser a qualsiasi dimensione ed evitare di scorrere. Si trattava di un'operazione relativamente semplice in Flash, poiché l'intero gioco era disegnato in vettori e poteva essere ingrandito o rimpicciolito senza perdere fedeltà. Ma era più complicato in HTML. Abbiamo provato a utilizzare la scala CSS, ma abbiamo ottenuto una tela sfocata:
La nostra soluzione è ridisegnare la plancia, la rastrelliera e le tessere ogni volta che l'utente ridimensiona il browser:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
Il risultato sono immagini nitide e layout accattivanti su qualsiasi dimensione dello schermo:
Arriva al punto
Poiché ogni riquadro è posizionato in modo assoluto e deve essere allineato con precisione alla plancia e al rack, abbiamo bisogno di un sistema di posizionamento affidabile. Utilizziamo due funzioni, Bounds
e Point
, per gestire la posizione degli elementi nello spazio globale (la pagina HTML). Bounds
descrive un'area rettangolare sulla pagina, mentre Point
descrive una coordinata x,y rispetto all'angolo in alto a sinistra della pagina (0,0), noto anche come punto di registrazione.
Con Bounds
, possiamo rilevare l'intersezione di due elementi rettangolari (ad esempio quando una scheda attraversa la rastrelliera) o se un'area rettangolare (ad esempio uno spazio per due lettere) contiene un punto arbitrario (ad esempio il punto centrale di una scheda). Ecco l'implementazione di Bounds:
// bounds.js
function Bounds(element) {
var x = element.offsetLeft;
var y = element.offsetTop;
var w = element.offsetWidth;
var h = element.offsetHeight;
this.left = x;
this.right = x + w;
this.top = y;
this.bottom = y + h;
this.width = w;
this.height = h;
this.x = x;
this.y = y;
this.midx = x + (w / 2);
this.midy = y + (h / 2);
this.topleft = new Point(x, y);
this.topright = new Point(x + w, y);
this.bottomleft = new Point(x, y + h);
this.bottomright = new Point(x + w, y + h);
this.middle = new Point(x + (w / 2), y + (h / 2));
}
Bounds.prototype.contains = function (point) {
return point.x > this.left &&
point.x < this.right &&
point.y > this.top &&
point.y < this.bottom;
}
Bounds.prototype.intersects = function (bounds) {
return this.contains(bounds.topleft) ||
this.contains(bounds.topright) ||
this.contains(bounds.bottomleft) ||
this.contains(bounds.bottomright) ||
bounds.contains(this.topleft) ||
bounds.contains(this.topright) ||
bounds.contains(this.bottomleft) ||
bounds.contains(this.bottomright);
}
Bounds.prototype.toString = function () {
return [this.x, this.y, this.width, this.height].join(",");
}
Utilizziamo Point
per determinare la coordinata assoluta (angolo in alto a sinistra) di qualsiasi elemento della pagina o di un evento del mouse. Point
contiene anche metodi per calcolare la distanza e la direzione, necessari per creare effetti di animazione. Ecco l'implementazione di Point
:
// point.js
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.distance = function (point) {
var a = point.x - this.x;
var b = point.y - this.y;
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
Point.prototype.distanceX = function (point) {
return Math.abs(this.x - point.x);
}
Point.prototype.distanceY = function (point) {
return Math.abs(this.y - point.y);
}
Point.prototype.interpolate = function (point, pct) {
var x = this.x + ((point.x - this.x) * pct);
var y = this.y + ((point.y - this.y) * pct);
return new Point(x, y);
}
Point.prototype.offset = function (x, y) {
return new Point(this.x + x, this.y + y);
}
Point.prototype.vector = function (point) {
return new Point(point.x - this.x, point.y - this.y);
}
Point.prototype.toString = function () {
return this.x + "," + this.y;
}
// static
Point.fromElement = function (element) {
return new Point(element.offsetLeft, element.offsetTop);
}
// static
Point.fromEvent = function (evt) {
return new Point(evt.x || evt.clientX, evt.y || evt.clientY);
}
Queste funzioni costituiscono la base delle funzionalità di trascinamento e animazione. Ad esempio, utilizziamo Bounds.intersects()
per determinare se una tessera si sovrappone a uno spazio sulla plancia di gioco, Point.vector()
per determinare la direzione di una tessera trascinata e Point.interpolate()
in combinazione con un timer per creare un tween dinamico o un effetto di attenuazione.
Seguo il flusso
Sebbene i layout di dimensioni fisse siano più facili da produrre in Flash, i layout fluidi sono molto più facili da generare con HTML e il modello di riquadro CSS. Prendi in considerazione la seguente visualizzazione della griglia, con larghezza e altezza variabili:
Oppure prendi in considerazione il riquadro della chat. La versione Flash richiedeva più gestori di eventi per rispondere alle azioni del mouse, una maschera per l'area scorrevole, operazioni matematiche per calcolare la posizione di scorrimento e molto altro codice per unire il tutto.
La versione HTML, invece, è solo un <div>
con un'altezza fissa e la proprietà overflow impostata su hidden. Lo scorrimento non ci costa nulla.
In questi casi, come per le normali attività di layout, HTML e CSS superano Flash.
Mi senti?
Abbiamo riscontrato problemi con il tag <audio>
: semplicemente non era in grado di riprodurre ripetutamente effetti sonori brevi in alcuni browser. Abbiamo provato due soluzioni alternative. Innanzitutto, abbiamo allungato i file audio con spazi vuoti. Poi abbiamo provato ad alternare la riproduzione su più canali audio. Nessuna delle due tecniche era completamente efficace o elegante.
Alla fine abbiamo deciso di creare il nostro player audio Flash e di utilizzare l'audio HTML5 come opzione di riserva. Ecco il codice di base in Flash:
var sounds = new Array();
function playSound(path:String):void {
var sound:Sound = sounds[path];
if (sound == null) {
sound = new Sound();
sound.addEventListener(Event.COMPLETE, function (evt:Event) {
sound.play();
});
sound.load(new URLRequest(path));
sounds[path] = sound;
}
else {
sound.play();
}
}
ExternalInterface.addCallback("playSound", playSound);
In JavaScript, tentiamo di rilevare il player Flash incorporato. Se non va a buon fine, viene creato un nodo <audio>
per ogni file audio:
function play(String soundId) {
var src = "/audio/" + soundId + ".mp3";
// Flash
try {
var swf = window["swfplayer"] || document["swfplayer"];
swf.playSound(src);
}
// or HTML5 audio
catch (e) {
var sound = document.getElementById(soundId);
if (sound == null || sound == undefined) {
var sound = document.createElement("audio");
sound.id = soundId;
sound.src = src;
document.body.appendChild(sound);
}
sound.play();
}
}
Tieni presente che questa opzione funziona solo per i file MP3. Non ci siamo mai preoccupati di supportare i file OGG. Ci auguriamo che il settore decida di adottare un unico formato nel prossimo futuro.
Posizione del sondaggio
In HTML5 utilizziamo la stessa tecnica utilizzata in Flash per aggiornare lo stato del gioco: ogni 10 secondi il client richiede aggiornamenti al server. Se lo stato del gioco è cambiato dall'ultimo sondaggio, il client riceve e gestisce le modifiche; in caso contrario, non succede nulla. Questa tecnica di polling tradizionale è accettabile, anche se non molto elegante. Tuttavia, con il perfezionamento del gioco e l'aspettativa degli utenti di un'interazione in tempo reale sulla rete, vorremmo passare al polling lungo o a WebSockets. I WebSocket, in particolare, offrono molte opportunità per migliorare il gameplay.
Che strumento!
Abbiamo utilizzato Google Web Toolkit (GWT) per sviluppare sia l'interfaccia utente front-end sia la logica di controllo back-end (autenticazione, convalida, persistenza e così via). Il codice JavaScript stesso viene compilato dal codice sorgente Java. Ad esempio, la funzione Punto è adattata da Point.java
:
package com.wordico.client.view.layout;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;
public class Point {
public double x;
public double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double distance(Point point) {
double a = point.x - this.x;
double b = point.y - this.y;
return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
...
}
Alcuni classi di interfaccia utente hanno file di modelli corrispondenti in cui gli elementi della pagina sono "associati" ai membri della classe. Ad esempio, ChatPanel.ui.xml
corrisponde a ChatPanel.java
:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder
xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
xmlns:w="urn:import:com.wordico.client.view.widget">
<g:HTMLPanel>
<div class="palette">
<g:ScrollPanel ui:field="messagesScroll">
<g:FlowPanel ui:field="messagesFlow"></g:FlowPanel>
</g:ScrollPanel>
<g:TextBox ui:field="chatInput"></g:TextBox>
</div>
</g:HTMLPanel>
</ui:UiBinder>
I dettagli completi non rientrano nell'ambito di questo articolo, ma ti invitiamo a provare GWT per il tuo prossimo progetto HTML5.
Perché utilizzare Java? Innanzitutto, per la tipizzazione rigorosa. Sebbene la tipizzazione dinamica sia utile in JavaScript, ad esempio la capacità di un array di contenere valori di tipi diversi, può essere un problema in progetti di grandi dimensioni e complessi. In secondo luogo, per le funzionalità di refactoring. Prova a immaginare come modificare la firma di un metodo JavaScript in migliaia di righe di codice: non è facile. Ma con un buon IDE Java, è un gioco da ragazzi. Infine, a scopo di test. La scrittura di test di unità per le classi Java supera la tecnica consolidata del "salva ed esegui l'aggiornamento".
Riepilogo
A parte i problemi audio, HTML5 ha superato di gran lunga le nostre aspettative. Wordico non solo ha la stessa grafica di Flash, ma è anche fluido e reattivo. Non avremmo potuto farlo senza Canvas e CSS3. La nostra prossima sfida: adattare Wordico all'utilizzo da dispositivo mobile.