Leitfaden für HTML5-Spiele ohne viel Aufwand

Daniel X. Moore
Daniel X. Moore

Einführung

Sie möchten also ein Spiel mit Canvas und HTML5 erstellen? Folgen Sie dieser Anleitung und Sie sind im Handumdrehen startklar.

Für diese Anleitung sollten Sie mindestens grundlegende Kenntnisse in JavaScript haben.

Sie können zuerst das Spiel spielen oder direkt zum Artikel springen und sich den Quellcode für das Spiel ansehen.

Canvas erstellen

Damit wir etwas zeichnen können, müssen wir zuerst einen Canvas erstellen. Da dies ein Leitfaden für Anfänger ist, verwenden wir jQuery.

var CANVAS_WIDTH = 480;
var CANVAS_HEIGHT = 320;

var canvasElement = $("<canvas width='" + CANVAS_WIDTH + 
                      "' height='" + CANVAS_HEIGHT + "'></canvas>");
var canvas = canvasElement.get(0).getContext("2d");
canvasElement.appendTo('body');

Spielschleife

Um ein flüssiges und kontinuierliches Gameplay zu simulieren, möchten wir das Spiel aktualisieren und den Bildschirm nur schneller neu zeichnen, als das menschliche Gehirn und Auge wahrnehmen können.

var FPS = 30;
setInterval(function() {
  update();
  draw();
}, 1000/FPS);

Wir können die Methoden „update“ und „draw“ vorerst leer lassen. Wichtig ist, dass setInterval() sie regelmäßig aufruft.

function update() { ... }
function draw() { ... }

Hello World

Jetzt, da wir einen Gameloop haben, aktualisieren wir unsere draw-Methode, um Text auf dem Bildschirm zu zeichnen.

function draw() {
  canvas.fillStyle = "#000"; // Set color to black
  canvas.fillText("Sup Bro!", 50, 50);
}

Das ist ziemlich cool für statischen Text. Da wir aber bereits einen Gameloop eingerichtet haben, sollten wir ihn ganz einfach bewegen können.

var textX = 50;
var textY = 50;

function update() {
  textX += 1;
  textY += 1;
}

function draw() {
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Probieren Sie es jetzt aus. Wenn Sie der Anleitung folgen, sollte sich die Linie bewegen, aber auch die vorherigen Mal, als sie gezeichnet wurde, auf dem Bildschirm hinterlassen. Überlegen Sie kurz, warum das so sein könnte. Das liegt daran, dass wir das Display nicht löschen. Fügen wir der draw-Methode also Code zum Löschen des Bildschirms hinzu.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  canvas.fillStyle = "#000";
  canvas.fillText("Sup Bro!", textX, textY);
}

Jetzt, da sich Text auf dem Bildschirm bewegt, sind Sie auf dem Weg zu einem echten Spiel. Sie müssen nur die Steuerung optimieren, das Gameplay verbessern und die Grafik aufpolieren. Okay, vielleicht sind wir jetzt zu einem Siebtel des Wegs zu einem echten Spiel. Aber es gibt noch viel mehr zu entdecken.

Player erstellen

Erstellen Sie ein Objekt, das die Spielerdaten enthält und für Dinge wie das Zeichnen verantwortlich ist. Hier erstellen wir ein Spielerobjekt mit einem einfachen Objektliteral, das alle Informationen enthält.

var player = {
  color: "#00A",
  x: 220,
  y: 270,
  width: 32,
  height: 32,
  draw: function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  }
};

Wir verwenden vorerst ein einfaches farbiges Rechteck, um den Spieler darzustellen. Wenn wir das Spiel zeichnen, löschen wir den Canvas und zeichnen den Spieler.

function draw() {
  canvas.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  player.draw();
}

Tastatursteuerung

jQuery-Hotkeys verwenden

Das jQuery-Hotkeys-Plug-in macht die Schlüsselverwaltung in verschiedenen Browsern viel einfacher. Anstatt uns über unleserliche keyCode- und charCode-Probleme zu beschweren, die sich nicht auf einen Browser beschränken, können wir Ereignisse so binden:

$(document).bind("keydown", "left", function() { ... });

Es ist ein großer Vorteil, sich nicht um die Details kümmern zu müssen, welche Schlüssel welche Codes haben. Wir möchten einfach Dinge wie „Wenn der Spieler die Aufwärtstaste drückt, tue etwas“ sagen können. Mit jQuery Hotkeys ist das ganz einfach möglich.

Spielerbewegung

Die Verarbeitung von Tastatur-Ereignissen in JavaScript ist vollständig ereignisgesteuert. Das bedeutet, dass es keine integrierte Abfrage gibt, mit der geprüft werden kann, ob ein Schlüssel ausgefallen ist. Wir müssen also unsere eigene Abfrage verwenden.

Sie fragen sich vielleicht: „Warum nicht einfach eine ereignisgesteuerte Methode zur Schlüsselverwaltung verwenden?“ Das liegt daran, dass die Wiederholungsrate der Tastatur von System zu System variiert und nicht an das Timing des Gameloops gebunden ist. Das Gameplay kann also von System zu System stark variieren. Für eine einheitliche Benutzeroberfläche ist es wichtig, dass die Tastatur-Ereigniserkennung eng in den Gameloop eingebunden ist.

Die gute Nachricht ist, dass ich einen 16-zeiligen JS-Wrapper hinzugefügt habe, mit dem Ereignisabfragen möglich sind. Es heißt „key_status.js“ und du kannst den Status eines Schlüssels jederzeit abfragen, indem du keydown.left aufrufst.

Da wir jetzt prüfen können, ob Tasten gedrückt werden, können wir mit dieser einfachen Aktualisierungsmethode den Spieler bewegen.

function update() {
  if (keydown.left) {
    player.x -= 2;
  }

  if (keydown.right) {
    player.x += 2;
  }
}

Probier es doch einfach mal aus.

Der Player kann vom Bildschirm verschoben werden. Begrenzen wir die Position des Spielers, damit er innerhalb der Grenzen bleibt. Außerdem scheint der Player etwas langsam zu sein. Erhöhen wir also auch die Geschwindigkeit.

function update() {
  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

Das Hinzufügen weiterer Eingaben ist genauso einfach. Fügen wir also eine Art Projektil hinzu.

function update() {
  if (keydown.space) {
    player.shoot();
  }

  if (keydown.left) {
    player.x -= 5;
  }

  if (keydown.right) {
    player.x += 5;
  }

  player.x = player.x.clamp(0, CANVAS_WIDTH - player.width);
}

player.shoot = function() {
  console.log("Pew pew");
  // :) Well at least adding the key binding was easy...
};

Weitere Spielobjekte hinzufügen

Projektile

Fügen wir jetzt die Projektile hinzu. Zuerst benötigen wir eine Sammlung, in der wir sie alle speichern können:

var playerBullets = [];

Als Nächstes benötigen wir einen Konstruktor, um Aufzählungspunkte zu erstellen.

function Bullet(I) {
  I.active = true;

  I.xVelocity = 0;
  I.yVelocity = -I.speed;
  I.width = 3;
  I.height = 3;
  I.color = "#000";

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.active = I.active && I.inBounds();
  };

  return I;
}

Wenn der Spieler schießt, sollten wir eine Geschoss-Instanz erstellen und der Sammlung von Geschossen hinzufügen.

player.shoot = function() {
  var bulletPosition = this.midpoint();

  playerBullets.push(Bullet({
    speed: 5,
    x: bulletPosition.x,
    y: bulletPosition.y
  }));
};

player.midpoint = function() {
  return {
    x: this.x + this.width/2,
    y: this.y + this.height/2
  };
};

Jetzt müssen wir der Funktion „update_step“ die Aktualisierung der Aufzählungspunkte hinzufügen. Damit die Liste der Aufzählungspunkte nicht unendlich lang wird, wird sie so gefiltert, dass nur die aktiven Aufzählungspunkte enthalten sind. So können wir auch Kugeln entfernen, die mit einem Gegner kollidiert sind.

function update() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.update();
  });

  playerBullets = playerBullets.filter(function(bullet) {
    return bullet.active;
  });
}

Im letzten Schritt zeichnen Sie die Aufzählungspunkte:

function draw() {
  ...
  playerBullets.forEach(function(bullet) {
    bullet.draw();
  });
}

Feinde

Jetzt ist es an der Zeit, Feinde hinzuzufügen, ähnlich wie wir die Kugeln hinzugefügt haben.

  enemies = [];

function Enemy(I) {
  I = I || {};

  I.active = true;
  I.age = Math.floor(Math.random() * 128);

  I.color = "#A2B";

  I.x = CANVAS_WIDTH / 4 + Math.random() * CANVAS_WIDTH / 2;
  I.y = 0;
  I.xVelocity = 0
  I.yVelocity = 2;

  I.width = 32;
  I.height = 32;

  I.inBounds = function() {
    return I.x >= 0 && I.x <= CANVAS_WIDTH &&
      I.y >= 0 && I.y <= CANVAS_HEIGHT;
  };

  I.draw = function() {
    canvas.fillStyle = this.color;
    canvas.fillRect(this.x, this.y, this.width, this.height);
  };

  I.update = function() {
    I.x += I.xVelocity;
    I.y += I.yVelocity;

    I.xVelocity = 3 * Math.sin(I.age * Math.PI / 64);

    I.age++;

    I.active = I.active && I.inBounds();
  };

  return I;
};

function update() {
  ...

  enemies.forEach(function(enemy) {
    enemy.update();
  });

  enemies = enemies.filter(function(enemy) {
    return enemy.active;
  });

  if(Math.random() < 0.1) {
    enemies.push(Enemy());
  }
};

function draw() {
  ...

  enemies.forEach(function(enemy) {
    enemy.draw();
  });
}

Bilder laden und zeichnen

Es ist cool, all diese Boxen herumfliegen zu sehen, aber Bilder dazu wären noch cooler. Das Laden und Zeichnen von Bildern auf dem Canvas ist in der Regel eine nervenaufreibende Angelegenheit. Um diese Probleme zu vermeiden, können wir eine einfache Dienstprogrammklasse verwenden.

player.sprite = Sprite("player");

player.draw = function() {
  this.sprite.draw(canvas, this.x, this.y);
};

function Enemy(I) {
  ...

  I.sprite = Sprite("enemy");

  I.draw = function() {
    this.sprite.draw(canvas, this.x, this.y);
  };

  ...
}

Kollisionserkennung

Es gibt viele Angebote auf dem Bildschirm, die aber nicht miteinander interagieren. Damit alles weiß, wann es explodieren soll, müssen wir eine Art Kollisionserkennung hinzufügen.

Verwenden wir einen einfachen Algorithmus zur rechteckigen Kollisionserkennung:

function collides(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}

Es gibt einige Kollisionen, die wir prüfen möchten:

  1. Spielerprojektile => gegnerische Schiffe
  2. Spieler => Feindliche Schiffe

Erstellen wir eine Methode zum Umgang mit Kollisionen, die wir über die Update-Methode aufrufen können.

function handleCollisions() {
  playerBullets.forEach(function(bullet) {
    enemies.forEach(function(enemy) {
      if (collides(bullet, enemy)) {
        enemy.explode();
        bullet.active = false;
      }
    });
  });

  enemies.forEach(function(enemy) {
    if (collides(enemy, player)) {
      enemy.explode();
      player.explode();
    }
  });
}

function update() {
  ...
  handleCollisions();
}

Jetzt müssen wir dem Spieler und den Gegnern die Explode-Methoden hinzufügen. Dadurch werden sie zum Entfernen markiert und eine Explosion wird hinzugefügt.

function Enemy(I) {
  ...

  I.explode = function() {
    this.active = false;
    // Extra Credit: Add an explosion graphic
  };

  return I;
};

player.explode = function() {
  this.active = false;
  // Extra Credit: Add an explosion graphic and then end the game
};

Ton

Zum Abschluss fügen wir noch ein paar coole Soundeffekte hinzu. Die Verwendung von Audioinhalten in HTML5 kann, ähnlich wie bei Bildern, etwas mühsam sein. Dank unserer magischen Formel „sound.js“ ist das aber ganz einfach.

player.shoot = function() {
  Sound.play("shoot");
  ...
}

function Enemy(I) {
  ...

  I.explode = function() {
    Sound.play("explode");
    ...
  }
}

Die API ist jetzt zwar fehlerfrei, aber das Hinzufügen von Tönen ist derzeit der schnellste Weg, um Ihre Anwendung zum Absturz zu bringen. Es kommt nicht selten vor, dass Töne aussetzen oder der gesamte Browsertab abstürzt. Also halte Taschentücher bereit.

Farewell

Hier ist noch einmal die vollständige funktionierende Spieldemo. Sie können den Quellcode auch als ZIP-Datei herunterladen.

Ich hoffe, Ihnen hat das Erlernen der Grundlagen zum Erstellen eines einfachen Spiels in JavaScript und HTML5 gefallen. Wenn wir auf der richtigen Abstraktionsebene programmieren, können wir uns von den schwierigeren Teilen der APIs abgrenzen und auch bei zukünftigen Änderungen flexibel reagieren.

Verweise