Einleitung
Bei der Umwandlung unseres Wordico-Kreuzworträtselspiels von Flash in HTML5 bestand unsere erste Aufgabe darin, alles zu verstehen, was wir über die Erstellung einer ansprechenden Nutzererfahrung im Browser wussten. Während Flash eine einzelne, umfassende API für alle Aspekte der Anwendungsentwicklung bot – von der Vektorzeichnung über die Erkennung von Polygontreffern bis hin zur XML-Analyse, bot HTML5 eine Unmenge an Spezifikationen mit unterschiedlicher Browserunterstützung. Außerdem haben wir uns gefragt, ob HTML, eine dokumentenspezifische Sprache, und CSS, eine kastenzentrierte Sprache, zum Erstellen eines Spiels geeignet sind. Würde das Spiel in allen Browsern einheitlich dargestellt werden wie in Flash und würde es auch genauso gut aussehen und funktionieren? Für Wordico lautete die Antwort ja.
Welchen Vektor hast du, Victor?
Wir entwickelten die ursprüngliche Version von Wordico ausschließlich mithilfe von Vektorgrafiken: Linien, Kurven, Füllungen und Farbverläufe. Das Ergebnis war sowohl hochkompakt als auch unendlich skalierbar:
Außerdem haben wir die Flash-Zeitachse genutzt, um Objekte mit mehreren Status zu erstellen. Wir haben beispielsweise neun benannte Keyframes für das Space
-Objekt verwendet:
In HTML5 verwenden wir jedoch ein Bitmap-Sprite:
Um das 15 x 15-Gameboard aus einzelnen Bereichen zu erstellen, iterieren wir über eine 225-stellige Zeichenfolgennotation, in der jedes Leerzeichen durch ein anderes Zeichen dargestellt wird (z. B. "t" für das Dreifachbuchstaben und "T" für ein Dreifachwort). Dies war eine unkomplizierte Operation in Flash. Wir haben einfach Leerzeichen ausgestanzt und in einem Raster angeordnet:
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 ist das etwas komplizierter. Mit dem <canvas>
-Element, einer Bitmap-Zeichenfläche, wird das Spielfeld quadratisch bemalt. Der erste Schritt besteht darin, das Bild-Sprite zu laden. Nach dem Laden durchlaufen wir die Layoutnotation und zeichnen mit jeder Iteration einen anderen Teil des Bildes:
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;
}
Hier ist das Ergebnis im Webbrowser. Der Canvas selbst verfügt über einen CSS-Schlagschatten:
Das Konvertieren des Kachelobjekts war eine ähnliche Übung. In Flash wurden Textfelder und Vektorformen verwendet:
In HTML5 werden zur Laufzeit drei Bild-Sprites in einem einzelnen <canvas>
-Element kombiniert:
Jetzt haben wir 100 Leinwand (eine für jede Kachel) und einen Leinwanddruck für das Spielfeld. Hier ist das Markup für eine „H“-Kachel:
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Hier ist das entsprechende CSS:
.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));
}
Wir wenden CSS3-Effekte an, wenn die Kachel gezogen wird (Schatten, Deckkraft und Skalierung) und wenn die Kachel auf dem Rack liegt (Reflexion):
Die Verwendung von Rasterbildern hat einige offensichtliche Vorteile. Das Ergebnis ist pixelgenau. Zweitens können diese Bilder vom Browser im Cache gespeichert werden. Drittens können wir mit ein wenig Zusatzaufwand die Bilder austauschen, um neue Kacheldesigns zu erstellen – zum Beispiel eine Metallkachel. Diese Designarbeit können Sie in Photoshop statt in Flash erledigen.
Der Nachteil? Durch die Verwendung von Bildern gewähren wir programmatischen Zugriff auf die Textfelder. In Flash war es ein einfacher Vorgang, die Farbe oder andere Eigenschaften des Typs zu ändern. In HTML5 sind diese Eigenschaften in die Bilder selbst integriert. Wir haben HTML-Text ausprobiert, allerdings erforderte er viel Markup und CSS. Wir haben auch Canvas-Text-Elemente ausprobiert, aber die Ergebnisse waren in allen Browsern unterschiedlich.)
Unscharfe Logik
Wir wollten das Browserfenster in jeder Größe optimal nutzen und Scrollen vermeiden. Dies war eine relativ einfache Operation in Flash, da das gesamte Spiel in Vektoren gezeichnet wurde und ohne Einbußen in der Grafik vergrößert oder verkleinert werden konnte. In HTML war es etwas komplizierter. Wir haben versucht, die CSS-Skalierung zu verwenden, doch schließlich war der Canvas verschwommen:
Unsere Lösung besteht darin, das Spielfeld, das Rack und die Kacheln jedes Mal neu zu zeichnen, wenn der Nutzer die Größe des Browsers ändert:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
So erhalten wir gestochen scharfe Bilder und ansprechende Layouts für jede Bildschirmgröße:
Kommen Sie auf den Punkt
Da jede Kachel absolut positioniert ist und genau auf dem Spielbrett und dem Rack ausgerichtet werden muss, benötigen wir ein zuverlässiges Positionierungssystem. Wir verwenden die beiden Funktionen Bounds
und Point
, um die Position von Elementen im globalen Bereich (der HTML-Seite) zu verwalten. Bounds
beschreibt einen rechteckigen Bereich auf der Seite, während Point
eine x,y-Koordinate relativ zur oberen linken Ecke der Seite (0,0) beschreibt, die auch als Registrierungspunkt bezeichnet wird.
Mit Bounds
können wir die Schnittmenge von zwei rechteckigen Elementen erkennen (z. B. wenn eine Kachel das Rack kreuzt) oder ob ein rechteckiger Bereich (z. B. ein Raum mit zwei Buchstaben) einen beliebigen Punkt enthält (z. B. den Mittelpunkt einer Kachel). Hier ist die Implementierung von 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(",");
}
Wir verwenden Point
, um die absolute Koordinate (oben links) eines beliebigen Elements auf der Seite oder eines Mausereignisses zu bestimmen. Point
enthält außerdem Methoden zum Berechnen von Entfernung und Richtung, die für die Erstellung von Animationseffekten erforderlich sind. Hier ist die Implementierung von 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);
}
Diese Funktionen bilden die Grundlage der Drag-and-drop- und Animationsfunktionen. Zum Beispiel verwenden wir Bounds.intersects()
, um zu bestimmen, ob eine Kachel einen Bereich auf dem Spielfeld überschneidet. Wir verwenden Point.vector()
, um die Richtung einer gezogenen Kachel zu bestimmen, und wir verwenden Point.interpolate()
in Kombination mit einem Timer, um einen Motion Tween, einen Easing-Effekt, zu erzeugen.
Mit dem Strom schwimmend
Während Layouts mit fester Größe in Flash einfacher zu erstellen sind, lassen sich fließende Layouts mit HTML und dem CSS-Boxmodell viel einfacher generieren. Betrachten Sie die folgende Rasteransicht mit variabler Breite und Höhe:
Oder ziehen Sie das Chatfeld in Betracht. Bei der Flash-Version waren mehrere Event-Handler erforderlich, um auf Mausaktionen zu reagieren, eine Maske für den scrollbaren Bereich, Berechnungen für die Berechnung der Scrollposition und eine Menge anderer Code, um sie zusammenzukleben.
Die HTML-Version ist im Vergleich dazu nur ein <div>
mit fester Höhe und der Überlauf-Eigenschaft, die auf „Ausgeblendet“ gesetzt ist. Scrollen kostet uns nichts.
In solchen Fällen, also bei gewöhnlichen Layoutaufgaben, liegt HTML und CSS vor dem Flash.
Können Sie mich jetzt hören?
Wir hatten Schwierigkeiten mit dem <audio>
-Tag. Es war in bestimmten Browsern einfach nicht in der Lage, kurze Soundeffekte wiederholt abzuspielen. Wir haben zwei Behelfslösungen ausprobiert. Zuerst haben wir die Sounddateien mit toten Luft aufgefüllt, um sie länger zu machen. Dann haben wir versucht, die abwechselnde Wiedergabe über mehrere Audiokanäle hinweg durchzuführen. Keine der beiden Methoden war absolut effektiv oder elegant.
Schließlich beschlossen wir, unseren eigenen Flash-Audio-Player einzuführen und HTML5-Audio als Fallback zu verwenden. Hier ist der grundlegende Code 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 wird versucht, den eingebetteten Flash-Player zu finden. Wenn das nicht funktioniert, erstellen wir einen <audio>
-Knoten für jede Audiodatei:
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();
}
}
Hinweis: Dies funktioniert nur für MP3-Dateien. Wir haben uns nie die Mühe gemacht, OGG zu unterstützen. Wir hoffen, dass sich die Branche in naher Zukunft auf ein einheitliches Format einigen wird.
Abstimmungsposition
In HTML5 nutzen wir dieselbe Technik wie in Flash, um den Spielstatus zu aktualisieren: Alle zehn Sekunden fordert der Client den Server nach Aktualisierungen an. Wenn sich der Spielstatus seit der letzten Umfrage geändert hat, empfängt und verarbeitet der Client die Änderungen. Andernfalls passiert nichts. Diese traditionelle Abfragetechnik ist akzeptabel, wenn auch nicht ganz elegant. Wir würden jedoch gerne auf Long Polling oder WebSockets umstellen, wenn das Spiel weiterentwickelt wird und die Nutzer Echtzeitinteraktionen über das Netzwerk erwarten. Insbesondere WebSockets bietet viele Möglichkeiten, das Spiel zu verbessern.
Was für ein Tool!
Wir haben mit dem Google Web Toolkit (GWT) sowohl die Frontend-Benutzeroberfläche als auch die Backend-Steuerlogik (Authentifizierung, Validierung, Persistenz usw.) entwickelt. Das JavaScript selbst wird aus dem Java-Quellcode kompiliert. Die Punktfunktion wird beispielsweise von Point.java
angepasst:
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));
}
...
}
Einige UI-Klassen haben entsprechende Vorlagendateien, in denen Seitenelemente an Kursmitglieder gebunden sind. ChatPanel.ui.xml
entspricht beispielsweise 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>
Sämtliche Details werden in diesem Artikel nicht behandelt, aber wir empfehlen Ihnen, sich für Ihr nächstes HTML5-Projekt mit GWT vertraut zu machen.
Warum Java? Zunächst zur strikten Eingabe. Während die dynamische Eingabe in JavaScript nützlich ist, z. B. weil ein Array Werte verschiedener Typen enthalten kann, kann dies bei großen, komplexen Projekten zu einem Problem werden. Zweitens zu den Refaktorierungsfunktionen. Überlegen Sie, wie Sie die Signatur einer JavaScript-Methode über Tausende Codezeilen hinweg ändern würden – und zwar nicht so einfach! Mit einer guten Java-IDE ist das ein Kinderspiel. Zu Testzwecken. Das Schreiben von Einheitentests für Java-Klassen schlägt die bewährte Technik „Speichern und aktualisieren“.
Zusammenfassung
Mit Ausnahme unserer Audioprobleme hat HTML5 unsere Erwartungen deutlich übertroffen. Wordico sieht nicht nur gut aus wie in Flash, es ist auch genauso flüssig und responsiv. Ohne Canvas und CSS3 wäre das nicht möglich gewesen. Unsere nächste Herausforderung besteht darin, Wordico für die mobile Nutzung anzupassen.