JavaScriptsSpiele

Asteroids

Ein Raumschiff fliegt über die Seite, auf der es steht — und schießt sie Element für Element in Trümmer.

Asteroids war Ende der siebziger Jahre einer der ersten großen Arkade-Hits: ein Raumschiff im Weltall, das treibende Felsbrocken zerschießt. Erik Rothoff Andersson hat aus dieser Idee ein JavaScript gemacht, das den Spieß umdreht — die Asteroiden sind hier die Bausteine der Webseite selbst. Wir haben es damals auf erkie.github.io gefunden und ins Archiv aufgenommen.

Ein Klick auf „Spiel starten“ legt eine unsichtbare Spielfläche über die ganze Seite und setzt das Raumschiff oben links ab. Gesteuert wird mit den Pfeiltasten: Pfeil hoch gibt Schub, links und rechts drehen das Schiff. Wer lieber die linke Hand benutzt, nimmt W, A und D. Die Leertaste feuert, und jedes getroffene Element verschwindet mit einer kleinen Funkenwolke aus der Seite. Der Punktestand steht unten rechts, zehn Punkte pro Treffer. Solltet ihr die Ziele aus den Augen verlieren, hält die Taste B alle zerstörbaren Elemente rot blinkend umrandet.

Beenden könnt ihr das Spiel jederzeit mit ESC. Alles, was ihr abgeschossen habt, ist damit natürlich noch nicht wieder da — die Seite ist ja wirklich verändert worden. Ein Neuladen mit F5 stellt sie vollständig her. Solange das Spiel läuft, ist die Seite nicht bedienbar; ihr solltet also erst lesen und dann schießen.

Technisch ist das Script eine schöne Fundgrube. Es zeichnet auf einem canvas-Element, arbeitet mit Vektoren für Position, Richtung und Geschwindigkeit, und die Trefferabfrage funktioniert nicht über Formen, sondern über document.elementFromPoint — der Browser selbst verrät, was an einer Bildschirmstelle liegt. Wer wissen will, wie eine Spielschleife und eine einfache Flugphysik aufgebaut sind, findet weiter unten eine Erklärung der wichtigsten Stellen.

Auf einer eigenen Seite eingebaut ist das Script vor allem ein Gag für Besucher — an eine Aprilscherz-Seite oder eine Fehlerseite gehängt, sorgt es zuverlässig für Verblüffung. Wer klassisches Arkade-Gefühl ohne Kollateralschaden sucht, findet in derselben Kategorie BreakOut, Pong und Snake.

Spiele Baujahr 2010 läuft in deinem Browser

Steuerung: Pfeiltasten (oder W, A, D) fliegen und drehen, die Leertaste schießt, B hebt alle zerstörbaren Elemente hervor, ESC beendet das Spiel. Ein Neuladen der Seite mit F5 stellt alles wieder her.

Script von Erik Rothoff Andersson

So funktioniert das Script

Das Script ist ungewöhnlich lang, aber gut sortiert. Es besteht aus vier Bausteinen: einem kleinen Rechenwerkzeug für Vektoren (Punkte und Richtungen), der Suche nach den zerstörbaren Elementen der Seite, einer Spielschleife, die fünfzigmal pro Sekunde alles ein Stück weiterbewegt, und den Zeichenbefehlen für das Canvas. Wir gehen die zentralen Mechanismen an je einem kurzen Ausschnitt durch — den Rest könnt ihr im Code darunter in Ruhe nachlesen.

Die Spielschleife

this.update = function () {
  const nowTime = new Date().getTime();
  const tDelta = (nowTime - lastUpdate) / 1000;
  lastUpdate = nowTime;
  // ... rechnen und zeichnen ...
  setTimeout(updateFunc, 1000 / FPS);
};

Die Funktion update() ist das Herz des Spiels. Ganz am Ende ruft sie sich über setTimeout selbst wieder auf — bei FPS = 50 also etwa alle 20 Millisekunden. So entsteht eine Endlosschleife, die den Browser trotzdem nicht blockiert.

Wichtig ist die Zeile mit tDelta (gesprochen „Delta t“, die verstrichene Zeit): Sie misst, wie viele Sekunden seit dem letzten Durchlauf wirklich vergangen sind. Alle Bewegungen werden anschließend mit diesem Wert multipliziert. Dadurch fliegt das Raumschiff auf einem schnellen und auf einem langsamen Rechner gleich schnell — der langsame Rechner macht einfach größere Sprünge.

Bewegung und Physik

if (this.keysPressed[code("up")] || this.keysPressed[code("W")]) {
  this.vel.add(this.dir.mulNew(acc * tDelta));
  drawFlame = true;
} else {
  this.vel.mul(0.96);
}

Das Raumschiff hat zwei Vektoren: dir zeigt, wohin die Nase blickt, vel (von velocity, Geschwindigkeit) beschreibt, wohin es sich tatsächlich bewegt. Wird Schub gegeben, kommt ein Stück Richtung zur Geschwindigkeit hinzu — deshalb add. Ohne Schub wird die Geschwindigkeit pro Durchlauf mit 0.96 multipliziert, sie schrumpft also um vier Prozent. Das ist die Reibung, die das Schiff langsam ausrollen lässt.

if (this.vel.len() > maxSpeed) this.vel.setLength(maxSpeed);
this.pos.add(this.vel.mulNew(tDelta));

Danach wird die Geschwindigkeit auf maxSpeed gedeckelt und erst dann auf die Position addiert. Genau dieses Nacheinander von Beschleunigen, Bremsen und Verschieben macht das typische, etwas schlitternde Asteroids-Fahrgefühl aus. Wer lieber ein trägeres Schiff möchte, verkleinert oben acc (die Beschleunigung); wer eine Eisfläche will, setzt den Reibungsfaktor 0.96 näher an 1.

Der Rand des Bildschirms

if (this.pos.x > w) {
  window.scrollTo(this.scrollPos.x + 50, this.scrollPos.y);
  this.pos.x = 0;
} else if (this.pos.x < 0) {
  window.scrollTo(this.scrollPos.x - 50, this.scrollPos.y);
  this.pos.x = w;
}

Verlässt das Schiff rechts das Bild, taucht es links wieder auf — und die Seite scrollt zugleich ein Stück weiter. So kommt ihr auch an Elemente heran, die weiter unten stehen. w und h sind Breite und Höhe des sichtbaren Fensters; sie werden bei jeder Größenänderung neu bestimmt.

Die Tastatursteuerung

function keyName(event) {
  const key = event.key || "";
  return key.length === 1 ? key.toUpperCase() : key;
}

function eventKeydown(event) {
  const key = keyName(event);
  that.keysPressed[key] = true;
  if (key === code(" ")) that.firedAt = 1;
  ...
}

Das Script merkt sich nicht, dass eine Taste gedrückt wurde, sondern welche gerade gedrückt ist: keysPressed ist ein Objekt, in dem beim keydown ein Eintrag auf true und beim keyup wieder auf false gesetzt wird. Die Spielschleife schaut dann bei jedem Durchlauf nach, was gerade anliegt. Nur so lassen sich mehrere Tasten gleichzeitig halten — fliegen, drehen und schießen in einem.

event.key liefert den Namen der Taste als Text: "ArrowUp", "Escape", " " für die Leertaste oder eben "w". Die Hilfsfunktion keyName() schreibt einzelne Buchstaben groß, damit w und W dieselbe Taste sind. Früher wurde dafür die inzwischen veraltete Nummer event.keyCode benutzt.

Was ist getroffen?

function getElementFromPoint(x, y) {
  applyVisibility("hidden");
  let element = document.elementFromPoint(x, y);
  if (!element) {
    applyVisibility("visible");
    return false;
  }
  if (element.nodeType === 3) element = element.parentNode;
  applyVisibility("visible");
  return element;
}

Hier steckt der eigentliche Trick des Spiels. Statt Formen zu vergleichen, fragt das Script den Browser: document.elementFromPoint(x, y) liefert das Element, das an dieser Bildschirmstelle liegt. Weil das Spiel-Canvas über der ganzen Seite liegt, würde es sich selbst zurückbekommen — deshalb wird die Spielfläche für einen Augenblick auf visibility: hidden gesetzt und gleich danach wieder sichtbar gemacht. Sehen kann man das nicht, es passiert innerhalb desselben Durchlaufs.

const murdered = getElementFromPoint(this.bullets[i].pos.x, this.bullets[i].pos.y);
if (murdered && murdered.tagName &&
    indexOf(ignoredTypes, murdered.tagName.toUpperCase()) === -1 &&
    hasOnlyTextualChildren(murdered) &&
    murdered.className !== "ASTEROIDSYEAH") {
  addParticles(this.bullets[i].pos);
  this.dying.push(murdered);
  this.bullets.splice(i, 1);
}

Bevor etwas zerstört wird, prüft das Script drei Dinge: Das Element darf nicht in der Liste ignoredTypes stehen (dort stehen HTML, BODY, SCRIPT und ähnliche Bausteine, ohne die die Seite zusammenbräche), es darf nur Text enthalten und nicht zum Spiel selbst gehören. Trifft alles zu, wandert es in die Liste dying, wird ein Stück weiter unten mit removeChild aus der Seite entfernt und der Punktestand um zehn erhöht. Zugleich sorgt addParticles für die rot-gelben Funken.

Zeichnen auf dem Canvas

this.ctx.drawPlayer = function () {
  this.save();
  this.translate(that.pos.x, that.pos.y);
  this.rotate(that.dir.angle());
  this.tracePoly(playerVerts);
  this.fillStyle = "white";
  this.fill();
  this.tracePoly(playerVerts);
  this.stroke();
  this.restore();
};

Das Raumschiff ist ein Dreieck aus drei Eckpunkten (playerVerts). Gezeichnet wird es immer an derselben Stelle — nämlich um den Nullpunkt herum. Verschoben und gedreht wird stattdessen das Koordinatensystem: translate rückt den Nullpunkt an die Schiffsposition, rotate dreht ihn in Flugrichtung. save() und restore() merken sich den Zustand davor und stellen ihn wieder her, damit die nächste Zeichenoperation wieder normal beginnt. Das ist ein Muster, das euch bei fast jeder Canvas-Animation begegnet.

Was sich leicht ändern lässt

const FPS = 50;
const acc = 300;
const maxSpeed = 600;
const rotSpeed = 360;
const bulletSpeed = 700;
const timeBetweenFire = 150;
const maxBullets = 20;

Ziemlich weit oben im Script stehen alle Stellschrauben beieinander. rotSpeed ist die Drehgeschwindigkeit in Grad pro Sekunde, timeBetweenFire der Mindestabstand zwischen zwei Schüssen in Millisekunden — kleiner heißt Dauerfeuer. Auch die Farben der Funken (["red", "yellow"] in randomParticleColor) und die Größe des Schiffs (playerWidth, playerHeight) lassen sich gefahrlos anpassen.

Script für die eigene Homepage

Kopiert euch den kompletten Code und fügt ihn an der Stelle eurer Seite ein, an der das Script erscheinen soll. Er läuft ohne weitere Dateien und ohne fremde Server.

Mehr aus der Kategorie Spiele