Fallstudie – Racer aufbauen

Active Theory
Active Theory

Einleitung

Racer ist ein webbasiertes Chrome-Experiment für Mobilgeräte, das von Active Theory entwickelt wurde. Bis zu 5 Freunde können ihr Smartphone oder Tablet verbinden und auf jedem Bildschirm Rennen machen. Gerüstet mit dem Konzept, Design und Prototyp von Google Creative Lab und dem Sound von Plan8 haben wir vor der Markteinführung auf der I/O 2013 acht Wochen lang Builds iteriert. Da das Spiel nun schon seit einigen Wochen verfügbar ist, konnten wir von der Entwickler-Community einige Fragen zur Funktionsweise stellen. Im Folgenden finden Sie eine Übersicht über die wichtigsten Funktionen und Antworten auf die am häufigsten gestellten Fragen.

Track

Eine ziemlich offensichtliche Herausforderung für uns bestand darin, ein webbasiertes Spiel für Mobilgeräte zu entwickeln, das auf einer Vielzahl von Geräten gut funktioniert. Spieler mussten in der Lage sein, ein Rennen mit verschiedenen Smartphones und Tablets zu erstellen. Ein Spieler könnte ein Nexus 4 haben und möchte gegen seinen Freund wetteifern, der ein iPad hat. Wir mussten eine Methode finden, um für jedes Rennen eine gemeinsame Streckengröße zu bestimmen. Die Lösung musste je nach den Spezifikationen des am Rennen teilnehmenden Geräts unterschiedlich große Strecken umfassen.

Titelabmessungen berechnen

Sobald ein Spieler beitritt, werden Informationen zu seinem Gerät an den Server gesendet und mit anderen Spielern geteilt. Beim Bau des Tracks werden diese Daten verwendet, um Höhe und Breite des Tracks zu berechnen. Wir berechnen die Höhe, indem wir die Höhe des kleinsten Bildschirms ermitteln. Die Breite ist die Gesamtbreite aller Bildschirme. Im Beispiel unten hätte der Track also eine Breite von 1152 Pixel und eine Höhe von 519 Pixel.

Der rote Bereich zeigt in diesem Beispiel die Gesamtbreite und -höhe des Tracks an.
Der rote Bereich zeigt die Gesamtbreite und -höhe des Tracks in diesem Beispiel an.
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;
}

Track zeichnen

Paper.js ist ein Open-Source-Framework für Vektorgrafiken, das auf HTML5 Canvas ausgeführt wird. Wir haben festgestellt, dass Paper.js das perfekte Tool zum Erstellen von Vektorformen für die Tracks ist. Deshalb haben wir damit die SVG-Tracks gerendert, die in Adobe Illustrator auf einem <canvas>-Element erstellt wurden. Zum Erstellen des Tracks hängt die TrackModel-Klasse den SVG-Code an das DOM an und erfasst Informationen zu den ursprünglichen Abmessungen und zur Positionierung, die an das TrackPathView-Element übergeben werden, wodurch der Track auf einem Canvas dargestellt wird.

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;

Nachdem der Track gezeichnet wurde, ermittelt jedes Gerät seinen x-Versatz basierend auf seiner Position in der Geräte-Lineup-Reihenfolge und positioniert den Track entsprechend.

var x = 0;

for (var i = 0; i < screens.length; i++) {
  if (i < PLAYER_INDEX) {
    x += screens[i].w;
  }
}
Der x-Offset kann dann verwendet werden, um den entsprechenden Teil des Tracks anzuzeigen.
Der x-Offset kann dann verwendet werden, um den entsprechenden Teil des Tracks anzuzeigen.

CSS-Animationen

Paper.js nutzt viel CPU-Verarbeitung zum Einzeichnen der Spuren, was auf den verschiedenen Geräten mehr oder weniger Zeit in Anspruch nimmt. Dazu brauchten wir ein Ladeprogramm, das so lange in einer Schleife wiedergegeben wird, bis alle Geräte den Track verarbeitet haben. Das Problem bestand darin, dass jede JavaScript-basierte Animation aufgrund der CPU-Anforderungen von Paper.js Frames überspringte. Geben Sie CSS-Animationen ein, die auf einem separaten UI-Thread ausgeführt werden, sodass wir den Glanz des „Building TRACK“-Text reibungslos animieren können.

.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);
  }
}
}

CSS-Sprite

CSS war auch bei In-Game-Effekten praktisch. Die Mobilgeräte mit ihrer begrenzten Leistung werden ständig beschäftigt, um die Autos zu animieren, die über die Gleise fahren. Wir haben also Sprites verwendet, um vorgerenderte Animationen in das Spiel zu integrieren. In einem CSS-Sprite wird bei Übergängen eine schrittbasierte Animation angewendet, durch die die Eigenschaft background-position geändert wird, wodurch die Autoexplosion entsteht.

#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;
  }
}

Das Problem bei dieser Technik ist, dass Sie nur Sprite Sheets verwenden können, die auf einer einzelnen Zeile angeordnet sind. Wenn Sie mehrere Zeilen als Schleife wiedergeben möchten, muss die Animation über mehrere Keyframe-Deklarationen verkettet sein.

#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;
  }
}

Autos rendern

Wie bei jedem Rennspiel wussten wir, dass es den Nutzern ein Gefühl von Beschleunigung und Fahrgefühl geben sollte. Ein unterschiedliches Maß an Zugkraft war wichtig für das Game-Balancing und den Spaßfaktor. Sobald ein Spieler ein Gefühl für die Physik hatte, erfuhr er, dass er ein Erfolgserlebnis hat und ein besserer Rennfahrer wurde.

Nochmals haben wir Paper.js aufgerufen, das ein umfangreiches Set an mathematischen Dienstprogrammen enthält. Wir verwendeten einige dieser Methoden, um das Auto entlang des Pfads zu bewegen, während wir die Position und Drehung jedes Frames gleichmäßig anpassten.

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);
}

Bei der Optimierung des Autorenderings haben wir einen interessanten Punkt gefunden. Unter iOS wurde die beste Leistung durch Anwendung einer translate3d-Transformation auf das Auto erreicht:

_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';

In Chrome für Android wurde die beste Leistung durch Berechnung der Matrixwerte und Anwenden einer Matrixtransformation erreicht:

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 + ')';

Synchronisierung der Geräte

Der wichtigste (und schwierigste) Teil der Entwicklung war die geräteübergreifende Synchronisierung des Spiels. Wir dachten, Nutzer könnten verzeihen, wenn ein Auto aufgrund einer langsamen Verbindung hin und wieder ein paar Frames übersprungen hat. Es wäre aber nicht fröhlich, wenn dein Auto auf mehreren Bildschirmen gleichzeitig angezeigt wird. Die Lösung des Problems erforderte viel Versuch und Irrtum, aber schließlich haben wir uns auf ein paar Tricks geeinigt, sodass es funktioniert hat.

Latenz berechnen

Als Ausgangspunkt für die Synchronisierung von Geräten müssen Sie wissen, wie lange es dauert, bis Nachrichten vom Compute Engine-Relay empfangen werden. Das Schwierige ist, dass die Uhren auf jedem Gerät nie vollständig synchron sind. Um dieses Problem zu umgehen, mussten wir den Zeitunterschied zwischen dem Gerät und dem Server ermitteln.

Um die Zeitverschiebung zwischen dem Gerät und dem Hauptserver zu ermitteln, senden wir eine Nachricht mit dem aktuellen Gerätezeitstempel. Der Server antwortet dann mit dem ursprünglichen Zeitstempel und dem Zeitstempel des Servers. Wir verwenden die Antwort, um die tatsächliche Zeitdifferenz zu berechnen.

var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;

Es reicht nicht aus, dies nur einmal zu tun, da der Umlauf zum Server nicht immer symmetrisch ist. Daher kann es länger dauern, bis die Antwort an den Server gesendet wird, als vom Server zurückgegeben. Um dieses Problem zu umgehen, fragen wir den Server mehrmals ab und verwenden dabei das Medianwert. Dadurch können wir maximal 10 ms von der tatsächlichen Differenz zwischen Gerät und Server entfernt sein.

Beschleunigung/Verlangsamung

Wenn Spieler 1 den Bildschirm drückt oder wieder loslässt, wird das Beschleunigungsereignis an den Server gesendet. Nach dem Empfang fügt der Server seinen aktuellen Zeitstempel hinzu und übergibt diese Daten dann an alle anderen Spieler.

Wenn ein Gerät ein Ereignis vom Typ „Beschleunigen an“ oder „Beschleunigen aus“ empfängt, können wir den Serverversatz (oben berechnet) verwenden, um herauszufinden, wie lange es gedauert hat, bis die Nachricht empfangen wurde. Das ist nützlich, da Spieler 1 die Nachricht in 20 ms empfangen kann, während Spieler 2 möglicherweise 50 ms benötigt, um sie zu empfangen. Dies würde dazu führen, dass sich das Auto an zwei verschiedenen Stellen befindet, da Gerät 1 die Beschleunigung früher starten würde.

Wir können die Zeit, die bis zum Empfang des Ereignisses gedauert hat, in Frames umwandeln. Bei 60 fps beträgt jeder Frame 16,67 ms.So können wir die Geschwindigkeit (Beschleunigung) oder Reibung (Verlangsamung) des Autos erhöhen, um die verpassten Frames zu berücksichtigen.

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();
  }
}}

Wenn im obigen Beispiel Spieler 1 das Auto auf dem Bildschirm anzeigt und die Zeit bis zum Empfang der Nachricht weniger als 75 ms beträgt, passt er die Geschwindigkeit des Autos an und beschleunigt es, um die Differenz auszugleichen. Wenn das Gerät nicht auf dem Bildschirm zu sehen ist oder die Nachricht zu lange gedauert hat, wird die Rendering-Funktion ausgeführt und das Auto an die gewünschte Stelle gebracht.

Synchronisierung der Autos

Auch wenn Sie die Latenz bei der Beschleunigung einkalkulieren, kann das Auto dennoch nicht mehr synchron sein und auf mehreren Bildschirmen gleichzeitig angezeigt werden, insbesondere beim Wechsel von einem Gerät zum nächsten. Um dies zu verhindern, werden regelmäßig Update-Ereignisse gesendet, damit die Autos auf allen Bildschirmen an derselben Position bleiben.

Wenn das Auto auf dem Bildschirm zu sehen ist, sendet dieses Gerät alle vier Frames seine Werte an jedes andere Gerät. Wenn das Auto nicht sichtbar ist, aktualisiert die App die Werte mit den empfangenen Werten und fährt das Fahrzeug basierend auf der Zeit, die für das Update-Ereignis benötigt wurde, weiter.

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();
  }
}

Fazit

Schon als wir das Konzept für Racer hörten, wussten wir, dass es das Potenzial für ein ganz besonderes Projekt hat. Wir erstellten schnell einen Prototyp, der uns eine ungefähre Vorstellung davon gab, wie Latenz und Netzwerkleistung vermieden werden können. Es war ein anspruchsvolles Projekt, das uns in den späten Nächten und langen Wochenenden sehr beschäftigt hielt, aber es war ein tolles Gefühl, als das Spiel Gestalt annahm. Letztendlich sind wir mit dem Endergebnis sehr zufrieden. Mit dem Konzept von Google Creative Lab wurden die Grenzen der Browsertechnologie auf unterhaltsame Weise erweitert, und als Entwickler können wir uns nicht mehr verlangen.