Introducción
Racer es un experimento de Chrome para dispositivos móviles basado en la Web desarrollado por Active Theory. Hasta 5 amigos pueden conectar sus teléfonos o tablets para competir en todas las pantallas. Con el concepto, el diseño y el prototipo de Google Creative Lab y el sonido de Plan8, iteramos en las compilaciones durante 8 semanas antes del lanzamiento en I/O 2013. Ahora que el juego está disponible desde hace algunas semanas, tuvimos la oportunidad de responder algunas preguntas de la comunidad de desarrolladores sobre cómo funciona. A continuación, se incluye un desglose de las funciones clave y las respuestas a las preguntas más frecuentes.
La pista
Un desafío bastante obvio al que nos enfrentamos fue cómo hacer un juego para dispositivos móviles basado en la Web que funcione bien en una amplia variedad de dispositivos. Los jugadores debían poder crear una carrera con diferentes teléfonos y tablets. Un jugador podría tener un Nexus 4 y querer competir contra su amigo que tiene un iPad. Necesitábamos encontrar una forma de determinar un tamaño de pista común para cada carrera. La solución debía incluir el uso de segmentos de diferentes tamaños según las especificaciones de cada dispositivo incluido en la carrera.
Cómo calcular las dimensiones de la pista
A medida que se une cada jugador, la información sobre su dispositivo se envía al servidor y se comparte con otros jugadores. Cuando se construye la pista, estos datos se usan para calcular la altura y el ancho de la pista. Para calcular la altura, buscamos la altura de la pantalla más pequeña, y el ancho es el ancho total de todas las pantallas. En el siguiente ejemplo, la pista tendría un ancho de 1,152 píxeles y una altura de 519 píxeles.

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;
}
Cómo dibujar la pista
Paper.js es un framework de secuencias de comandos de gráficos vectoriales de código abierto que se ejecuta en Canvas de HTML5. Descubrimos que Paper.js era la herramienta perfecta para crear formas vectoriales para los segmentos, por lo que usamos sus capacidades para renderizar los segmentos SVG que se compilaron en Adobe Illustrator en un elemento <canvas>
. Para crear el segmento, la clase TrackModel
agrega el código SVG al DOM y recopila información sobre las dimensiones y el posicionamiento originales que se pasarán a TrackPathView
, que dibujará el segmento en un lienzo.
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;
Una vez que se dibuja la pista, cada dispositivo encuentra su desplazamiento en X según su posición en el orden de la alineación de dispositivos y posiciona la pista según corresponda.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

Animaciones de CSS
Paper.js usa mucho procesamiento de CPU para dibujar los carriles de la ruta, y este proceso llevará más o menos tiempo en diferentes dispositivos. Para controlar esto, necesitábamos un cargador que realizara un bucle hasta que todos los dispositivos terminaran de procesar la pista. El problema era que cualquier animación basada en JavaScript omitía fotogramas debido a los requisitos de CPU de Paper.js. Ingresa las animaciones de CSS, que se ejecutan en un subproceso de IU independiente, lo que nos permite animar sin problemas el brillo en el 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
El CSS también fue útil para los efectos en el juego. Los dispositivos móviles, con su potencia limitada, se mantienen ocupados animando los autos que circulan por los rieles. Para agregar más emoción, usamos sprites como una forma de implementar animaciones renderizadas previamente en el juego. En un sprite CSS, las transiciones aplican una animación basada en pasos que cambia la propiedad background-position
y crea la explosión del automóvil.
#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;
}
}
El problema con esta técnica es que solo puedes usar hojas de sprites dispuestas en una sola fila. Para hacer un bucle en varias filas, la animación debe encadenarse a través de varias declaraciones de fotogramas clave.
#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;
}
}
Renderización de los automóviles
Como con cualquier juego de carreras de autos, sabíamos que era importante darle al usuario una sensación de aceleración y manejo. Aplicar una cantidad diferente de tracción fue importante para el equilibrio del juego y el factor diversión, de modo que, una vez que un jugador se familiarice con la física, sienta un sentido de logro y se convierta en un mejor piloto.
Una vez más, llamamos a Paper.js, que incluye un amplio conjunto de utilidades matemáticas. Usamos algunos de sus métodos para mover el automóvil a lo largo de la ruta y, al mismo tiempo, ajustar su posición y rotación de forma fluida en cada fotograma.
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);
}
Mientras optimizábamos la renderización de automóviles, encontramos un punto interesante. En iOS, se logró el mejor rendimiento aplicando una transformación translate3d
al automóvil:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
En Chrome para Android, se logró el mejor rendimiento calculando los valores de la matriz y aplicando una transformación 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 + ')';
Cómo mantener los dispositivos sincronizados
La parte más importante (y difícil) del desarrollo fue asegurarse de que el juego se sincronizara en todos los dispositivos. Pensamos que los usuarios podrían ser tolerantes si, en ocasiones, un vehículo omite algunos fotogramas debido a una conexión lenta, pero no sería muy divertido si tu vehículo salteara y apareciera en varias pantallas a la vez. Para resolver este problema, tuvimos que probar muchas opciones, pero, finalmente, encontramos algunos trucos que hicieron que funcionara.
Cálculo de la latencia
El punto de partida para sincronizar dispositivos es saber cuánto tiempo tardan en recibirse los mensajes del relé de Compute Engine. La parte complicada es que los relojes de cada dispositivo nunca estarán completamente sincronizados. Para solucionar este problema, tuvimos que encontrar la diferencia de tiempo entre el dispositivo y el servidor.
Para encontrar la compensación de tiempo entre el dispositivo y el servidor principal, enviamos un mensaje con la marca de tiempo actual del dispositivo. Luego, el servidor responderá con la marca de tiempo original junto con la marca de tiempo del servidor. Usamos la respuesta para calcular la diferencia real en el tiempo.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Hacer esto una vez no es suficiente, ya que el recorrido de ida y vuelta al servidor no siempre es simétrico, lo que significa que la respuesta puede tardar más en llegar al servidor que en que el servidor la devuelva. Para evitar esto, sondeamos el servidor varias veces y tomamos el resultado de la mediana. Esto nos permite obtener una diferencia de 10 ms entre el dispositivo y el servidor.
Aceleración/desaceleración
Cuando el jugador 1 presiona o suelta la pantalla, el evento de aceleración se envía al servidor. Una vez que se recibe, el servidor agrega su marca de tiempo actual y, luego, pasa esos datos a todos los demás jugadores.
Cuando un dispositivo recibe un evento de "activar aceleración" o "desactivar aceleración", podemos usar el desfase del servidor (calculado anteriormente) para saber cuánto tiempo tardó en recibirse ese mensaje. Esto es útil, ya que el jugador 1 puede recibir el mensaje en 20 ms, pero el jugador 2 puede tardar 50 ms en recibirlo. Esto provocaría que el automóvil esté en dos lugares diferentes, ya que el dispositivo 1 iniciaría la aceleración antes.
Podemos tomar el tiempo que tardó en recibirse el evento y convertirlo en fotogramas. A 60 fps, cada fotograma es de 16.67 ms, por lo que podemos agregar más velocidad (aceleración) o fricción (desaceleración) al automóvil para compensar los fotogramas que se perdieron.
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();
}
}}
En el ejemplo anterior, si el jugador 1 tiene el automóvil en la pantalla y el tiempo que tardó en recibir el mensaje es inferior a 75 ms, se ajustará la velocidad del automóvil y se acelerará para compensar la diferencia. Si el dispositivo no está en pantalla o el mensaje tardó demasiado, se ejecutará la función de renderización y, en realidad, hará que el vehículo salte a donde debe estar.
Cómo mantener sincronizados los automóviles
Incluso después de tener en cuenta la latencia en la aceleración, el automóvil podría salir de sincronización y aparecer en varias pantallas a la vez, en especial cuando se pasa de un dispositivo al siguiente. Para evitar esto, se envían eventos de actualización con frecuencia para mantener los autos en la misma posición en la pista en todas las pantallas.
La lógica es que, cada 4 fotogramas, si el automóvil es visible en la pantalla, ese dispositivo envía sus valores a cada uno de los otros dispositivos. Si el vehículo no está visible, la app actualiza los valores con los recibidos y, luego, avanza el vehículo según el tiempo que tardó en recibir el evento de actualización.
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();
}
}
Conclusión
En cuanto escuchamos el concepto de Racer, supimos que tenía el potencial de ser un proyecto muy especial. Creamos rápidamente un prototipo que nos dio una idea aproximada de cómo superar la latencia y el rendimiento de la red. Fue un proyecto desafiante que nos mantuvo ocupados durante las noches y los fines de semana largos, pero fue una gran sensación cuando el juego comenzó a tomar forma. En última instancia, estamos muy contentos con el resultado final. El concepto de Google Creative Lab llevó los límites de la tecnología de navegadores de una manera divertida, y como desarrolladores, no podíamos pedir más.