Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.

View in English Always switch to English

Buttons

Dies ist der 10. von 11 Schritten im Tutorial zum Erstellen eines Breakout-Spiels mit reinem JavaScript. Anstatt das Spiel sofort zu starten, können wir die Entscheidung den Spielenden überlassen, indem wir einen Start-Button hinzufügen. Sehen wir uns an, wie das geht.

Neue Variablen

Wir benötigen eine boolesche Variable, die angibt, ob das Spiel gestartet wurde, und einen AbortController, um die Event-Listener des Buttons zu entfernen, sobald das Spiel startet. Fügen Sie diese Zeilen unter Ihren anderen Variablen auf oberster Ebene ein:

js
let playing = false;
const buttonControls = new AbortController();

Den Button zum Spiel hinzufügen

Wir können das Spritesheet des Buttons genauso laden wie die Wackelanimation des Balls. Fügen Sie nach Ihren anderen Klassen für Spielobjekte eine Klasse Button hinzu:

js
class Button extends GameObject {
  size = { w: 120, h: 40 };
  frame = 0;
  constructor(url, ctx) {
    super(url, ctx);
    this.pos = { x: ctx.canvas.width / 2, y: ctx.canvas.height / 2 };
  }
}

Der Button ist auf der Canvas zentriert. Seine Eigenschaft frame wählt das Bild für den Normalzustand (0), den Hover-Zustand (1) oder den gedrückten Zustand (2) aus.

Die Methode draw() berechnet die Spalte und Zeile des Frames im Spritesheet und zeichnet nur diesen Frame, genau wie beim Ball.

js
class Button extends GameObject {
  // …
  draw() {
    const columns = Math.floor(this.asset.width / this.size.w);
    const { left, top } = this.hitbox;
    this.ctx.drawImage(
      this.asset,
      (this.frame % columns) * this.size.w,
      Math.floor(this.frame / columns) * this.size.h,
      this.size.w,
      this.size.h,
      left,
      top,
      this.size.w,
      this.size.h,
    );
  }
}

Erstellen Sie den Button unter den Definitionen für Ball, Paddle und Bricks:

js
const startButton = new Button("img/button.png", ctx);

Sie müssen außerdem das Spritesheet des Buttons herunterladen und in Ihrem Verzeichnis /img speichern.

Fügen Sie in update() nach drawStatus() Folgendes hinzu, damit der Button angezeigt wird, bis das Spiel startet:

js
if (!playing) {
  startButton.draw();
}

Eingaben für den Button verarbeiten

Die Verarbeitung von Eingaben für den Button ist etwas aufwendig, weil wir mit einer Canvas statt mit nativen HTML-Elementen arbeiten. Wir müssen die Prüfung der Trefferfläche, die Animation und die Event-Verarbeitung selbst implementieren.

Die Methode containsPointer() wandelt die Koordinaten des Zeigers in Canvas-Koordinaten um und prüft anschließend, ob sich der Zeiger innerhalb der Trefferfläche befindet:

js
class Button extends GameObject {
  // …
  containsPointer(event) {
    const bounds = this.ctx.canvas.getBoundingClientRect();
    const x =
      ((event.clientX - bounds.left) * this.ctx.canvas.width) / bounds.width;
    const y =
      ((event.clientY - bounds.top) * this.ctx.canvas.height) / bounds.height;
    const { left, right, top, bottom } = this.hitbox;
    return x >= left && x <= right && y >= top && y <= bottom;
  }
}

Fügen Sie am Ende des Skripts eine Funktion initButtonControls() hinzu, die Pointer-Event-Listener zur Steuerung des Buttons registriert. Sofern nicht anders angegeben, fügen Sie alles Folgende in diesem Abschnitt zum Funktionskörper hinzu.

js
function initButtonControls() {
  // Add code here
}

Definieren Sie zunächst options mit dem signal, über das diese Listener entfernt werden, sobald das Spiel startet. Erfassen Sie außerdem die Pointer-ID, damit nur der Zeiger, mit dem der Button gedrückt wurde, ihn aktivieren kann.

js
const options = { signal: buttonControls.signal };
let pressedPointer = null;

Wenn der Zeiger den Button verlässt, wird der Frame-Index zurückgesetzt, wodurch der Button wieder sein Standardaussehen erhält. Wird der Zeiger außerhalb des Buttons losgelassen, wird der Tastendruck abgebrochen. Diese Gesten können auf verschiedene Arten erfolgen. Fügen Sie deshalb wiederverwendbare Funktionen dafür hinzu.

js
function resetFrame(event) {
  if (pressedPointer === null || pressedPointer === event.pointerId) {
    startButton.frame = 0;
  }
}
function cancelPress(event) {
  if (event.pointerId === pressedPointer) {
    pressedPointer = null;
    resetFrame(event);
  }
}

Definieren Sie als Nächstes den Handler für pointermove. Er setzt den Frame-Index – und ändert damit das Aussehen des Buttons –, wenn sich der Zeiger über dem Button befindet. Wenn bereits ein Zeiger gedrückt gehalten wird, werden andere Zeiger ignoriert und nicht als zusätzliche Hover-Ereignisse berücksichtigt.

js
canvas.addEventListener(
  "pointermove",
  (event) => {
    if (pressedPointer !== null && pressedPointer !== event.pointerId) {
      return;
    }
    if (startButton.containsPointer(event)) {
      startButton.frame = pressedPointer === null ? 1 : 2;
    } else {
      resetFrame(event);
    }
  },
  options,
);

Definieren Sie als Nächstes den Handler für pointerdown. Er setzt den Frame-Index für das Aussehen im gedrückten Zustand und speichert außerdem pressedPointer. Dies geschieht nur, wenn derzeit kein anderer Zeiger gedrückt gehalten wird und es sich bei einer Maus oder einem ähnlichen Gerät um einen Linksklick handelt. Durch Pointer-Capture empfängt die Canvas weiterhin Pointer-Events, selbst wenn der Zeiger ihren Bereich verlässt.

js
canvas.addEventListener(
  "pointerdown",
  (event) => {
    if (
      event.button !== 0 ||
      pressedPointer !== null ||
      !startButton.containsPointer(event)
    ) {
      return;
    }
    pressedPointer = event.pointerId;
    startButton.frame = 2;
    canvas.setPointerCapture(event.pointerId);
  },
  options,
);

Definieren Sie als Nächstes den Handler für pointerup. Er startet das Spiel nur dann, wenn der Zeiger losgelassen wird, während er sich noch über dem Button befindet. Andernfalls gilt der Tastendruck als abgebrochen.

js
canvas.addEventListener(
  "pointerup",
  (event) => {
    if (event.pointerId !== pressedPointer) {
      return;
    }
    if (startButton.containsPointer(event)) {
      pressedPointer = null;
      startGame();
    } else {
      cancelPress(event);
    }
  },
  options,
);

Wenn der Zeiger die Canvas verlässt, eine native Pointer-Cancel-Geste erfolgt oder das durch den pointerdown-Listener eingerichtete Pointer-Capture verloren geht, muss jeweils ein Zurücksetzen beziehungsweise Abbrechen ausgelöst werden. Diese Events werden nicht anhand der Trefferfläche gefiltert, da sie auch Tastendrücke bereinigen müssen, bei denen der Zeiger den Button verlässt.

js
canvas.addEventListener("pointerleave", resetFrame, options);
canvas.addEventListener("pointercancel", cancelPress, options);
canvas.addEventListener("lostpointercapture", cancelPress, options);

Wir registrieren diese Listener erst, nachdem die Assets geladen wurden. Ersetzen Sie den bisherigen Aufruf zum Vorladen durch Folgendes, das auch das Bild des Buttons lädt:

js
Promise.all(
  [ball, paddle, ...bricks, startButton].map((obj) => obj.preload()),
).then(() => {
  ball.pos.x = paddle.pos.x;
  ball.pos.y = paddle.hitbox.top - ball.size.h / 2;
  initButtonControls();
  requestAnimationFrame(update);
});

Das Spiel starten

Nun müssen wir die oben im Code referenzierte Funktion startGame() definieren:

js
function startGame() {
  buttonControls.abort();
  ball.vel = { x: 150, y: -150 };
  playing = true;
  lastTimestamp = null;
}

Wenn der Button gedrückt wird, entfernen wir alle Event-Listener des Buttons, legen die Anfangsgeschwindigkeit des Balls fest und setzen die Eigenschaft playing auf true. Außerdem setzen wir lastTimestamp zurück, damit bei der ersten Aktualisierung der Ballbewegung nicht die Zeit vor dem Loslassen des Buttons berücksichtigt wird.

Gehen Sie zum Abschluss dieses Abschnitts zurück zu Ihrer Klasse Ball, suchen Sie die Zeile vel = { x: 150, y: -150 } und ersetzen Sie sie durch vel = { x: 0, y: 0 }. Der Ball soll sich erst bewegen, wenn der Button gedrückt wurde – nicht vorher!

Das Paddle vor Spielbeginn unbeweglich halten

Es funktioniert wie erwartet, aber wir können das Paddle auch dann noch bewegen, wenn das Spiel noch nicht gestartet ist. Das wirkt etwas merkwürdig. Um das zu verhindern, können wir die Eigenschaft playing nutzen und das Paddle nur dann beweglich machen, wenn das Spiel gestartet wurde. Fügen Sie dazu !playing wie folgt zur Bedingung im vorhandenen pointermove-Listener des Paddles hinzu:

js
if (!playing || paddle.size.w === undefined) {
  return;
}

So bleibt das Paddle unbeweglich, nachdem alles geladen und vorbereitet wurde, aber bevor das eigentliche Spiel beginnt.

Vergleichen Sie Ihren Code

Hier sehen Sie, wie Ihr bisheriger Stand aussehen sollte – als lauffähiges Beispiel. Klicken Sie auf den Button „Play“, um den Quellcode anzuzeigen.

Nächste Schritte

Als Letztes werden wir in dieser Artikelreihe das Gameplay noch interessanter gestalten, indem wir der Art, wie der Ball vom Paddle abprallt, etwas Zufall hinzufügen.