Introdução
Quando convertemos nosso jogo de palavras cruzadas Wordico de Flash para HTML5, nossa primeira tarefa foi descansar tudo o que sabíamos sobre como criar uma experiência do usuário avançada no navegador. Embora o Flash oferecesse uma API única e abrangente para todos os aspectos do desenvolvimento de aplicativos, desde desenho vetorial e detecção de ocorrências em polígonos até análise XML, o HTML5 oferecia uma série de especificações com suporte variado ao navegador. Também nos perguntamos se o HTML, uma linguagem específica de documento, e o CSS, uma linguagem centrada em caixas, eram adequados para a criação de um jogo. O jogo será exibido de maneira uniforme em todos os navegadores, assim como no Flash, e teria a mesma aparência e o mesmo comportamento? Para o Wordico, a resposta foi sim.
Qual é seu vetor, Victor?
Desenvolvemos a versão original do Wordico usando apenas gráficos vetoriais: linhas, curvas, preenchimentos e gradientes. O resultado foi altamente compacto e infinitamente escalonável:
Também aproveitamos a linha do tempo do Flash para criar objetos com vários estados. Por exemplo, usamos nove frames-chave nomeados para o objeto Space
:
No entanto, em HTML5, usamos um sprite bitmap:
Para criar o gameboard de 15 x 15 usando espaços individuais, iteramos uma notação de string de 225 caracteres em que cada espaço é representado por um caractere diferente (como "t" para letra tripla e "T" para palavra tripla). Essa foi uma operação simples no Flash. Nós simplesmente marcamos os espaços e os organizamos em uma grade:
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);
No HTML5, é um pouco mais complicado. Usamos o elemento <canvas>
, uma superfície de desenho em bitmap, para pintar o gameboard um quadrado de cada vez. A primeira etapa é carregar o sprite da imagem. Depois do carregamento, iteramos a notação de layout, desenhando uma parte diferente da imagem a cada iteração:
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;
}
Aqui está o resultado no navegador da Web. Observe que a tela em si tem uma sombra projetada em CSS:
Converter o objeto de bloco foi um exercício semelhante. No Flash, usamos campos de texto e formas vetoriais:
Em HTML5, combinamos três sprites de imagem em um único elemento <canvas>
durante a execução:
Agora temos 100 telas (uma para cada bloco), além de uma tela para o tabuleiro. Esta é a marcação para um bloco "H":
<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>
Confira o CSS correspondente:
.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));
}
Aplicamos efeitos CSS3 quando o bloco está sendo arrastado (sombra, opacidade e dimensionamento) e quando o bloco está no rack (reflexão):
O uso de imagens de varredura tem algumas vantagens óbvias. Primeiro, o resultado é preciso. Segundo, essas imagens podem ser armazenadas em cache pelo navegador. Terceiro, com um pouco mais de trabalho, podemos trocar as imagens para criar novos designs de blocos - como uma peça de metal - e esse trabalho de design pode ser feito no Photoshop em vez de em Flash.
A desvantagem? Ao usar imagens, desistimos do acesso programático aos campos de texto. No Flash, era uma operação simples alterar a cor ou outras propriedades do tipo. No HTML5, essas propriedades são incorporadas às próprias imagens. Tentamos usar texto HTML, mas foi necessário usar muitas marcações e CSS extras. Também tentamos usar o texto em tela, mas os resultados eram inconsistentes em todos os navegadores.
Lógica difusa
Queríamos usar ao máximo a janela do navegador em qualquer tamanho, sem precisar rolar a tela. Essa era uma operação relativamente simples em Flash, já que o jogo todo era desenhado em vetores e poderia ser dimensionado para mais ou para menos sem perder a fidelidade. Mas era mais complicado em HTML. Tentamos usar o escalonamento CSS, mas acabamos com uma tela desfocada:
Nossa solução é redesenhar o gameboard, o rack e os blocos sempre que o usuário redimensionar o navegador:
window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);
...
rack.setConstraints(rackWidth, rackHeight);
...
tileManager.resizeTiles(tileSize);
});
Chegamos a imagens nítidas e layouts agradáveis em qualquer tamanho de tela:
Vá direto ao ponto
Como cada bloco está totalmente posicionado e precisa estar alinhado precisamente ao tabuleiro e ao rack, precisamos de um sistema confiável de posicionamento. Usamos duas funções, Bounds
e Point
, para ajudar a gerenciar a localização dos elementos no espaço global (a página HTML). Bounds
descreve uma área retangular na página, enquanto Point
descreve uma coordenada x,y em relação ao canto superior esquerdo da página (0,0), também conhecida como o ponto de registro.
Com Bounds
, podemos detectar a interseção de dois elementos retangulares (como quando um bloco cruza o rack) ou se uma área retangular (como um espaço com duas letras) contém um ponto arbitrário (como o ponto central de um bloco). Confira a implementação de limites:
// 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(",");
}
Usamos Point
para determinar a coordenada absoluta (canto superior esquerdo) de qualquer elemento na página ou de um evento do mouse. Point
também contém métodos para calcular a distância e a direção, necessários para criar efeitos de animação. Confira a implementação de 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);
}
Essas funções formam a base dos recursos de arrastar e soltar e de animação. Por exemplo, usamos Bounds.intersects()
para determinar se um bloco se sobrepõe a um espaço no tabuleiro. Usamos Point.vector()
para determinar a direção de um bloco arrastado e usamos Point.interpolate()
em combinação com um timer para criar uma interpolação de movimento ou efeito de easing.
Me deixo levar
Embora layouts de tamanho fixo sejam mais fáceis de produzir em Flash, layouts fluidos são muito mais fáceis de gerar com HTML e o modelo de caixa CSS. Considere a seguinte visualização em grade, com largura e altura variáveis:
Ou considere o painel de chat. A versão do Flash exigia vários manipuladores de eventos para responder às ações do mouse, uma máscara para a área de rolagem, matemática para calcular a posição de rolagem e muitos outros códigos para uni-la.
Em comparação, a versão HTML é apenas uma <div>
com altura fixa e a propriedade flutuante definida como oculta. A rolagem não custa nada.
Em casos como esse, tarefas comuns de layout, HTML e CSS ofuscam o Flash.
Você está me ouvindo agora?
Tivemos problemas com a tag <audio>
porque ela simplesmente não era capaz de reproduzir efeitos sonoros curtos repetidamente em determinados navegadores. Tentamos duas soluções alternativas. Primeiro, preenchemos os arquivos de som com ar morto para deixá-los mais longos. Depois, tentamos alternar a reprodução em vários canais de áudio. Nenhuma das técnicas era completamente eficaz ou elegante.
Por fim, decidimos implementar nosso próprio player de áudio em Flash e usar áudio HTML5 como substituto. Este é o código básico em 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);
Em JavaScript, tentamos detectar o Flash player incorporado. Se isso falhar, criaremos um nó <audio>
para cada arquivo de som:
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();
}
}
Esse recurso funciona apenas para arquivos MP3. Nunca nos preocupamos em oferecer suporte a OGG. Esperamos que o setor escolha um único formato no futuro próximo.
Posição da enquete
Usamos no HTML5 a mesma técnica usada em Flash para atualizar o estado do jogo: a cada 10 segundos, o cliente solicita atualizações ao servidor. Se o estado do jogo tiver mudado desde a última enquete, o cliente vai receber e processar as mudanças. Caso contrário, nada acontecerá. Essa técnica de votação tradicional é aceitável, se não muito elegante. No entanto, gostaríamos de mudar para pesquisas longas ou WebSockets à medida que o jogo amadurecer e os usuários passarem a esperar interação em tempo real pela rede. Os WebSockets, em especial, ofereceriam muitas oportunidades para melhorar o jogo.
Que ferramenta!
Usamos o Google Web Toolkit (GWT) para desenvolver a interface de usuário de front-end e a lógica de controle de back-end (autenticação, validação, persistência e assim por diante). O próprio JavaScript é compilado a partir do código-fonte Java. Por exemplo, a função Point é adaptada de 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));
}
...
}
Algumas classes de interface têm arquivos de modelo correspondentes em que os elementos da página são "vinculados" aos membros da classe. Por exemplo, ChatPanel.ui.xml
corresponde a 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>
Os detalhes completos estão além do escopo deste artigo, mas recomendamos que você acesse o GWT para seu próximo projeto HTML5.
Por que usar Java? Primeiro, para digitação restrita. Embora a digitação dinâmica seja útil em JavaScript - por exemplo, a capacidade de uma matriz de conter valores de tipos diferentes - ela pode ser uma dor de cabeça em projetos grandes e complexos. Segundo, para recursos de refatoração. Pense em como você mudaria uma assinatura de método JavaScript em milhares de linhas de código, de maneira fácil! Mas, com um bom IDE em Java, é instantâneo. Por fim, para fins de teste. A criação de testes de unidade para classes Java supera a técnica consagrada de "salvar e atualizar".
Resumo
Exceto por problemas de áudio, o HTML5 superou muito nossas expectativas. Além de ter uma aparência tão boa quanto no Flash, o Wordico é fluido e responsivo. Não teríamos conseguido fazer isso sem o Canvas e o CSS3. Nosso próximo desafio: adaptar o Wordico para uso em dispositivos móveis.