Introdução
O Racer é um experimento do Chrome para dispositivos móveis baseado na Web desenvolvido pela Active Theory. Até cinco amigos podem conectar os smartphones ou tablets para competir em todas as telas. Com o conceito, o design e o protótipo do Google Creative Lab e o som do Plan8, iteramos os builds por oito semanas até o lançamento na I/O 2013. Agora que o jogo está no ar há algumas semanas, tivemos a chance de responder a algumas perguntas da comunidade de desenvolvedores sobre como ele funciona. Confira abaixo um detalhamento dos principais recursos e respostas para as perguntas mais frequentes.
A faixa
Um desafio bastante óbvio que enfrentamos foi como criar um jogo para dispositivos móveis baseado na Web que funcionasse bem em uma ampla variedade de dispositivos. Os jogadores precisavam criar uma corrida com diferentes smartphones e tablets. Um jogador pode ter um Nexus 4 e querer competir contra um amigo que tem um iPad. Precisávamos encontrar uma maneira de determinar um tamanho de pista comum para cada corrida. A solução precisava envolver o uso de faixas de tamanhos diferentes, dependendo das especificações de cada dispositivo incluído na corrida.
Como calcular as dimensões da faixa
À medida que cada jogador entra, informações sobre o dispositivo são enviadas ao servidor e compartilhadas com outros jogadores. Quando a faixa está sendo criada, esses dados são usados para calcular a altura e a largura dela. Calculamos a altura encontrando a altura da tela menor, e a largura é a largura total de todas as telas. No exemplo abaixo, a faixa teria uma largura de 1.152 pixels e uma altura de 519 pixels.

this.getDimensions = function () {
var response = {};
response.width = 0;
response.height = _gamePlayers[0].scrn.h; // First screen height
response.screens = [];
for (var i = 0; i < _gamePlayers.length; i++) {
var player = _gamePlayers[i];
response.width += player.scrn.w;
if (player.scrn.h < response.height) {
// Find the smallest screen height
response.height = player.scrn.h;
}
response.screens.push(player.scrn);
}
return response;
}
Desenhar a faixa
O Paper.js é um framework de script de gráficos vetoriais de código aberto executado em cima da tela HTML5. Descobrimos que a Paper.js era a ferramenta perfeita para criar formas vetoriais para as faixas. Por isso, usamos os recursos dela para renderizar as faixas SVG criadas no Adobe Illustrator em um elemento <canvas>
. Para criar a faixa, a classe TrackModel
anexa o código SVG ao DOM e coleta informações sobre as dimensões e o posicionamento originais que serão transmitidos para o TrackPathView
, que vai desenhar a faixa em uma tela.
paper.install(window);
_paper = new paper.PaperScope();
_paper.setup('track_canvas');
var svg = document.getElementById('track');
var layer = new _paper.Layer();
_path = layer.importSvg(svg).firstChild.firstChild;
_path.strokeColor = '#14a8df';
_path.strokeWidth = 2;
Depois que a faixa é desenhada, cada dispositivo encontra o deslocamento x com base na posição na ordem de apresentação dos dispositivos e posiciona a faixa de acordo com isso.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

Animações CSS
O Paper.js usa muito processamento de CPU para desenhar as faixas da pista, e esse processo leva mais ou menos tempo em diferentes dispositivos. Para lidar com isso, precisamos de um carregador em loop até que todos os dispositivos terminem de processar a faixa. O problema era que qualquer animação baseada em JavaScript pulava frames devido aos requisitos de CPU do Paper.js. Entre as animações CSS, que são executadas em um thread separado da interface, permitindo que a animação do brilho seja executada sem problemas no texto "BUILDING TRACK".
.glow {
width: 290px;
height: 290px;
background: url('img/track-glow.png') 0 0 no-repeat;
background-size: 100%;
top: 0;
left: -290px;
z-index: 1;
-webkit-animation: wipe 1.3s linear 0s infinite;
}
@-webkit-keyframes wipe {
0% {
-webkit-transform: translate(-300px, 0);
}
25% {
-webkit-transform: translate(-300px, 0);
}
75% {
-webkit-transform: translate(920px, 0);
}
100% {
-webkit-transform: translate(920px, 0);
}
}
}
Sprites de CSS
O CSS também foi útil para efeitos no jogo. Os dispositivos móveis, com energia limitada, ficam ocupados animando os carros que correm nas pistas. Para aumentar a empolgação, usamos sprites como uma forma de implementar animações pré-renderizadas no jogo. Em um sprite CSS, as transições aplicam uma animação baseada em etapas que muda a propriedade background-position
, criando a explosão do carro.
#sprite {
height: 100px;
width: 100px;
background: url('sprite.jpg') 0 0 no-repeat;
-webkit-animation: play-sprite 0.33s linear 0s steps(9) infinite;
}
@-webkit-keyframes play-sprite {
0% {
background-position: 0 0;
}
100% {
background-position: -900px 0;
}
}
O problema dessa técnica é que você só pode usar sprites dispostos em uma única linha. Para fazer um loop em várias linhas, a animação precisa ser encadeada por várias declarações de frame-chave.
#sprite {
height: 100px;
width: 100px;
background: url('sprite.jpg') 0 0 no-repeat;
-webkit-animation-name: row1, row2, row3;
-webkit-animation-duration: 0.2s;
-webkit-animation-delay: 0s, 0.2s, 0.4s;
-webkit-animation-timing-function: steps(5), steps(5), steps(5);
-webkit-animation-fill-mode: forwards;
}
@-webkit-keyframes row1 {
0% {
background-position: 0 0;
}
100% {
background-position: -500px 0;
}
}
@-webkit-keyframes row2 {
0% {
background-position: 0 -100px;
}
100% {
background-position: -500px -100px;
}
}
@-webkit-keyframes row3 {
0% {
background-position: 0 -200px;
}
100% {
background-position: -500px -200px;
}
}
Renderização dos carros
Como em qualquer jogo de corrida de carros, sabíamos que era importante dar ao usuário uma sensação de aceleração e controle. Aplicar uma quantidade diferente de tração foi importante para o equilíbrio do jogo e o fator diversão, para que, quando um jogador tivesse uma noção da física, ele tivesse uma sensação de realização e se tornasse um piloto melhor.
Mais uma vez, chamamos o Paper.js, que vem com um extenso conjunto de utilitários matemáticos. Usamos alguns métodos para mover o carro ao longo do caminho, ajustando a posição e a rotação do carro suavemente em cada frame.
var trackOffset = _path.length - (_elapsed % _path.length);
var trackPoint = _path.getPointAt(trackOffset);
var trackAngle = _path.getTangentAt(trackOffset).angle;
// Apply the throttle
_velocity.length += _throttle;
if (!_throttle) {
// Slow down since the throttle is off
_velocity.length *= FRICTION;
}
if (_velocity.length > MAXVELOCITY) {
_velocity.length = MAXVELOCITY;
}
_velocity.angle = trackAngle;
trackOffset -= _velocity.length;
_elapsed += _velocity.length;
// Find if a lap has been completed
if (trackOffset < 0) {
while (trackOffset < 0) trackOffset += _path.length;
trackPoint = _path.getPointAt(trackOffset);
console.log('LAP COMPLETE!');
}
if (_velocity.length > 0.1) {
// Render the car if there is actually velocity
renderCar(trackPoint);
}
Enquanto estávamos otimizando a renderização de carros, encontramos um ponto interessante. No iOS, a melhor performance foi alcançada aplicando uma transformação translate3d
ao carro:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
No Chrome para Android, a melhor performance foi alcançada calculando os valores da matriz e aplicando uma transformação de matriz:
var rad = _rotation.rotation * (Math.PI * 2 / 360);
var cos = Math.cos(rad);
var sin = Math.sin(rad);
var a = parseFloat(cos).toFixed(8);
var b = parseFloat(sin).toFixed(8);
var c = parseFloat(-sin).toFixed(8);
var d = a;
_car.style.webkitTransform = 'matrix(' + a + ', ' + b + ', ' + c + ', ' + d + ', ' + _position.x + ', ' + _position.y + ')';
Manter os dispositivos sincronizados
A parte mais importante (e difícil) do desenvolvimento foi garantir que o jogo fosse sincronizado entre os dispositivos. Achamos que os usuários poderiam perdoar se um carro ocasionalmente pulasse alguns frames devido a uma conexão lenta, mas não seria muito divertido se o carro estivesse pulando e aparecendo em várias telas ao mesmo tempo. A solução exigiu muitos testes e erros, mas, no final, encontramos alguns truques que funcionaram.
Como calcular a latência
O ponto de partida para sincronizar dispositivos é saber quanto tempo leva para as mensagens serem recebidas do relé do Compute Engine. O problema é que os relógios de cada dispositivo nunca estarão totalmente sincronizados. Para contornar esse problema, precisamos encontrar a diferença de tempo entre o dispositivo e o servidor.
Para encontrar o deslocamento de tempo entre o dispositivo e o servidor principal, enviamos uma mensagem com o carimbo de data/hora atual do dispositivo. O servidor vai responder com o carimbo de data/hora original e o do servidor. Usamos a resposta para calcular a diferença real no tempo.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Fazer isso uma vez não é suficiente, porque a viagem de ida e volta ao servidor não é sempre simétrica, o que significa que pode levar mais tempo para a resposta chegar ao servidor do que para o servidor retornar. Para contornar isso, consultamos o servidor várias vezes, usando o resultado médio. Isso nos leva a uma diferença de 10 ms entre o dispositivo e o servidor.
Aceleração/desaceleração
Quando o Jogador 1 pressiona ou libera a tela, o evento de aceleração é enviado ao servidor. Depois de recebido, o servidor adiciona o carimbo de data/hora atual e transmite os dados a todos os outros jogadores.
Quando um evento "aceleração ativada" ou "aceleração desativada" é recebido por um dispositivo, podemos usar o deslocamento do servidor (calculado acima) para descobrir quanto tempo levou para que a mensagem fosse recebida. Isso é útil porque o Jogador 1 pode receber a mensagem em 20 ms, mas o Jogador 2 pode levar 50 ms para receber. Isso resultaria no carro estar em dois lugares diferentes, porque o dispositivo 1 iniciaria a aceleração mais cedo.
Podemos usar o tempo que levou para receber o evento e convertê-lo em frames. A 60 fps, cada frame tem 16,67 ms.Portanto, podemos adicionar mais velocidade (aceleração) ou atrito (desaceleração) ao carro para compensar os frames perdidos.
var frames = time / 16.67;
var onScreen = this.isOnScreen() && time < 75;
for (var i = 0; i < frames; i++) {
if (onScreen) {
_velocity.length += _throttle * Math.round(frames * .215);
} else {
_this.render();
}
}}
No exemplo acima, se o Jogador 1 tiver o carro na tela e o tempo que ele levou para receber a mensagem for menor que 75ms, ele vai ajustar a velocidade do carro, acelerando-o para compensar a diferença. Se o dispositivo não estiver na tela ou a mensagem demorar muito, a função de renderização será executada e o carro vai pular para onde precisa estar.
Manter os carros sincronizados
Mesmo após considerar a latência na aceleração, o carro ainda pode sair da sincronização e aparecer em várias telas ao mesmo tempo, especificamente ao fazer a transição de um dispositivo para outro. Para evitar isso, os eventos de atualização são enviados com frequência para manter os carros na mesma posição na pista em todas as telas.
A lógica é que, a cada quatro frames, se o carro estiver visível na tela, esse dispositivo vai enviar os valores para cada um dos outros dispositivos. Se o carro não estiver visível, o app vai atualizar os valores com os recebidos e, em seguida, vai mover o carro com base no tempo que levou para receber o evento de atualização.
this.getValues = function () {
_values.p = _position.clone();
_values.r = _rotation;
_values.e = _elapsed;
_values.v = _velocity.length;
_values.pos = _this.position;
return _values;
}
this.setValues = function (val, time) {
_position.x = val.p.x;
_position.y = val.p.y;
_rotation = val.r;
_elapsed = val.e;
_velocity.length = val.v;
var frames = time / 16.67;
for (var i = 0; i < frames; i++) {
_this.render();
}
}
Conclusão
Assim que ouvimos o conceito do Racer, sabíamos que ele tinha potencial para ser um projeto muito especial. Rapidamente, criamos um protótipo que nos deu uma ideia aproximada de como superar a latência e o desempenho da rede. Foi um projeto desafiador que nos manteve ocupados durante noites e fins de semana longos, mas foi uma sensação incrível quando o jogo começou a tomar forma. Estamos muito felizes com o resultado final. O conceito do Google Creative Lab ultrapassou os limites da tecnologia de navegadores de uma forma divertida, e, como desenvolvedores, não poderíamos pedir mais.