Введение
Когда мы конвертировали нашу кроссворд Wordico из Flash в HTML5, нашей первой задачей было забыть все, что мы знали о создании удобного пользовательского интерфейса в браузере. В то время как Flash предлагал единый комплексный API для всех аспектов разработки приложений — от векторного рисования до обнаружения попаданий в полигоны и анализа XML, — HTML5 предлагал множество спецификаций с различной поддержкой браузеров. Мы также задались вопросом, подходят ли HTML, язык, ориентированный на работу с документами, и CSS, ориентированный на блоки язык, для создания игры. Будет ли игра одинаково отображаться во всех браузерах, как во Flash, и будет ли она выглядеть и вести себя так же хорошо? Для Wordico ответ был утвердительным.
Каков твой вектор, Виктор?
Мы разработали оригинальную версию Wordico, используя только векторную графику: линии, кривые, заливки и градиенты. Результат оказался одновременно очень компактным и бесконечно масштабируемым:
Мы также воспользовались преимуществами временной шкалы Flash для создания объектов, имеющих несколько состояний. Например, мы использовали девять именованных ключевых кадров для объекта Space
:
Однако в HTML5 мы используем растровый спрайт:
Чтобы создать игровое поле 15x15 из отдельных пробелов, мы перебираем строковую нотацию длиной 225 символов, в которой каждый пробел представлен отдельным символом (например, «t» для тройной буквы и «T» для тройного слова). Во Flash это была простая операция; мы просто вырезали пробелы и располагали их в сетке:
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);
В HTML5 все немного сложнее. Мы используем элемент <canvas>
, поверхность рисования растрового изображения, для рисования игрового поля по одному квадрату за раз. Первый шаг — загрузить спрайт изображения. После загрузки мы перебираем нотацию макета, рисуя на каждой итерации новую часть изображения:
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;
}
Вот результат в веб-браузере. Обратите внимание, что сам холст имеет тень CSS:
Преобразование объекта-плитки было аналогичным упражнением. Во Flash мы использовали текстовые поля и векторные фигуры:
В HTML5 мы объединяем три спрайта изображения в одном элементе <canvas>
во время выполнения:
Теперь у нас есть 100 холстов (по одному на каждую плитку) плюс холст для игрового поля. Вот разметка для плитки «H»:
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Вот соответствующий 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));
}
Мы применяем эффекты CSS3 при перетаскивании плитки (тень, непрозрачность и масштабирование) и когда плитка находится на стойке (отражение):
Использование растровых изображений имеет ряд очевидных преимуществ. Во-первых, результат имеет точность до пикселя. Во-вторых, эти изображения могут кэшироваться браузером. В-третьих, приложив небольшую дополнительную работу, мы можем поменять изображения, чтобы создать новый дизайн плитки, например, металлическую черепицу, и эту работу по дизайну можно выполнить в Photoshop, а не во Flash.
Недостаток? Используя изображения, мы отказываемся от программного доступа к текстовым полям. Во Flash это была простая операция по изменению цвета или других свойств шрифта; в HTML5 эти свойства встроены в сами изображения. (Мы попробовали текст HTML, но для этого потребовалось много дополнительной разметки и CSS. Мы также попробовали текст холста, но результаты были непоследовательными в разных браузерах.)
Нечеткая логика
Мы хотели в полной мере использовать окно браузера любого размера и избежать прокрутки. Во Flash это была относительно простая операция, поскольку вся игра была нарисована в векторах и ее можно было увеличивать или уменьшать без потери точности. Но в HTML все было сложнее. Мы попробовали использовать масштабирование CSS, но в итоге получили размытый холст:
Наше решение — перерисовывать игровое поле, стойку и плитки всякий раз, когда пользователь меняет размер браузера:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
В итоге мы получаем четкие изображения и приятный макет на экране любого размера:
Перейдем к делу
Поскольку каждая плитка абсолютно позиционирована и должна точно совпадать с игровой доской и стойкой, нам нужна надежная система позиционирования. Мы используем две функции, Bounds
и Point
, чтобы управлять расположением элементов в глобальном пространстве (странице HTML). Bounds
описывает прямоугольную область на странице, а Point
описывает координату x,y относительно верхнего левого угла страницы (0,0), также известную как точка регистрации.
С помощью Bounds
мы можем обнаружить пересечение двух прямоугольных элементов (например, когда плитка пересекает стойку) или содержит ли прямоугольная область (например, пробел из двух букв) произвольную точку (например, центральную точку плитки). . Вот реализация 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(",");
}
Мы используем Point
для определения абсолютной координаты (верхний левый угол) любого элемента на странице или события мыши. Point
также содержит методы расчета расстояния и направления, необходимые для создания анимационных эффектов. Вот реализация 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);
}
Эти функции составляют основу возможностей перетаскивания и анимации. Например, мы используем Bounds.intersects()
чтобы определить, перекрывает ли плитка пространство на игровом поле; мы используем Point.vector()
для определения направления перетаскиваемой плитки; и мы используем Point.interpolate()
в сочетании с таймером для создания анимации движения или эффекта замедления.
Плывите по течению
Хотя макеты фиксированного размера легче создавать во Flash, гибкие макеты гораздо проще создавать с помощью HTML и блочной модели CSS. Рассмотрим следующий вид сетки с переменной шириной и высотой:
Или рассмотрите панель чата. Версия Flash требовала нескольких обработчиков событий для реагирования на действия мыши, маски для области прокрутки, математических вычислений для расчета положения прокрутки и множества другого кода для объединения всего этого.
Для сравнения, HTML-версия представляет собой просто <div>
с фиксированной высотой и скрытым свойством переполнения. Прокрутка нам ничего не стоит.
В подобных случаях (обычные задачи верстки) HTML и CSS затмевают Flash.
Ты слышишь меня сейчас?
У нас были проблемы с тегом <audio>
— он просто не мог повторно воспроизводить короткие звуковые эффекты в некоторых браузерах. Мы попробовали два обходных пути. Во-первых, мы добавили в звуковые файлы мертвый воздух, чтобы сделать их длиннее. Затем мы попробовали попеременное воспроизведение по нескольким аудиоканалам. Ни одна из техник не была полностью эффективной или элегантной.
В конечном итоге мы решили создать собственный Flash-аудиоплеер и использовать HTML5-аудио в качестве запасного варианта. Вот основной код во 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);
В JavaScript мы пытаемся обнаружить встроенный Flash-плеер. Если это не помогло, мы создаем узел <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();
}
}
Обратите внимание, что это работает только для файлов MP3 — мы никогда не удосужились поддерживать OGG. Мы надеемся, что в ближайшем будущем индустрия остановится на едином формате.
Позиция в опросе
В HTML5 мы используем тот же метод, что и во Flash, для обновления состояния игры: каждые 10 секунд клиент запрашивает обновления у сервера. Если состояние игры изменилось с момента последнего опроса, клиент получает и обрабатывает изменения; в противном случае ничего не происходит. Эта традиционная методика опроса приемлема, хотя и не совсем элегантна. Однако мы хотели бы переключиться на длинный опрос или WebSockets по мере того, как игра становится более зрелой и пользователи начинают ожидать взаимодействия по сети в реальном времени. В частности, WebSockets предоставит множество возможностей для улучшения игрового процесса.
Какой инструмент!
Мы использовали Google Web Toolkit (GWT) для разработки как внешнего пользовательского интерфейса, так и внутренней логики управления (аутентификация, проверка, сохранение и т. д.). Сам JavaScript компилируется из исходного кода Java. Например, функция Point адаптирована из 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));
}
...
}
Некоторые классы пользовательского интерфейса имеют соответствующие файлы шаблонов, в которых элементы страницы «привязаны» к членам класса. Например, ChatPanel.ui.xml
соответствует 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>
Полная информация выходит за рамки этой статьи, но мы рекомендуем вам попробовать GWT для вашего следующего проекта HTML5.
Зачем использовать Java? Во-первых, для строгой типизации. Хотя динамическая типизация полезна в JavaScript (например, способность массива хранить значения разных типов), она может стать головной болью в больших и сложных проектах. Во-вторых, для возможностей рефакторинга. Подумайте, как бы вы изменили сигнатуру метода JavaScript в тысячах строк кода — это непросто! Но с хорошей Java IDE это совсем несложно. Наконец, в целях тестирования. Написание модульных тестов для классов Java превосходит проверенный временем метод «сохранения и обновления».
Краткое содержание
Если не считать проблем со звуком, HTML5 значительно превзошел наши ожидания. Wordico не только выглядит так же хорошо, как и во Flash, но и столь же плавный и отзывчивый. Мы бы не справились без Canvas и CSS3. Наша следующая задача: адаптация Wordico для мобильного использования.