Einführung
Racer ist ein webbasiertes mobiles Chrome-Experiment, das von Active Theory entwickelt wurde. Bis zu fünf Freunde können ihre Smartphones oder Tablets verbinden und auf allen Bildschirmen gegeneinander antreten. Ausgestattet mit dem Konzept, dem Design und dem Prototyp von Google Creative Lab und dem Sound von Plan8 haben wir acht Wochen lang an den Builds gearbeitet, die zur Einführung auf der I/O 2013 führen sollten. Nachdem das Spiel nun seit einigen Wochen online ist, haben wir die Gelegenheit bekommen, einige Fragen von der Entwickler-Community zur Funktionsweise zu beantworten. Im Folgenden finden Sie eine Übersicht über die wichtigsten Funktionen und Antworten auf die am häufigsten gestellten Fragen.
Der Track
Eine ziemlich offensichtliche Herausforderung bestand darin, ein webbasiertes Mobilspiel zu entwickeln, das auf einer Vielzahl von Geräten gut funktioniert. Die Spieler mussten in der Lage sein, ein Rennen mit verschiedenen Smartphones und Tablets zu erstellen. Ein Spieler könnte ein Nexus 4 haben und gegen seinen Freund antreten, der ein iPad hat. Wir mussten eine Möglichkeit finden, eine gemeinsame Streckengröße für jedes Rennen zu bestimmen. Die Lösung musste je nach Spezifikationen der einzelnen Geräte im Rennen unterschiedliche Spurgrößen verwenden.
Track-Dimensionen berechnen
Wenn ein Spieler beitritt, werden Informationen zu seinem Gerät an den Server gesendet und mit anderen Spielern geteilt. Beim Bau der Strecke werden diese Daten verwendet, um die Höhe und Breite der Strecke zu berechnen. Die Höhe wird anhand der Höhe des kleinsten Bildschirms berechnet. Die Breite entspricht der Gesamtbreite aller Bildschirme. Im Beispiel unten hätte der Titel also eine Breite von 1.152 Pixeln und eine Höhe von 519 Pixeln.

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-Scripting-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 die Funktionen des Tools verwendet, um die SVG-Tracks zu rendern, die in Adobe Illustrator in einem <canvas>
-Element erstellt wurden. Um den Track zu erstellen, 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 die TrackPathView
übergeben werden, die den Track auf einem Canvas zeichnet.
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-Offset basierend auf seiner Position in der Gerätereihenfolge und positioniert den Track entsprechend.
var x = 0;
for (var i = 0; i < screens.length; i++) {
if (i < PLAYER_INDEX) {
x += screens[i].w;
}
}

CSS-Animationen
Paper.js benötigt viel CPU-Leistung, um die Fahrstreifen zu zeichnen. Dieser Vorgang dauert auf verschiedenen Geräten unterschiedlich lang. Dazu benötigten wir einen Lader, der in einer Schleife läuft, bis alle Geräte die Verarbeitung des Titels abgeschlossen haben. Das Problem war, dass bei jeder JavaScript-basierten Animation Frames aufgrund der CPU-Anforderungen von Paper.js übersprungen wurden. Hier kommen CSS-Animationen ins Spiel, die in einem separaten UI-Thread ausgeführt werden. So konnten wir den Glanz über den Text „BUILDING TRACK“ flüssig animieren.
.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-Sprites
CSS eignet sich auch gut für In-Game-Effekte. Die Akkus von Mobilgeräten sind aufgrund der Animation der über die Gleise fahrenden Züge schnell leer. Um das Spiel noch spannender zu machen, haben wir Sprites verwendet, um vorab gerenderte Animationen in das Spiel einzubinden. In einem CSS-Sprite wird bei Übergängen eine schrittweise Animation angewendet, die die Eigenschaft background-position
ändert und so die Autoexplosion erzeugt.
#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 in einer einzigen Zeile angeordnet sind. Damit die Animation durch mehrere Zeilen iteriert wird, müssen mehrere Keyframe-Deklarationen miteinander verknüpft werden.
#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 war es uns wichtig, den Nutzern ein Gefühl für Beschleunigung und Fahrverhalten zu vermitteln. Die unterschiedlichen Traktionsgrade waren wichtig für das Spielbalancing und den Spaßfaktor. Sobald ein Spieler ein Gefühl für die Physik entwickelt hat, sollte er ein Erfolgserlebnis haben und ein besserer Rennfahrer werden.
Wir haben wieder Paper.js verwendet, das eine umfangreiche Auswahl an mathematischen Dienstprogrammen bietet. Wir haben einige seiner Methoden verwendet, um das Auto entlang des Pfades zu bewegen und dabei die Position und Drehung des Autos in jedem Frame reibungslos anzupassen.
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);
}
Beim Optimieren des Auto-Renderings haben wir einen interessanten Punkt gefunden. Unter iOS wurde die beste Leistung erzielt, indem eine translate3d
-Transformation auf das Auto angewendet wurde:
_car.style.webkitTransform = 'translate3d('+_position.x+'px, '+_position.y+'px, 0px)rotate('+_rotation+'deg)';
In Chrome für Android wurde die beste Leistung durch das Berechnen der Matrixwerte und das Anwenden einer Matrixtransformation erzielt:
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 + ')';
Geräte synchronisieren
Der wichtigste (und schwierigste) Teil der Entwicklung bestand darin, dafür zu sorgen, dass das Spiel auf allen Geräten synchronisiert wird. Wir dachten, dass Nutzer nachsichtig sein würden, wenn ein Auto aufgrund einer langsamen Verbindung gelegentlich ein paar Frames überspringt. Es wäre aber nicht sehr unterhaltsam, wenn das Auto herumspringt und auf mehreren Bildschirmen gleichzeitig erscheint. Die Lösung dieses Problems erforderte viel Ausprobieren, aber wir haben uns schließlich auf ein paar Tricks geeinigt, die es möglich gemacht haben.
Latenz berechnen
Der Ausgangspunkt für die Gerätesynchronisierung ist die Kenntnis darüber, wie lange es dauert, bis Nachrichten vom Compute Engine-Relay empfangen werden. Das Problem dabei ist, dass die Uhren auf den einzelnen Geräten nie vollständig synchron sind. Um das zu umgehen, mussten wir die Zeitdifferenz zwischen dem Gerät und dem Server ermitteln.
Um die Zeitdifferenz 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. Anhand der Antwort wird die tatsächliche Zeitdifferenz berechnet.
var currentTime = Date.now();
var latency = Math.round((currentTime - e.time) * .5);
var serverTime = e.serverTime;
currentTime -= latency;
var difference = currentTime - serverTime;
Das ist nicht ausreichend, da die Hin- und Rücklaufzeit zum Server nicht immer symmetrisch ist. Das bedeutet, dass es möglicherweise länger dauert, bis die Antwort beim Server ankommt, als bis der Server sie zurückgibt. Um dies zu vermeiden, fragen wir den Server mehrmals ab und nehmen den Medianwert. So sind wir innerhalb von 10 ms vom tatsächlichen Unterschied zwischen Gerät und Server.
Beschleunigung/Bremsung
Wenn Spieler 1 auf den Bildschirm drückt oder loslässt, wird das Beschleunigungsereignis an den Server gesendet. Nach dem Empfang fügt der Server seinen aktuellen Zeitstempel hinzu und leitet die Daten an alle anderen Spieler weiter.
Wenn ein Gerät ein Ereignis vom Typ „Beschleunigung aktivieren“ oder „Beschleunigung deaktivieren“ empfängt, können wir anhand des oben berechneten Serveroffsets ermitteln, wie lange es gedauert hat, bis diese Nachricht empfangen wurde. Das ist nützlich, da Spieler 1 die Nachricht möglicherweise in 20 ms erhält, Spieler 2 aber 50 ms dafür benötigt. Das würde dazu führen, dass sich das Auto an zwei verschiedenen Orten befindet, da Gerät 1 die Beschleunigung früher startet.
Wir können die Zeit, die zum Empfang des Ereignisses benötigt wurde, in Frames umwandeln. Bei 60 fps entspricht jeder Frame 16,67 ms.Wir können dem Auto also mehr Geschwindigkeit (Beschleunigung) oder Reibung (Bremsung) hinzufügen, um die fehlenden Frames auszugleichen.
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 Spieler 1 im obigen Beispiel das Auto auf dem Bildschirm hat und die Zeit, die zum Empfang der Nachricht benötigt wurde, weniger als 75 ms beträgt, wird die Geschwindigkeit des Autos angepasst, um die Differenz auszugleichen. Wenn sich das Gerät nicht auf dem Bildschirm befindet oder die Nachricht zu lange gedauert hat, wird die Rendering-Funktion ausgeführt und das Auto springt an die gewünschte Stelle.
Synchronisierung der Autos aufrechterhalten
Selbst wenn die Verzögerung bei der Beschleunigung berücksichtigt wird, kann es passieren, dass das Auto nicht synchron ist und auf mehreren Bildschirmen gleichzeitig erscheint, insbesondere beim Wechsel von einem Gerät zum nächsten. Um dies zu verhindern, werden häufig Aktualisierungsereignisse gesendet, damit die Autos auf allen Bildschirmen an derselben Position auf der Strecke bleiben.
Wenn das Auto auf dem Bildschirm sichtbar ist, sendet das Gerät alle vier Frames seine Werte an alle anderen Geräte. Wenn das Auto nicht sichtbar ist, aktualisiert die App die Werte mit den empfangenen und bewegt das Auto dann entsprechend der Zeit, die für das Update-Ereignis benötigt wurde.
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
Als wir das Konzept für Racer hörten, wussten wir, dass es das Potenzial zu einem ganz besonderen Projekt hat. Wir haben schnell einen Prototyp erstellt, der uns eine grobe Vorstellung davon gab, wie wir Latenz und Netzwerkleistung überwinden können. Es war ein herausforderndes Projekt, das uns an langen Nächten und Wochenenden beschäftigt hat. Es war aber ein tolles Gefühl, als das Spiel Gestalt annahm. Letztendlich sind wir sehr zufrieden mit dem Endergebnis. Das Konzept von Google Creative Lab hat die Grenzen der Browsertechnologie auf spielerische Weise erweitert. Als Entwickler können wir uns nichts Besseres wünschen.