Buttons
Dies ist der 10. Schritt von 11 im Tutorial zum Erstellen eines Breakout-Spiels mit reinem JavaScript. Anstatt das Spiel sofort zu starten, können wir diese Entscheidung den Spielenden überlassen, indem wir einen Start-Button hinzufügen, den sie drücken können. 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 beginnt. Fügen Sie diese Zeilen unter Ihren anderen Variablen auf oberster Ebene ein:
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:
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 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, wie wir es beim Ball getan haben.
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, Schläger und Steine:
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 beginnt:
if (!playing) {
startButton.draw();
}
Eingaben für den Button verarbeiten
Die Verarbeitung von Eingaben für den Button ist etwas aufwendig, weil wir es mit einem Canvas statt mit nativen HTML-Elementen zu tun haben. Wir müssen die Trefferprüfung, die Animation und die Ereignisbehandlung selbst implementieren.
Die Methode containsPointer() rechnet die Koordinaten des Zeigers in Canvas-Koordinaten um und prüft dann, ob sie innerhalb der Trefferfläche liegen:
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, wird alles Folgende in diesem Abschnitt zum Funktionskörper hinzugefügt.
function initButtonControls() {
// Add code here
}
Definieren Sie zunächst options mit dem signal, das zum Entfernen dieser Listener verwendet wird, sobald das Spiel beginnt. Erfassen Sie außerdem die Pointer-ID, damit nur der Zeiger, mit dem der Button gedrückt wurde, ihn aktivieren kann.
const options = { signal: buttonControls.signal };
let pressedPointer = null;
Wenn sich der Zeiger aus dem Button herausbewegt, wird der Frame-Index zurückgesetzt, wodurch der Button wieder sein Standardaussehen erhält. Wird der Zeiger außerhalb des Buttons losgelassen, wird das Drücken abgebrochen. Diese Gesten können auf verschiedene Arten erfolgen. Fügen Sie deshalb wiederverwendbare Funktionen dafür hinzu.
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 ein Zeiger über dem Button befindet. Wenn ein Zeiger bereits gedrückt ist, werden andere Zeiger ignoriert und lösen keinen zusätzlichen Hover-Zustand aus.
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 ebenfalls den Frame-Index auf das Aussehen für den gedrückten Zustand und speichert außerdem pressedPointer. Das geschieht nur, wenn derzeit kein anderer Zeiger gedrückt ist und bei einer Maus oder einem ähnlichen Gerät mit der linken Taste geklickt wurde. Durch Pointer Capture empfängt der Canvas weiterhin Pointer-Events, auch wenn sich der Zeiger außerhalb seiner Grenzen bewegt.
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, allerdings nur, wenn der Zeiger losgelassen wird, während er sich noch über dem Button befindet. Andernfalls gilt das Drücken als abgebrochen.
canvas.addEventListener(
"pointerup",
(event) => {
if (event.pointerId !== pressedPointer) {
return;
}
if (startButton.containsPointer(event)) {
pressedPointer = null;
startGame();
} else {
cancelPress(event);
}
},
options,
);
Wenn der Zeiger den Canvas verlässt, eine native Abbruchgeste für den Zeiger erfolgt oder die vom pointerdown-Listener eingerichtete Pointer Capture endet, muss ebenfalls ein Zurücksetzen beziehungsweise Abbrechen ausgelöst werden. Diese Ereignisse werden nicht anhand der Trefferfläche gefiltert, da auch ein Drücken abgebrochen werden muss, bei dem der Zeiger den Button verlässt.
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 Button-Bild lädt:
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
Jetzt müssen wir die oben im Code verwendete Funktion startGame() definieren:
function startGame() {
buttonControls.abort();
ball.vel = { x: 150, y: -150 };
playing = true;
lastTimestamp = null;
}
Wenn der Button gedrückt wird, entfernen wir alle Listener des Buttons, setzen die Anfangsgeschwindigkeit des Balls und setzen die Eigenschaft playing auf true. Außerdem setzen wir lastTimestamp zurück, damit die erste Aktualisierung der Ballbewegung nicht die Zeit vor dem Loslassen des Buttons berücksichtigt.
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!
Den Schläger vor Spielbeginn unbeweglich halten
Das funktioniert wie erwartet, aber wir können den Schläger immer noch bewegen, bevor das Spiel begonnen hat. Das sieht etwas seltsam aus. Um das zu verhindern, können wir die Eigenschaft playing verwenden und den Schläger nur dann beweglich machen, wenn das Spiel begonnen hat. Fügen Sie dazu !playing zur Abbruchbedingung im vorhandenen pointermove-Listener des Schlägers hinzu:
if (!playing || paddle.size.w === undefined) {
return;
}
So bleibt der Schläger 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 Live-Beispiel. Um den Quellcode anzuzeigen, klicken Sie auf den „Play“-Button.
Nächste Schritte
Als Letztes werden wir in dieser Artikelreihe das Spielgeschehen noch interessanter gestalten, indem wir etwas Zufälligkeit hinzufügen, wenn der Ball vom Schläger abprallt.