Wstęp
Gdy przekonwertowaliśmy naszą krzyżówkę Wordico z Flasha na HTML5, naszym pierwszym zadaniem było uświadomienie sobie wszystkiego o tym, jak zwiększyć wygodę użytkowników w przeglądarce. Choć Flash oferował pojedynczy, wszechstronny interfejs API do obsługi wszystkich aspektów tworzenia aplikacji – od rysowania wektorowego przez wykrywanie trafień w postaci wielokątów po analizowanie XML – język HTML5 udostępniał wiele specyfikacji z różnymi opcjami obsługi przeglądarek. Zastanawialiśmy się też, czy język HTML (czyli język związany z dokumentami) i CSS (czyli język zorientowany na pudełka) nadają się do tworzenia gier. Czy gra będzie się wyświetlać tak samo w różnych przeglądarkach, jak we Flashu, oraz czy będzie wyglądać i działać tak samo dobrze? W przypadku Wordico odpowiedź brzmi: tak.
Jaki jest Twój wektor, Victor?
Opracowaliśmy oryginalną wersję Wordico tylko przy użyciu grafiki wektorowej: linii, krzywych, wypełnienia i gradientów. Wynik był bardzo kompaktowy i skalowalny w nieskończoność:
Skorzystaliśmy też z osi czasu Flasha, aby utworzyć obiekty mające wiele stanów. Na przykład użyliśmy 9 nazwanych klatek kluczowych dla obiektu Space
:
Natomiast w HTML5 używamy sprite’a mapowanego na bitach:
Aby utworzyć planszę 15 x 15 z pojedynczych obszarów, stosujemy iterację obejmującą 225-znakowy zapis tekstowy, w którym każda spacja jest reprezentowana przez inny znak (na przykład „t” oznacza potrójną literę i „T” dla potrójnego wyrazu). To proste zadanie we Flashu – wystarczy zaznaczyć miejsca i ułożyć je w 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. Używamy elementu <canvas>
, czyli powierzchni do rysowania bitmapami, do malowania planszy gier pojedynczo. Pierwszym krokiem jest wczytanie sprite’a obrazu. Po załadowaniu obrazu powtarzamy zapis układu, rysując przy każdej iteracji 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. Zwróć uwagę, że sam obszar roboczy zawiera cień CSS:
Konwertowanie obiektu kafelkowego było podobnym ćwiczeniem. We Flashu używaliśmy pól tekstowych i kształtów wektorowych:
W HTML5 łączymy 3 sprite'y graficzne w jednym elemencie <canvas>
w czasie działania:
Mamy teraz 100 odbitek na płótnie (po jednym na każdy kafelek) oraz przestrzeń na planszy. Oto znaczniki dla kafelka „H”:
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Oto odpowiadająca jej usługa porównywania cen:
.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 są stosowane, gdy kafelek jest przeciągany (cień, nieprzezroczystość i skalowanie) oraz gdy znajduje się on na stojaku (odbicie):
Stosowanie obrazów rastrowych ma pewne oczywiste zalety. Po pierwsze, wynik jest pikselowy. Po drugie, obrazy te mogą być zapisywane w pamięci podręcznej przeglądarki. Po trzecie, przy odrobinie pracy możemy zamienić je na nowe projekty płytek – na przykład metalowej płytki. Takie prace można wykonać w programie Photoshop zamiast we Flashu.
Jakie są wady? Stosując obrazy, rezygnujemy z automatycznego dostępu do pól tekstowych. We Flashu zmiana koloru i innych właściwości typu kreacji była prosta – w HTML5 te właściwości są wbudowane w obrazy. (Próbowaliśmy użyć tekstu HTML, ale wymagało to dużo dodatkowych znaczników i kodu CSS. Próbowaliśmy też użyć tekstu na płótnie, ale wyniki były niespójne w różnych przeglądarkach).
Logika przybliżona
Chcieliśmy w pełni wykorzystać możliwości okna przeglądarki niezależnie od rozmiaru i uniknąć przewijania. Operacja we Flashu była stosunkowo prosta, ponieważ cała gra była narysowana wektorami i można było skalować obraz w górę lub w dół bez utraty jakości. Jednak kod HTML był trudniejszy. Próbowaliśmy zastosować skalowanie CSS, ale w rezultacie otrzymaliśmy rozmytą przestrzeń roboczą:
Nasze rozwiązanie polega na ponownym rysowaniu planszy, stojaka i kafelków za każdym razem, gdy użytkownik zmieni rozmiar przeglądarki:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
Dzięki temu otrzymujesz wyraźne obrazy i ładne układy na ekranach dowolnego rozmiaru.
Przejdź do rzeczy
Ponieważ każdy kafelek jest absolutnie zlokalizowany i musi ściśle przylegać do planszy oraz stojaka, potrzebujemy niezawodnego systemu pozycjonowania. Używamy 2 funkcji: Bounds
i Point
, które ułatwiają zarządzanie lokalizacją elementów w przestrzeni globalnej (stronie HTML). Bounds
opisuje prostokątny obszar na stronie, a Point
– współrzędne x,y względem lewego górnego rogu strony (0,0), nazywane też punktem rejestracyjnym.
Funkcja Bounds
pozwala wykryć przecięcie 2 prostokątnych elementów (na przykład gdy płytka przecina szafę) lub ustalić, czy prostokątny obszar (na przykład dwuliterowa przestrzeń) zawiera dowolny punkt (na przykład punkt środkowy kafelka). Oto implementacja granic:
// 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(",");
}
Parametr Point
określa współrzędną bezwzględną (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 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 są podstawą do przeciągania i upuszczania oraz tworzenia animacji. Na przykład używamy Bounds.intersects()
, aby określić, czy kafelek nakłada się na przestrzeń na planszy, Point.vector()
do określenia kierunku przeciągniętego kafelka. Używamy funkcji Point.interpolate()
w połączeniu z licznikiem czasu, aby uzyskać efekt wygładzania lub animacji.
Płynę z prądem
Chociaż układy o stałym rozmiarze łatwiej jest tworzyć we Flashu, układy płynne można znacznie łatwiej wygenerować za pomocą kodu HTML i modelu pola CSS. Na przykład widok siatki o zmiennej szerokości i wysokości:
Możesz też przejść do panelu czatu. Wersja Flash wymagała wielu modułów obsługi zdarzeń reagowania na działania myszy, maski dla obszaru przewijanego, funkcji matematycznych do obliczania pozycji przewijania i dużej ilości innego kodu do łączenia elementów w całość.
Dla porównania wersja HTML to po prostu element <div>
o stałej wysokości i ukrytą właściwość overflow. Przewijanie nic nie kosztuje.
W takich przypadkach są to zwykłe zadania związane z układem – HTML i CSS wyróżniają się Flash.
Słyszysz mnie teraz?
Mieliśmy problemy z tagiem <audio>
– w niektórych przeglądarkach nie mógł on po prostu odtwarzać krótkich efektów dźwiękowych w niektórych przeglądarkach. Próbowaliśmy obejść ten problem na dwa sposoby. Najpierw uzupełniliśmy pliki dźwiękowe powietrzem ciszy, aby były dłuższe. Następnie próbowaliśmy włączyć odtwarzanie naprzemienne w kilku kanałach audio. Żadna z metod nie okazała się skuteczna ani elegancka.
Ostatecznie zdecydowaliśmy się wdrożyć własny odtwarzacz audio Flash i w zastępstwie użyć dźwięku w formacie HTML5. Oto podstawowy kod we Flashu:
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 kodzie JavaScript próbujemy wykryć umieszczony 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 przejmowaliśmy się z obsługą formatu OGG. Mamy nadzieję, że w najbliższej przyszłości branża zdecyduje się na jednolity format.
Pozycja ankiety
W HTML5 używamy tej samej metody co we Flashu, by odświeżać stan gry: co 10 sekund klient prosi serwer o aktualizacje. Jeśli stan gry zmienił się od czasu ostatniej ankiety, to klient otrzymuje zmiany i je obsługuje. W przeciwnym razie nic się nie dzieje. Ta tradycyjna technika ankiet jest akceptowalna, choć niezbyt elegancka. Chcielibyśmy jednak przejść na długie ankiety lub WebSockets, ponieważ rozgrywka dobiega końca i użytkownicy oczekują interakcji w sieci w czasie rzeczywistym. Zwłaszcza WebSockets daje wiele możliwości poprawy rozgrywki.
Co za narzędzie!
Skorzystaliśmy z Google Web Toolkit (GWT) do opracowania zarówno interfejsu frontendu, jak i logiki kontroli backendu (uwierzytelniania, weryfikacji, trwałości itp.). Kod JavaScript jest kompilowany z kodu źródłowego Java. Na przykład funkcja punktu została adaptowana z zastosowanej 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 mają odpowiednie pliki szablonów, w których elementy strony są „powiązane” z członkami klasy. Na przykład ChatPanel.ui.xml
odpowiada wartości 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>
Szczegółowe informacje wykraczają poza zakres tego artykułu, ale zachęcamy do zapoznania się z GWT przy kolejnych projektach HTML5.
Dlaczego warto używać Javy? Po pierwsze, do ścisłego pisania. Pisanie dynamiczne jest przydatne w języku JavaScript – na przykład możliwość przechowywania wartości różnych typów przez tablicę – jednak w przypadku dużych, złożonych projektów może być kłopotliwe. Po drugie, jeśli chodzi o możliwości refaktoryzacji. Zastanów się, jak zmienić podpis metody JavaScript w tysiącach wierszy kodu – nie jest to łatwe. Jednak w przypadku dobrego środowiska IDE Java to żmudna migracja. I wreszcie, do celów testowych. Pisanie testów jednostkowych klas Java przekracza uznaną technikę „zapisywania i odświeżania”.
Podsumowanie
Oprócz problemów z dźwiękiem HTML5 znacznie przekroczył nasze oczekiwania. Wordico nie tylko wygląda tak samo dobrze jak we Flashu, ale jest też tak samo elastyczni i elastyczny. Nie udało się nam to bez Canvas i CSS3. Przed nami następne wyzwanie: dostosuj Wordico do użytku na urządzeniach mobilnych.