Einführung
Als wir unser Wordico-Kreuzworträtsel von Flash zu HTML5 umgestellt haben, mussten wir zuerst alles vergessen, was wir über die Erstellung einer ansprechenden Benutzeroberfläche im Browser wussten. Während Flash eine einzige umfassende API für alle Aspekte der Anwendungsentwicklung bot – von der Vektorzeichnung über die Polygon-Treffererkennung bis hin zum XML-Parsing –, bot HTML5 eine Vielzahl von Spezifikationen mit unterschiedlicher Browserunterstützung. Außerdem haben wir uns gefragt, ob HTML, eine dokumentspezifische Sprache, und CSS, eine boxorientierte Sprache, für die Entwicklung eines Spiels geeignet sind. Würde das Spiel wie in Flash in allen Browsern einheitlich angezeigt werden und würde es genauso gut aussehen und funktionieren? Für Wordico lautete die Antwort ja.
Was ist Ihr Vektor, Viktor?
Wir haben die ursprüngliche Version von Wordico ausschließlich mit Vektorgrafiken entwickelt: Linien, Kurven, Füllungen und Farbverläufe. Das Ergebnis war sowohl sehr kompakt als auch unbegrenzt skalierbar:
Außerdem haben wir die Flash-Zeitleiste genutzt, um Objekte mit mehreren Status zu erstellen. Für das Objekt Space
haben wir beispielsweise neun benannte Frames verwendet:
In HTML5 verwenden wir jedoch einen Bitmapped-Sprite:
Um das 15 × 15 Felder große Spielbrett aus einzelnen Feldern zu erstellen, iterieren wir über eine 225-stellige Stringnotation, in der jedes Feld durch ein anderes Zeichen dargestellt wird (z. B. „t“ für dreifache Buchstaben und „T“ für dreifache Wörter). In Flash war das ganz einfach: Wir haben einfach Bereiche erstellt 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. Wir verwenden das <canvas>
-Element, eine Bitmap-Zeichenfläche, um das Spielbrett nach und nach zu zeichnen. Laden Sie zuerst den Bild-Sprite. Nach dem Laden durchlaufen wir die Layoutnotation und zeichnen bei jeder Iteration einen anderen Teil des Bilds:
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;
}
Das Ergebnis im Webbrowser: Der Canvas selbst hat einen CSS-Schatten:
Die Umwandlung des Kachelobjekts war eine ähnliche Übung. In Flash haben wir Textfelder und Vektorformen verwendet:
In HTML5 kombinieren wir drei Bild-Sprites zur Laufzeit in einem einzigen <canvas>
-Element:
Jetzt haben wir 100 Canvasse (jeweils eines für jede Kachel) sowie ein Canvas für das Spielbrett. 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 sie sich im Rack befindet (Reflexion):
Die Verwendung von Rasterbildern hat einige offensichtliche Vorteile. Erstens: Das Ergebnis ist pixelgenau. Zweitens können diese Bilder vom Browser im Cache gespeichert werden. Drittens: Mit ein wenig zusätzlicher Arbeit können wir die Bilder austauschen, um neue Fliesendesigns zu erstellen, z. B. eine Metallfliese. Diese Designarbeit kann in Photoshop statt in Flash erfolgen.
Der Nachteil? Wenn wir Bilder verwenden, verzichten wir auf den programmatischen Zugriff auf die Textfelder. In Flash war es einfach, die Farbe oder andere Eigenschaften des Typs zu ändern. In HTML5 sind diese Eigenschaften in die Bilder selbst eingebettet. Wir haben es mit HTML-Text versucht, aber das erforderte viel zusätzliches Markup und CSS. Wir haben auch Canvas-Text ausprobiert, aber die Ergebnisse waren in verschiedenen Browsern nicht einheitlich.)
Fuzzy-Logik
Wir wollten das Browserfenster in jeder Größe optimal nutzen und Scrollen vermeiden. Das war in Flash relativ einfach, da das gesamte Spiel in Vektoren gezeichnet wurde und ohne Qualitätsverlust vergrößert oder verkleinert werden konnte. In HTML war es jedoch schwieriger. Wir haben versucht, die CSS-Skalierung zu verwenden, aber das Canvas wurde unscharf:
Unsere Lösung besteht darin, das Spielfeld, das Rack und die Kacheln 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 bei jeder Bildschirmgröße gestochen scharfe Bilder und ansprechende Layouts:
Schnell zum Punkt kommen
Da jede Kachel absolut positioniert ist und genau mit dem Spielbrett und dem Rack ausgerichtet werden muss, benötigen wir ein zuverlässiges Positionierungssystem. Wir verwenden zwei 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, auch als Registrierpunkt bezeichnet.
Mit Bounds
können wir die Überschneidung zweier rechteckiger Elemente erkennen (z. B. wenn eine Kachel das Rack kreuzt) oder ob ein rechteckiger Bereich (z. B. ein Bereich mit zwei Buchstaben) einen beliebigen Punkt (z. B. den Mittelpunkt einer Kachel) enthält. 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(",");
}
Mit Point
wird die absolute Koordinate (links oben) eines Elements auf der Seite oder eines Mausereignisses ermittelt. Point
enthält auch 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 für Drag-and-drop- und Animationsfunktionen. Mit Bounds.intersects()
können wir beispielsweise feststellen, ob eine Kachel einen Bereich auf dem Spielbrett überlappt. Mit Point.vector()
können wir die Richtung einer gezogenen Kachel bestimmen. Und mit Point.interpolate()
in Kombination mit einem Timer können wir einen Motion-Tween oder einen Ease-Effekt erstellen.
Mit dem Strom schwimmend
Layouts mit fester Größe lassen sich zwar einfacher in Flash erstellen, aber mit HTML und dem CSS-Box-Modell lassen sich flexible Layouts viel einfacher generieren. Sehen Sie sich die folgende Ansicht mit variabler Breite und Höhe an:
Oder das Chatfeld. Die Flash-Version erforderte mehrere Ereignishandler, um auf Mausaktionen zu reagieren, eine Maske für den scrollbaren Bereich, Mathematik zum Berechnen der Scrollposition und viel anderen Code, um alles zusammenzufügen.
Die HTML-Version besteht dagegen nur aus einem <div>
mit einer festen Höhe und der Eigenschaft „overflow“ (Überlauf) auf „hidden“ (ausgeblendet). Das Scrollen kostet uns nichts.
In solchen Fällen – bei einfachen Layoutaufgaben – sind HTML und CSS die bessere Wahl.
Können Sie mich jetzt hören?
Wir hatten Probleme mit dem <audio>
-Tag. In bestimmten Browsern konnten kurze Audioeffekte nicht wiederholt abgespielt werden. Wir haben zwei Lösungen ausprobiert. Zuerst haben wir die Audiodateien mit Stille gefüllt, um sie zu verlängern. Dann haben wir versucht, die Wiedergabe auf mehrere Audiokanäle aufzuteilen. Keine der beiden Techniken war vollständig effektiv oder elegant.
Letztendlich haben wir uns entschieden, unseren eigenen Flash-Audioplayer zu entwickeln 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 versuchen wir, den eingebetteten Flash-Player zu erkennen. Andernfalls erstellen wir für jede Audiodatei einen <audio>
-Knoten:
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: Das 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 einziges Format einigen wird.
Umfrageposition
Wir verwenden in HTML5 dieselbe Technik wie in Flash, um den Spielstatus zu aktualisieren: Alle 10 Sekunden fragt der Client den Server nach Updates. Wenn sich der Spielstatus seit der letzten Umfrage geändert hat, empfängt und verarbeitet der Client die Änderungen. Andernfalls passiert nichts. Diese traditionelle Umfragetechnik ist akzeptabel, wenn auch nicht ganz elegant. Wir möchten jedoch zu Long Polling oder WebSockets wechseln, sobald das Spiel ausgereift ist und Nutzer Echtzeitinteraktionen über das Netzwerk erwarten. Insbesondere WebSockets bieten viele Möglichkeiten, das Gameplay zu verbessern.
Was für ein Tool!
Wir haben das Google Web Toolkit (GWT) verwendet, um sowohl die Front-End-Benutzeroberfläche als auch die Back-End-Steuerungslogik (Authentifizierung, Validierung, Persistenz usw.) zu entwickeln. Das JavaScript selbst wird aus Java-Quellcode kompiliert. Die Funktion „Punkt“ wurde beispielsweise von Point.java
übernommen:
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 Klassenmitglieder 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>
Die vollständigen Details gehen über den Rahmen dieses Artikels hinaus. Wir empfehlen Ihnen jedoch, GWT für Ihr nächstes HTML5-Projekt zu verwenden.
Vorteile von Java Erstens: die strenge Typisierung. Die dynamische Typisierung ist in JavaScript zwar nützlich, z. B. weil ein Array Werte verschiedener Typen enthalten kann, bei großen, komplexen Projekten kann sie jedoch zu Problemen führen. Zweitens: Refactoring-Funktionen. Stellen Sie sich vor, wie Sie eine JavaScript-Methodensignatur in Tausenden von Codezeilen ändern würden – das ist nicht einfach! Mit einer guten Java-IDE ist das jedoch ein Kinderspiel. Schließlich zu Testzwecken. Das Schreiben von Unit-Tests für Java-Klassen ist besser als die bewährte Methode „Speichern und aktualisieren“.
Zusammenfassung
Abgesehen von den Audioproblemen hat HTML5 unsere Erwartungen bei weitem übertroffen. Wordico sieht nicht nur genauso gut aus wie in Flash, sondern ist auch genauso flüssig und reaktionsschnell. Ohne Canvas und CSS3 wäre das nicht möglich gewesen. Unsere nächste Herausforderung: Wordico für die mobile Nutzung anpassen.