Introduction
Lorsque nous avons converti notre jeu de mots croisés Wordico de Flash vers HTML5, notre première tâche a été de désapprendre tout ce que nous savions sur la création d'une expérience utilisateur riche dans le navigateur. Alors que Flash proposait une API unique et complète pour tous les aspects du développement d'applications (du dessin vectoriel à la détection de polygones en passant par l'analyse XML), HTML5 proposait un ensemble de spécifications disparates avec une compatibilité variable avec les navigateurs. Nous nous sommes également demandés si le langage HTML, spécifique aux documents, et le langage CSS, centré sur les cases, étaient adaptés à la création d'un jeu. Le jeu s'affichera-t-il de manière uniforme dans tous les navigateurs, comme il le faisait dans Flash ? Son apparence et son comportement seront-ils aussi agréables ? Pour Wordico, la réponse était oui.
Quel est votre vecteur, Victor ?
Nous avons développé la version d'origine de Wordico en utilisant uniquement des graphiques vectoriels: lignes, courbes, remplissages et dégradés. Le résultat est à la fois très compact et infiniment évolutif:
Nous avons également utilisé la timeline Flash pour créer des objets ayant plusieurs états. Par exemple, nous avons utilisé neuf clés-images nommées pour l'objet Space
:
En HTML5, cependant, nous utilisons un sprite bitmap:
Pour créer le plateau de jeu de 15 x 15 cases à partir d'espaces individuels, nous itérons sur une notation de chaîne de 225 caractères dans laquelle chaque espace est représenté par un caractère différent (par exemple, "t" pour une lettre triple et "T" pour un mot triple). Il s'agissait d'une opération simple dans Flash. Nous avons simplement créé des espaces et les avons disposés dans une grille:
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);
En HTML5, c'est un peu plus compliqué. Nous utilisons l'élément <canvas>
, une surface de dessin bitmap, pour peindre le plateau de jeu une case après l'autre. La première étape consiste à charger le sprite d'image. Une fois chargée, nous itérons la notation de mise en page, en dessinant une partie différente de l'image à chaque itération:
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;
}
Voici le résultat dans le navigateur Web. Notez que le canevas lui-même comporte une ombre portée CSS:
La conversion de l'objet carte a été un exercice similaire. Dans Flash, nous avons utilisé des champs de texte et des formes vectorielles:
En HTML5, nous combinons trois sprites d'image dans un seul élément <canvas>
au moment de l'exécution:
Nous avons maintenant 100 canevas (un pour chaque carte) et un canevas pour le plateau de jeu. Voici le balisage d'une carte "H" :
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Voici le code CSS correspondant:
.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));
}
Nous appliquons des effets CSS3 lorsque la carte est en cours de glisser-déposer (ombre, opacité et mise à l'échelle) et lorsqu'elle est posée sur le rack (réflexion):
L'utilisation d'images matricielles présente certains avantages évidents. Tout d'abord, le résultat est précis au pixel près. Deuxièmement, ces images peuvent être mises en cache par le navigateur. Troisièmement, avec un peu de travail supplémentaire, nous pouvons remplacer les images pour créer de nouvelles conceptions de cartes (par exemple, une carte en métal). Ce travail de conception peut être effectué dans Photoshop plutôt que dans Flash.
Inconvénient ? En utilisant des images, nous abandonnons l'accès programmatique aux champs de texte. Dans Flash, il était facile de modifier la couleur ou d'autres propriétés du type. En HTML5, ces propriétés sont intégrées aux images elles-mêmes. (Nous avons essayé le texte HTML, mais cela nécessitait beaucoup de balisage et de CSS supplémentaires. Nous avons également essayé le texte du canevas, mais les résultats étaient incohérents d'un navigateur à l'autre.)
Logique floue
Nous voulions exploiter pleinement la fenêtre du navigateur à n'importe quelle taille et éviter le défilement. Il s'agissait d'une opération relativement simple dans Flash, car l'intégralité du jeu était dessinée en vecteurs et pouvait être agrandie ou réduite sans perdre en fidélité. Mais c'était plus compliqué en HTML. Nous avons essayé d'utiliser le scaling CSS, mais nous avons obtenu un canevas flou:
Notre solution consiste à redessiner le plateau de jeu, le rack et les tuiles chaque fois que l'utilisateur redimensionne le navigateur:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
Nous obtenons ainsi des images nettes et des mises en page agréables quelle que soit la taille de l'écran:
Aller à l'essentiel
Étant donné que chaque carte est positionnée de manière absolue et doit s'aligner précisément sur le plateau de jeu et le rack, nous avons besoin d'un système de positionnement fiable. Nous utilisons deux fonctions, Bounds
et Point
, pour gérer l'emplacement des éléments dans l'espace global (la page HTML). Bounds
décrit une zone rectangulaire sur la page, tandis que Point
décrit une coordonnée x,y par rapport à l'angle supérieur gauche de la page (0,0), également appelé point d'enregistrement.
Avec Bounds
, nous pouvons détecter l'intersection de deux éléments rectangulaires (par exemple, lorsqu'une carte croise le rack) ou si une zone rectangulaire (comme un espace à deux lettres) contient un point arbitraire (comme le point central d'une carte). Voici l'implémentation de 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(",");
}
Nous utilisons Point
pour déterminer les coordonnées absolues (coin supérieur gauche) de n'importe quel élément de la page ou d'un événement de souris. Point
contient également des méthodes permettant de calculer la distance et la direction, qui sont nécessaires pour créer des effets d'animation. Voici l'implémentation de 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);
}
Ces fonctions constituent la base des fonctionnalités de glisser-déposer et d'animation. Par exemple, nous utilisons Bounds.intersects()
pour déterminer si une tuile chevauche un espace sur le plateau de jeu, Point.vector()
pour déterminer la direction d'une tuile déplacée et Point.interpolate()
en combinaison avec un minuteur pour créer un tween de mouvement ou un effet d'atténuation.
Je suis le mouvement, en toute fluidité
Bien que les mises en page de taille fixe soient plus faciles à produire en Flash, les mises en page fluides sont beaucoup plus faciles à générer avec HTML et le modèle de boîte CSS. Prenons l'exemple de la vue de grille suivante, dont la largeur et la hauteur sont variables:
ou le panneau de chat. La version Flash nécessitait plusieurs gestionnaires d'événements pour répondre aux actions de la souris, un masque pour la zone à faire défiler, des calculs pour calculer la position de défilement et beaucoup d'autres codes pour les assembler.
En comparaison, la version HTML n'est qu'un <div>
avec une hauteur fixe et la propriété overflow définie sur "hidden". Le défilement ne nous coûte rien.
Dans ce cas (tâches de mise en page ordinaires), HTML et CSS surpassent Flash.
Vous m'entendez maintenant ?
Nous avons eu du mal avec la balise <audio>
. Elle n'était tout simplement pas capable de lire des effets sonores courts de manière répétée dans certains navigateurs. Nous avons essayé deux solutions de contournement. Tout d'abord, nous avons rempli les fichiers audio avec du silence pour les allonger. Nous avons ensuite essayé d'alterner la lecture sur plusieurs canaux audio. Aucune de ces deux techniques n'était complètement efficace ni élégante.
Nous avons finalement décidé de créer notre propre lecteur audio Flash et d'utiliser l'audio HTML5 en cas de problème. Voici le code de base en 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);
En JavaScript, nous essayons de détecter le lecteur Flash intégré. Si cela échoue, nous créons un nœud <audio>
pour chaque fichier 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();
}
}
Notez que cette fonctionnalité ne fonctionne que pour les fichiers MP3. Nous n'avons jamais pris la peine de prendre en charge le format OGG. Nous espérons que le secteur se fixera sur un seul format dans un avenir proche.
Position du sondage
Nous utilisons la même technique en HTML5 qu'en Flash pour actualiser l'état du jeu: toutes les 10 secondes, le client demande au serveur des mises à jour. Si l'état du jeu a changé depuis le dernier sondage, le client reçoit et gère les modifications. Sinon, rien ne se passe. Cette technique de sondage traditionnelle est acceptable, mais pas très élégante. Toutefois, nous souhaitons passer à long polling ou WebSockets à mesure que le jeu évolue et que les utilisateurs s'attendent à une interaction en temps réel sur le réseau. WebSockets, en particulier, offre de nombreuses possibilités d'améliorer le jeu.
Quel outil !
Nous avons utilisé Google Web Toolkit (GWT) pour développer à la fois l'interface utilisateur du frontend et la logique de contrôle du backend (authentification, validation, persistance, etc.). Le code JavaScript lui-même est compilé à partir du code source Java. Par exemple, la fonction Point est adaptée de 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));
}
...
}
Certaines classes d'UI ont des fichiers de modèle correspondants dans lesquels les éléments de page sont "liés" aux membres de la classe. Par exemple, ChatPanel.ui.xml
correspond à 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>
Cet article n'a pas vocation à présenter GWT en détail, mais nous vous invitons à l'utiliser pour votre prochain projet HTML5.
Pourquoi utiliser Java ? Tout d'abord, pour le typage strict. Bien que la saisie dynamique soit utile en JavaScript (par exemple, la capacité d'une matrice à contenir des valeurs de différents types), elle peut être un casse-tête dans les projets volumineux et complexes. Deuxièmement, pour les fonctionnalités de refactoring. Imaginez comment vous pourriez modifier la signature d'une méthode JavaScript dans des milliers de lignes de code. Ce n'est pas facile ! Mais avec un bon IDE Java, c'est un jeu d'enfant. Enfin, à des fins de test. Écrire des tests unitaires pour des classes Java est préférable à la technique éprouvée consistant à "enregistrer et actualiser".
Résumé
À l'exception de nos problèmes audio, HTML5 a largement dépassé nos attentes. Wordico est aussi beau que dans Flash, mais il est aussi fluide et réactif. Nous n'aurions pas pu y arriver sans Canvas et CSS3. Notre prochain défi: adapter Wordico aux mobiles.