Introduction
Racer est un test Chrome mobile basé sur le Web développé par Active Theory. Jusqu'à cinq amis peuvent connecter leurs téléphones ou tablettes pour courir sur tous les écrans. Armés du concept, de la conception et du prototype du Google Creative Lab, et du son de Plan8, nous avons itéré sur les builds pendant huit semaines avant le lancement à l'I/O 2013. Maintenant que le jeu est en ligne depuis quelques semaines, nous avons eu l'occasion de répondre à certaines questions de la communauté des développeurs sur son fonctionnement. Vous trouverez ci-dessous un récapitulatif des principales fonctionnalités et les réponses aux questions les plus fréquentes.
Le titre
Le défi évident auquel nous avons été confrontés était de créer un jeu mobile basé sur le Web qui fonctionne bien sur une grande variété d'appareils. Les joueurs devaient pouvoir créer une course avec différents téléphones et tablettes. Un joueur peut avoir un Nexus 4 et vouloir s'affronter avec son ami qui possède un iPad. Nous devions trouver un moyen de déterminer une taille de piste commune pour chaque course. La solution devait impliquer l'utilisation de pistes de différentes tailles en fonction des spécifications de chaque appareil inclus dans la course.
Calculer les dimensions des pistes
Lorsque chaque joueur rejoint la partie, des informations sur son appareil sont envoyées au serveur et partagées avec les autres joueurs. Lors de la création du canal, ces données sont utilisées pour calculer sa hauteur et sa largeur. Nous calculons la hauteur en déterminant la hauteur du plus petit écran, et la largeur correspond à la largeur totale de tous les écrans. Dans l'exemple ci-dessous, la piste aura une largeur de 1 152 pixels et une hauteur 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;
}
Dessin du tracé
Paper.js est un framework de script de graphiques vectoriels Open Source qui s'exécute sur HTML5 Canvas. Nous avons constaté que Paper.js était l'outil idéal pour créer des formes vectorielles pour les pistes. Nous avons donc utilisé ses fonctionnalités pour afficher les pistes SVG créées dans Adobe Illustrator sur un élément <canvas>
. Pour créer le rail, la classe TrackModel
ajoute le code SVG au DOM et collecte des informations sur les dimensions et le positionnement d'origine à transmettre à TrackPathView
, qui dessinera le rail sur un canevas.
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;
Une fois le canal dessiné, chaque appareil détermine son décalage X en fonction de sa position dans l'ordre de la ligne d'appareils, et positionne le canal en conséquence.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

Animations CSS
Paper.js utilise beaucoup de ressources de processeur pour dessiner les voies, et ce processus prend plus ou moins de temps sur différents appareils. Pour gérer cela, nous avons eu besoin d'un chargeur en boucle jusqu'à ce que tous les appareils aient terminé le traitement du titre. Le problème était que toute animation basée sur JavaScript sautait des frames en raison des exigences de processeur de Paper.js. Utilisez des animations CSS, qui s'exécutent sur un thread d'UI distinct, ce qui vous permet d'animer de manière fluide l'éclat sur le texte "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 CSS
Le CSS s'est également avéré utile pour les effets en jeu. Les appareils mobiles, dont la puissance est limitée, sont occupés à animer les voitures qui circulent sur les voies. Pour ajouter du piment, nous avons utilisé des sprites afin d'implémenter des animations prérendues dans le jeu. Dans un sprite CSS, les transitions appliquent une animation par étapes qui modifie la propriété background-position
, créant ainsi l'explosion de la voiture.
#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;
}
}
Le problème avec cette technique est que vous ne pouvez utiliser que des feuilles d'éléments graphiques disposées sur une seule ligne. Pour effectuer une boucle sur plusieurs lignes, l'animation doit être enchaînée via plusieurs déclarations d'images clés.
#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;
}
}
Affichage des voitures
Comme pour tout jeu de course automobile, nous savions qu'il était important de donner à l'utilisateur un sentiment d'accélération et de maniabilité. L'application d'une traction différente était importante pour l'équilibrage du jeu et le facteur de plaisir. Ainsi, une fois qu'un joueur a compris les principes physiques, il ressent un sentiment d'accomplissement et devient un meilleur pilote.
Nous avons de nouveau fait appel à Paper.js, qui est fourni avec un vaste ensemble d'utilitaires mathématiques. Nous avons utilisé certaines de ses méthodes pour déplacer la voiture le long du chemin, tout en ajustant sa position et sa rotation de manière fluide à chaque 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);
}
Lorsque nous optimisions le rendu des voitures, nous avons découvert un point intéressant. Sur iOS, les meilleures performances ont été obtenues en appliquant une transformation translate3d
à la voiture:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
Sur Chrome pour Android, les meilleures performances ont été obtenues en calculant les valeurs de la matrice et en appliquant une transformation matricielle:
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 + ')';
Maintenir la synchronisation des appareils
La partie la plus importante (et la plus difficile) du développement a consisté à s'assurer que le jeu était synchronisé sur tous les appareils. Nous pensions que les utilisateurs pourraient être indulgents si une voiture manquait occasionnellement quelques images en raison d'une connexion lente. Toutefois, ce ne serait pas très amusant si votre voiture sautait partout et apparaissait sur plusieurs écrans à la fois. Pour résoudre ce problème, nous avons dû procéder par tâtonnements, mais nous avons finalement trouvé quelques astuces qui ont fonctionné.
Calculer la latence
Le point de départ de la synchronisation des appareils consiste à connaître le temps nécessaire pour que les messages soient reçus à partir du relais Compute Engine. Le problème est que les horloges de chaque appareil ne seront jamais complètement synchronisées. Pour résoudre ce problème, nous avons dû déterminer la différence de temps entre l'appareil et le serveur.
Pour déterminer le décalage horaire entre l'appareil et le serveur principal, nous envoyons un message avec le code temporel actuel de l'appareil. Le serveur répond ensuite avec le code temporel d'origine ainsi que le code temporel du serveur. Nous utilisons la réponse pour calculer la différence de temps réelle.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Une seule fois n'est pas suffisant, car l'aller-retour vers le serveur n'est pas toujours symétrique, ce qui signifie qu'il peut s'écouler plus de temps pour que la réponse parvienne au serveur que pour que le serveur la renvoie. Pour contourner ce problème, nous interrogeons le serveur plusieurs fois et prenons le résultat médian. Nous obtenons ainsi une différence de 10 ms avec la différence réelle entre l'appareil et le serveur.
Accélération/Décélération
Lorsque le joueur 1 appuie ou relâche l'écran, l'événement d'accélération est envoyé au serveur. Une fois reçu, le serveur ajoute son code temporel actuel, puis transmet ces données à tous les autres joueurs.
Lorsqu'un événement "accélération activée" ou "accélération désactivée" est reçu par un appareil, nous pouvons utiliser le décalage du serveur (calculé ci-dessus) pour déterminer le temps nécessaire à la réception de ce message. Cela est utile, car le joueur 1 peut recevoir le message en 20 ms, mais le joueur 2 peut mettre 50 ms à le recevoir. La voiture se trouverait alors à deux endroits différents, car l'appareil 1 commencerait l'accélération plus tôt.
Nous pouvons convertir le temps nécessaire pour recevoir l'événement en images. À 60 images par seconde, chaque image correspond à 16,67 ms.Nous pouvons donc ajouter plus de vitesse (accélération) ou de friction (décélération) à la voiture pour tenir compte des images manquées.
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();
}
}}
Dans l'exemple ci-dessus, si le joueur 1 voit la voiture à l'écran et que le temps nécessaire pour recevoir le message est inférieur à 75 ms, il ajustera la vitesse de la voiture, en l'accélérant pour compenser la différence. Si l'appareil n'est pas à l'écran ou si le message a pris trop de temps, la fonction de rendu s'exécute et la voiture saute à l'endroit où elle doit se trouver.
Synchroniser les voitures
Même en tenant compte de la latence d'accélération, la voiture peut toujours être désynchronisée et apparaître sur plusieurs écrans à la fois, en particulier lors de la transition d'un appareil à un autre. Pour éviter cela, des événements de mise à jour sont envoyés fréquemment pour que les voitures restent à la même position sur la piste sur tous les écrans.
La logique est la suivante : tous les quatre frames, si la voiture est visible à l'écran, cet appareil envoie ses valeurs à chacun des autres appareils. Si la voiture n'est pas visible, l'application met à jour les valeurs avec celles reçues, puis fait avancer la voiture en fonction du temps qu'il a fallu pour obtenir l'événement de mise à jour.
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();
}
}
Conclusion
Dès que nous avons entendu le concept de Racer, nous avons su qu'il avait le potentiel d'être un projet très spécial. Nous avons rapidement créé un prototype qui nous a donné une idée approximative de la façon de surmonter les problèmes de latence et de performances réseau. Ce projet difficile nous a tenus occupés pendant de longues nuits et de longs week-ends, mais c'était un sentiment formidable lorsque le jeu a commencé à prendre forme. Au final, nous sommes très satisfaits du résultat. Le concept du Google Creative Lab a repoussé les limites de la technologie des navigateurs de manière amusante. En tant que développeurs, nous ne pouvions pas demander mieux.