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 des Tutorials zum Erstellen eines Breakout-Spiels mit reinem JavaScript. Statt das Spiel sofort zu starten, können wir diese 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, sowie einen AbortController, um die Event-Listener des Buttons beim Spielstart zu entfernen. 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 für den Button auf dieselbe Weise laden wie die Wackelanimation des Balls. Fügen Sie nach Ihren anderen Klassen für Spielobjekte eine Button-Klasse 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 dem Canvas zentriert. Seine frame-Eigenschaft wählt das Bild für den Normalzustand (0), den Hover-Zustand (1) oder den gedrückten Zustand (2) aus.

Die draw()-Methode berechnet die Spalte und Zeile des Frames im Spritesheet und zeichnet nur diesen Frame, wie wir es beim Ball getan haben.

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 unterhalb der Definitionen für Ball, Paddle und Bricks:

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

Sie müssen außerdem das Spritesheet für den Button 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 einem Canvas statt mit nativen HTML-Elementen arbeiten. Wir müssen Hitbox-Prüfung, Animation und Event-Verarbeitung selbst implementieren.

Die containsPointer()-Methode rechnet die Koordinaten des Pointers in Canvas-Koordinaten um und führt dann eine Hitbox-Prüfung durch:

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 initButtonControls()-Funktion hinzu, die Pointer-Event-Listener zur Steuerung des Buttons registriert. Alles Weitere in diesem Abschnitt wird, sofern nicht anders angegeben, an den Funktionskörper angehängt.

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

Definieren Sie zunächst options mit dem signal, über das diese Listener beim Spielstart entfernt werden. Erfassen Sie außerdem die Pointer-ID, damit nur der Pointer, der den Button gedrückt hat, ihn aktivieren kann.

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

Wenn sich der Pointer aus dem Button herausbewegt, wird der Frame-Index zurückgesetzt, sodass der Button wieder sein Standardaussehen erhält. Wird ein Druck außerhalb des Buttons beendet, wird er 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 pointermove-Handler. Er setzt den Frame-Index – und ändert damit das Aussehen des Buttons –, wenn sich ein Pointer über dem Button befindet. Wenn bereits ein Pointer gedrückt wird, werden andere Pointer ignoriert und nicht als zusätzliche Hover-Eingaben erfasst.

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 anschließend den pointerdown-Handler. Er setzt den Frame-Index auf das Aussehen für den gedrückten Zustand und speichert außerdem den pressedPointer. Das geschieht nur, wenn derzeit kein anderer Pointer gedrückt wird und bei einer Maus oder einem ähnlichen Gerät mit der linken Taste geklickt wird. Durch Pointer Capture empfängt der Canvas weiterhin Pointer-Events, auch wenn sich der Pointer außerhalb seiner Grenzen bewegt.

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 pointerup-Handler. Er startet das Spiel, aber nur, wenn der Pointer losgelassen wird, während er sich noch über dem Button befindet. Andernfalls gilt der Druck 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 Pointer den Canvas verlässt, eine native Pointer-Cancel-Geste erfolgt oder das vom pointerdown-Listener gesetzte Pointer Capture verloren geht, muss der Zustand ebenfalls zurückgesetzt beziehungsweise der Druck abgebrochen werden. Diese Events werden nicht anhand der Hitbox gefiltert, da sie auch einen Druck bereinigen müssen, bei dem der Pointer 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 Preload-Aufruf durch Folgendes, das auch das Button-Bild 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 verwendete 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, setzen die Anfangsgeschwindigkeit des Balls und setzen die Eigenschaft playing auf true. Außerdem setzen wir lastTimestamp zurück, damit beim ersten Bewegungsupdate keine Zeit vor dem Loslassen des Buttons berücksichtigt wird.

Kehren Sie zum Abschluss dieses Abschnitts zu Ihrer Ball-Klasse zurück, 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!

Das Paddle vor Spielbeginn unbeweglich halten

Alles funktioniert wie erwartet, aber wir können das Paddle noch bewegen, bevor das Spiel begonnen hat. Das wirkt etwas seltsam. Um dies zu verhindern, können wir die Eigenschaft playing verwenden und das Paddle nur dann beweglich machen, wenn das Spiel gestartet wurde. Fügen Sie dazu !playing wie folgt zur bestehenden Schutzbedingung im 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

So sollte Ihr bisheriger Stand aussehen – hier als lauffähiges Beispiel. Um den Quellcode anzuzeigen, klicken Sie auf den „Play“-Button.

Nächste Schritte

Zum Abschluss dieser Artikelreihe machen wir das Spiel noch interessanter, indem wir dem Abprallen des Balls vom Paddle etwas Zufall hinzufügen.