Introduzione
Racer è un esperimento su Chrome mobile basato sul web sviluppato da Active Theory. Fino a 5 amici possono collegare i propri smartphone o tablet per gareggiare su ogni schermo. Grazie al concept, al design e al prototipo di Google Creative Lab e all'audio di Plan8, abbiamo eseguito l'iterazione delle build per 8 settimane prima del lancio all'I/O 2013. Ora che il gioco è disponibile da alcune settimane, abbiamo avuto la possibilità di rispondere ad alcune domande della community di sviluppatori sul suo funzionamento. Di seguito è riportata una suddivisione delle funzionalità principali e le risposte alle domande che ci vengono poste più di frequente.
The Track
Una sfida abbastanza ovvia che abbiamo dovuto affrontare è stata come creare un gioco mobile basato sul web che funzioni bene su una vasta gamma di dispositivi. I giocatori dovevano essere in grado di creare una gara con telefoni e tablet diversi. Un giocatore potrebbe avere un Nexus 4 e voler sfidare il suo amico che ha un iPad. Dovevamo trovare un modo per determinare una dimensione comune per ogni tracciato. La soluzione doveva prevedere l'utilizzo di tracce di dimensioni diverse a seconda delle specifiche di ciascun dispositivo incluso nella gara.
Calcolo delle dimensioni dei canali
Quando ogni giocatore si unisce, le informazioni sul suo dispositivo vengono inviate al server e condivise con gli altri giocatori. Durante la costruzione del percorso, questi dati vengono utilizzati per calcolarne l'altezza e la larghezza. Calcoliamo l'altezza trovando l'altezza dello schermo più piccolo e la larghezza è la larghezza totale di tutti gli schermi. Nell'esempio seguente, la traccia avrà una larghezza di 1152 pixel e un'altezza di 519 pixel.

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;
}
Disegno della traccia
Paper.js è un framework di scripting per grafica vettoriale open source che funziona su HTML5 Canvas. Abbiamo scoperto che Paper.js è lo strumento perfetto per creare forme vettoriali per i percorsi, quindi abbiamo utilizzato le sue funzionalità per eseguire il rendering dei percorsi SVG creati in Adobe Illustrator in un elemento <canvas>
. Per creare il tracciato, la classe TrackModel
aggiunge il codice SVG al DOM e raccoglie informazioni sulle dimensioni e sul posizionamento originali da passare a TrackPathView
, che disegnerà il tracciato in una 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;
Una volta tracciata la traccia, ogni dispositivo trova il relativo offset X in base alla sua posizione nell'ordine di organizzazione dei dispositivi e posiziona la traccia di conseguenza.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

Animazioni CSS
Paper.js utilizza molta elaborazione della CPU per disegnare le corsie e questo processo richiederà più o meno tempo su dispositivi diversi. Per gestire questo problema, avevamo bisogno di un caricatore in loop fino al completamento dell'elaborazione della traccia su tutti i dispositivi. Il problema era che qualsiasi animazione basata su JavaScript saltava i frame a causa dei requisiti della CPU di Paper.js. Inserisci le animazioni CSS, che vengono eseguite in un thread dell'interfaccia utente separato, che ci consente di animare senza problemi la lucentezza del testo "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);
}
}
}
Sprite CSS
Il CSS è stato utile anche per gli effetti in-game. I dispositivi mobili, con la loro potenza limitata, sono impegnati ad animare le auto che corrono sui binari. Per un'esperienza ancora più coinvolgente, abbiamo utilizzato gli sprite per implementare nel gioco animazioni pre-renderizzate. In uno sprite CSS, le transizioni applicano un'animazione basata su passaggi che modifica la proprietà background-position
, creando l'esplosione dell'auto.
#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;
}
}
Il problema di questa tecnica è che puoi utilizzare solo sprite sheet disposti su una singola riga. Per eseguire il loop in più righe, l'animazione deve essere collegata tramite più dichiarazioni di fotogrammi chiave.
#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;
}
}
Rendering delle auto
Come per qualsiasi gioco di corse automobilistiche, sapevamo che era importante dare all'utente la sensazione di accelerazione e manovrabilità. L'applicazione di una diversa trazione è stata importante per il bilanciamento del gioco e per il fattore divertimento, in modo che, una volta che il giocatore avesse preso dimestichezza con la fisica, potesse avere un senso di realizzazione e diventare un pilota migliore.
Ancora una volta abbiamo utilizzato Paper.js, che include un'ampia serie di utilità matematiche. Abbiamo utilizzato alcuni dei suoi metodi per spostare l'auto lungo il percorso, regolando contemporaneamente la posizione e la rotazione dell'auto in ogni fotogramma.
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);
}
Durante l'ottimizzazione del rendering delle auto, abbiamo trovato un punto interessante. Su iOS, il rendimento migliore è stato ottenuto applicando una trasformazione translate3d
all'auto:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
Su Chrome per Android, il rendimento migliore è stato ottenuto calcolando i valori della matrice e applicando una trasformazione della matrice:
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 + ')';
Mantenere sincronizzati i dispositivi
La parte più importante (e difficile) dello sviluppo è stata assicurarsi che il gioco si sincronizzasse su tutti i dispositivi. Abbiamo pensato che gli utenti potessero essere indulgenti se un'auto saltava occasionalmente qualche fotogramma a causa di una connessione lenta, ma non sarebbe molto divertente se la tua auto saltasse da uno schermo all'altro, apparendo su più schermi contemporaneamente. Per risolvere il problema sono stati necessari molti tentativi ed errori, ma alla fine abbiamo trovato alcuni trucchi che ci hanno permesso di risolvere il problema.
Calcolo della latenza
Il punto di partenza per la sincronizzazione dei dispositivi è sapere quanto tempo occorre per ricevere i messaggi dal relè Compute Engine. Il problema è che gli orologi di ogni dispositivo non saranno mai completamente sincronizzati. Per risolvere il problema, dovevamo trovare la differenza di tempo tra il dispositivo e il server.
Per trovare il fuso orario tra il dispositivo e il server principale, inviamo un messaggio con il timestamp corrente del dispositivo. Il server risponderà con il timestamp originale e con il timestamp del server. Utilizziamo la risposta per calcolare la differenza di tempo effettiva.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Eseguire questa operazione una volta non è sufficiente, poiché il viaggio di andata e ritorno al server non è sempre simmetrico, il che significa che potrebbe essere necessario più tempo per la risposta per raggiungere il server rispetto al tempo necessario per la risposta del server. Per ovviare a questo problema, eseguiamo il polling del server più volte, prendendo il risultato mediano. In questo modo, otteniamo una differenza di 10 ms rispetto alla differenza effettiva tra il dispositivo e il server.
Accelerazione/decelerazione
Quando il giocatore 1 preme o rilascia lo schermo, l'evento di accelerazione viene inviato al server. Una volta ricevuti, il server aggiunge il timestamp corrente e poi trasmette i dati a tutti gli altri giocatori.
Quando un dispositivo riceve un evento "accelera on" o "accelera off", possiamo utilizzare l'offset del server (calcolato sopra) per scoprire il tempo necessario per la ricezione del messaggio. Questo è utile perché il giocatore 1 potrebbe ricevere il messaggio in 20 ms, mentre il giocatore 2 potrebbe impiegare 50 ms. Ciò comporterà che l'auto si trovi in due posti diversi perché il dispositivo 1 avvia l'accelerazione prima.
Possiamo prendere il tempo impiegato per ricevere l'evento e convertirlo in frame. A 60 fps, ogni frame corrisponde a 16,67 ms, quindi possiamo aggiungere più velocità (accelerazione) o attrito (decelerazione) all'auto per tenere conto dei frame mancanti.
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();
}
}}
Nell'esempio precedente, se il giocatore 1 ha l'auto sullo schermo e il tempo necessario per ricevere il messaggio è inferiore a 75 ms, regolerà la velocità dell'auto, aumentandola per compensare la differenza. Se il dispositivo non è sullo schermo o il messaggio ha impiegato troppo tempo, verrà eseguita la funzione di rendering e l'auto verrà spostata dove deve essere.
Mantenere sincronizzate le auto
Anche dopo aver tenuto conto della latenza nell'accelerazione, l'auto potrebbe comunque non essere sincronizzata e apparire su più schermate contemporaneamente, in particolare durante il passaggio da un dispositivo all'altro. Per evitare questo problema, gli eventi di aggiornamento vengono inviati di frequente per mantenere le auto nella stessa posizione sul tracciato su tutti gli schermi.
La logica è che, ogni 4 frame, se l'auto è visibile sullo schermo, il dispositivo invia i suoi valori a ciascuno degli altri dispositivi. Se l'auto non è visibile, l'app aggiorna i valori con quelli ricevuti e poi sposta l'auto in base al tempo necessario per ricevere l'evento di aggiornamento.
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();
}
}
Conclusione
Non appena abbiamo sentito il concept di Racer, abbiamo capito che aveva il potenziale per essere un progetto molto speciale. Abbiamo creato rapidamente un prototipo che ci ha dato un'idea approssimativa di come superare la latenza e le prestazioni della rete. È stato un progetto impegnativo che ci ha tenuto occupati fino a tarda notte e durante i fine settimana lunghi, ma è stata un'esperienza fantastica quando il gioco ha iniziato a prendere forma. Alla fine, siamo molto soddisfatti del risultato finale. Il concept di Google Creative Lab ha spinto i limiti della tecnologia dei browser in modo divertente e, in qualità di sviluppatori, non potevamo chiedere di più.