Wprowadzenie
Gdy przekształcaliśmy grę w krzyżówki Wordico z Flasha w HTML5, pierwszym zadaniem było zapomnienie wszystkiego, co wiedzieliśmy o tworzeniu bogatego środowiska użytkownika w przeglądarce. Podczas gdy Flash oferował jeden kompleksowy interfejs API do wszystkich aspektów tworzenia aplikacji – od rysowania wektorów po wykrywanie kolizji wielokątów i analizowanie kodu XML – HTML5 oferował mieszankę specyfikacji z różną obsługą w przeglądarkach. Zastanawialiśmy się też, czy HTML, język przeznaczony do tworzenia dokumentów, i CSS, język przeznaczony do tworzenia elementów, nadają się do tworzenia gier. Czy gra będzie wyświetlać się w różnych przeglądarkach tak samo jak w Flashu? Czy będzie wyglądać i działać tak samo? W przypadku Wordico odpowiedź brzmiała tak.
Jaki jest Twój wektor, Victor?
Pierwotna wersja Wordico została opracowana wyłącznie przy użyciu grafiki wektorowej: linii, krzywych, wypełnień i gradientów. W efekcie powstała bardzo zwarta i nieskończenie skalowalna platforma:
Korzystaliśmy też z osi czasu Flash, aby tworzyć obiekty o różnych stanach. Na przykład w przypadku obiektu Space
użyliśmy 9 nazwanych klatek kluczowych:
W HTML5 używamy jednak sprite’a bitmapowego:
Aby utworzyć planszeszę 15 x 15 z pojedynczych pól, iterujemy po ciągu znaków o długości 225 znaków, w którym każde pole jest reprezentowane przez inny znak (np. „t” dla potrójnej litery i „T” dla potrójnego słowa). W programie Flash było to proste: wystarczyło ustawić miejsca i ułożyć je w siatkową siatkę:
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);
W HTML5 jest to nieco bardziej skomplikowane. Do malowania planszy gry pojedynczymi kwadratami używamy elementu <canvas>
, który jest bitmapową powierzchnią do rysowania. Pierwszym krokiem jest załadowanie sprite’a obrazu. Po jego załadowaniu przechodzimy przez notację układu, za każdym razem rysując inną część obrazu:
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;
}
Oto wynik w przeglądarce. Pamiętaj, że sama kanwa ma cień padający CSS:
Konwertowanie obiektu płytki było podobnym ćwiczeniem. W programie Flash użyliśmy pól tekstowych i kształtów wektorowych:
W HTML5 łączymy 3 obrazy sprite’ów w jeden element <canvas>
w czasie wykonywania:
Teraz mamy 100 płót (po jednym dla każdej płytki) oraz płótno planszy do gry. Oto znacznik dla kafelka „H”:
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Oto odpowiedni kod 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));
}
Efekty CSS3 stosujemy, gdy kafelek jest przeciągany (cień, krycie i powiększanie) oraz gdy jest umieszczony na ruszcie (odbicie):
Korzystanie z obrazów rastrowych ma kilka oczywistych zalet. Po pierwsze, wynik jest dokładny do piksela. Po drugie, obrazy mogą być przechowywane w pamięci podręcznej przeglądarki. Po trzecie, przy odrobinie dodatkowej pracy możemy zastąpić obrazy, aby stworzyć nowe wzory płytek, np. metalową, a ta praca projektowa może zostać wykonana w Photoshopie zamiast w Flashu.
Minusy? Korzystając z obrazów, rezygnujemy z dostępu programowego do pól tekstowych. W Flash zmiana koloru lub innych właściwości typu była prostą operacją. W HTML5 te właściwości są wbudowane w same obrazy. (próbowaliśmy użyć tekstu HTML, ale wymagało to dużo dodatkowego znacznika i CSS. Spróbowaliśmy też użyć tekstu na kanwie, ale wyniki były niespójne w różnych przeglądarkach.)
logika rozmyta;
Chcieliśmy w pełni wykorzystać okno przeglądarki w dowolnym rozmiarze i uniknąć przewijania. Była to stosunkowo prosta operacja w Flashu, ponieważ cała gra została narysowana w wektora i można było ją powiększyć lub pomniejszyć bez utraty jakości. W HTML było to jednak trudniejsze. Spróbowaliśmy użyć skalowania CSS, ale uzyskaliśmy rozmyty obraz:
Nasze rozwiązanie polega na ponownym narysowaniu planszy, stojaków i płytek za każdym razem, gdy użytkownik zmienia rozmiar przeglądarki:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
W efekcie otrzymujemy ostre obrazy i przyjemne układy na dowolnym rozmiarze ekranu:
Pisz na temat
Ponieważ każda płytka jest umieszczona w ściśle określonym miejscu i musi być dokładnie dopasowana do planszy i stojaka, potrzebujemy niezawodnego systemu pozycjonowania. Do zarządzania lokalizacją elementów w przestrzeni globalnej (stronie HTML) używamy 2 funkcji: Bounds
i Point
. Bounds
określa prostokątny obszar na stronie, a Point
współrzędną x,y względem lewego górnego rogu strony (0,0), zwaną też punktem rejestracji.
Dzięki Bounds
możemy wykrywać przecięcie 2 prostokątnych elementów (np. gdy kafelek przecina ruszt) lub sprawdzać, czy prostokątny obszar (np. miejsce na 2 litery) zawiera dowolny punkt (np. środkowy punkt kafelka). Oto implementacja 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(",");
}
Używamy Point
, aby określić bezwzględne współrzędne (lewy górny róg) dowolnego elementu na stronie lub zdarzenia myszy. Point
zawiera też metody obliczania odległości i kierunku, które są niezbędne do tworzenia efektów animacji. Oto implementacja funkcji 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);
}
Te funkcje stanowią podstawę możliwości przeciągania i upuszczania oraz animacji. Na przykład używamy funkcji Bounds.intersects()
, aby określić, czy kafelek zachodzi na pole na planszy, używamy funkcji Point.vector()
, aby określić kierunek przeciąganego kafelka, a funkcję Point.interpolate()
w połączeniu z timerem, aby utworzyć animację przejścia lub efekt łagodnego przejścia.
Płynę z prądem
Układy o stałym rozmiarze łatwiej jest tworzyć w Flashu, ale układy elastyczne łatwiej generować za pomocą HTML i modelu pola CSS. Weź pod uwagę ten widok siatki o zmiennej szerokości i wysokości:
Możesz też skorzystać z panelu czatu. Wersja w Flashu wymagała wielu obciążających procesor metod obsługi zdarzeń, aby reagować na działania myszy, maski dla obszaru przewijania, obliczeń do obliczania pozycji przewijania oraz wielu innych fragmentów kodu, które miały je połączyć.
Wersja HTML to tylko element <div>
o stałej wysokości i z właściwością overflow ustawioną na „hidden” (ukryty). Przewijanie nie wiąże się z żadnymi kosztami.
W takich przypadkach, gdy chodzi o zwykłe zadania związane z układem, HTML i CSS wygrywają z Flashem.
Czy teraz mnie słyszysz?
Mieliśmy problemy z tagiem <audio>
, ponieważ w niektórych przeglądarkach nie można było odtwarzać krótkich efektów dźwiękowych wielokrotnie. Wypróbowaliśmy 2 rozwiązania. Najpierw wypełniliśmy pliki dźwiękowe pustymi fragmentami, aby wydłużyć je w czasie. Następnie spróbowaliśmy odtwarzania naprzemiennego na kilku kanałach audio. Żadna z tych technik nie była całkowicie skuteczna ani elegancka.
Ostatecznie zdecydowaliśmy się wdrożyć własny odtwarzacz dźwięku Flash i użyć dźwięku w formacie HTML5 jako alternatywy. Oto podstawowy kod w 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);
W JavaScriptzie próbujemy wykryć wbudowany odtwarzacz Flash. Jeśli to się nie uda, dla każdego pliku dźwiękowego tworzymy węzeł <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();
}
}
Pamiętaj, że ta funkcja działa tylko w przypadku plików MP3 – nigdy nie zawracaliśmy sobie głowy obsługą formatu OGG. Mamy nadzieję, że w najbliższej przyszłości branża ustali jeden format.
Pozycja ankiety
Aby odświeżać stan gry, używamy w HTML5 tej samej techniki co w Flashu: co 10 sekund klient prosi serwer o aktualizacje. Jeśli stan gry zmienił się od ostatniego zapytania, klient otrzyma i przetworzy zmiany. W przeciwnym razie nic się nie stanie. Ta tradycyjna technika ankietowania jest akceptowalna, choć niezbyt elegancka. Chcemy jednak przejść na długie odpytywanie lub WebSockets, ponieważ gra się rozwija, a użytkownicy oczekują interakcji w czasie rzeczywistym przez sieć. WebSockets stwarzają wiele możliwości ulepszenia rozgrywki.
To świetne narzędzie.
Do tworzenia interfejsu użytkownika i logiki sterowania na zapleczu (uwierzytelniania, walidacji, trwałości itp.) użyliśmy pakietu Google Web Toolkit (GWT). Sam kod JavaScript jest kompilowany z kodu źródłowego Java. Na przykład funkcja Punkt jest dostosowana z funkcji 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));
}
...
}
Niektóre klasy interfejsu użytkownika mają odpowiadające im pliki szablonów, w których elementy strony są „powiązane” z elementami klasy. Na przykład ChatPanel.ui.xml
odpowiada 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>
Pełne informacje wykraczają poza zakres tego artykułu, ale zachęcamy do skorzystania z GWT w przyszłych projektach HTML5.
Dlaczego warto używać Javy? Po pierwsze, ścisła typizacja. Typowanie dynamiczne jest przydatne w JavaScriptie, np. umożliwia tablicy przechowywanie wartości różnych typów, ale może być uciążliwe w przypadku dużych, złożonych projektów. Po drugie, możliwości refaktoryzacji. Pomyśl, jak zmienić sygnaturę metody JavaScript w tysiącach linii kodu – nie jest to łatwe. Ale przy użyciu dobrego środowiska IDE w języku Java jest to bardzo proste. Na koniec, do celów testowych. Pisanie testów jednostkowych dla klas Java jest lepsze niż tradycyjna metoda „zapisz i odśwież”.
Podsumowanie
Poza problemami z dźwiękiem, HTML5 znacznie przekroczył nasze oczekiwania. Wordico nie tylko wygląda tak samo dobrze jak w Flashu, ale jest też tak samo płynne i szybkie. Bez Canvasa i CSS3 nie udałoby nam się tego osiągnąć. Naszym kolejnym wyzwaniem jest dostosowanie Wordico do urządzeń mobilnych.