Guten Tag, liebe Kollegen!
Mein Name ist Alexander, ich bin Entwickler von HTML5-Spielen.
In einem der Unternehmen, an das ich meinen Lebenslauf gesendet habe, wurde ich gebeten, eine Testaufgabe abzuschließen. Ich habe zugestimmt und nach 1 Tag als Ergebnis das Spiel gesendet, das gemäß TOR HTML5 entwickelt wurde.

Da ich eine Ausbildung in Spielprogrammierung absolviere und meinen Code effizienter nutzen möchte, habe ich mich dazu entschlossen, einen Schulungsartikel über das abgeschlossene Projekt zu verfassen. Und da der abgeschlossene Test eine positive Bewertung erhielt und zu einer Einladung zu einem Vorstellungsgespräch führte, hat meine Entscheidung wahrscheinlich das Recht zu existieren und wird möglicherweise in Zukunft jemandem helfen.
Dieser Artikel gibt einen Überblick über den Arbeitsaufwand, der ausreicht, um die durchschnittliche Testaufgabe für die HTML5-Position des Entwicklers erfolgreich abzuschließen. Das Material kann auch für jeden von Interesse sein, der sich mit dem Phaser-Framework vertraut machen möchte. Wenn Sie bereits mit Phaser arbeiten und in JS schreiben, erfahren Sie, wie Sie ein Projekt in TypeScript entwickeln.
Unter cat gibt es also viel TypeScript-Code!
Einleitung
Wir geben eine kurze Erklärung des Problems.
- Wir werden ein einfaches HTML5-Spiel entwickeln - einen klassischen Pionier.
- Als Haupttools werden wir Phaser 3, Typoscript und Webpack verwenden.
- Das Spiel wird für den Desktop entworfen und im Browser ausgeführt.
Wir bieten Links zum endgültigen Projekt.
Links zur Demo und Quelle Und erinnere dich an die Mechanik des Pioniers, wenn plötzlich jemand die Spielregeln vergessen hat. Da dies jedoch unwahrscheinlich ist, werden die Regeln unter den Spoiler gestellt :)
Sapper-RegelnDas Spielfeld besteht aus Zellen, die in einer Tabelle angeordnet sind. Standardmäßig sind beim Start des Spiels alle Zellen geschlossen. In einigen Zellen sind Bomben platziert.
Wenn Sie mit der linken Maustaste auf eine geschlossene Zelle klicken, wird diese geöffnet. Wenn sich eine Bombe in einer offenen Zelle befand, endet das Spiel mit einer Niederlage.
Wenn sich keine Bombe in der Zelle befand, wird eine Zahl darin angezeigt, die die Anzahl der Bomben angibt, die sich in benachbarten Zellen befinden, bezogen auf die derzeit geöffnete Bombe. Wenn keine Bomben in der Nähe sind, sieht die Zelle leer aus.
Ein Rechtsklick auf eine geschlossene Zelle setzt ein Flag darauf. Der Spieler hat die Aufgabe, alle ihm zur Verfügung stehenden Flaggen so anzuordnen, dass sie alle verminteten Zellen markieren. Nachdem alle Flaggen platziert wurden, drückt der Spieler die linke Maustaste auf eine der offenen Zellen, um zu überprüfen, ob er gewonnen hat.
Als nächstes gehen wir direkt zum Handbuch. Das gesamte Material ist in kleine Schritte unterteilt, von denen jeder die Implementierung einer bestimmten Aufgabe in kurzer Zeit beschreibt. Wenn wir also Schritt für Schritt kleine Ziele erreichen, werden wir am Ende ein vollständiges Spiel erstellen. Verwenden Sie das Inhaltsverzeichnis, wenn Sie schnell zu einem bestimmten Schritt übergehen möchten.
1. Vorbereitung
1.1 Projektvorlage
Laden Sie die
Standard-Phaser-Projektvorlage herunter. Dies ist die empfohlene Vorlage des Autors des Frameworks und bietet uns die folgende Verzeichnisstruktur:
Für unser Projekt benötigen wir die aktuelle Datei
index.js
nicht. Löschen Sie sie daher. Erstellen Sie dann das Verzeichnis
/src/scripts/
und platzieren Sie die leere Datei
index.ts
darin. Wir werden alle unsere Skripte zu diesem Ordner hinzufügen.
Beachten Sie auch, dass beim Erstellen eines Projekts für die Produktion im Stammverzeichnis ein
dist
Verzeichnis erstellt wird, in dem der Release-Build abgelegt wird.
1.2 Build-Konfiguration
Wir werden das Webpack für die Montage verwenden. Da unsere Vorlage ursprünglich für die Arbeit mit JavaScript vorbereitet war und wir in TypeScript schreiben, müssen wir kleine Änderungen an der Konfiguration des Kollektors vornehmen.
webpack/base.js
den
webpack/base.js
hinzu, der den Einstiegspunkt beim
webpack/base.js
unseres Projekts angibt, sowie die Konfiguration von
ts-loader
, die die Regeln für das
webpack/base.js
TS-Skripten beschreibt:
Wir müssen auch die Datei tsconfig.json im Projektstamm erstellen. Für mich hat es folgenden Inhalt:
{ "compilerOptions": { "module": "commonjs", "lib": [ "dom", "es5", "es6", "es2015", "es2017", "es2015.promise" ], "target": "es5", "skipLibCheck": true }, "exclude": ["node_modules", "dist"] }
1.3 Module installieren
Installieren Sie alle Abhängigkeiten von package.json und fügen Sie die typescript- und ts-loader-Module hinzu:
npm i npm i typescript --save-dev npm i ts-loader --save-dev
Jetzt kann das Projekt mit der Entwicklung beginnen. Wir verfügen über 2 Befehle, die bereits in der Eigenschaft
scripts
in der Datei
package.json
definiert sind.
- Erstellen Sie ein Projekt zum Debuggen und öffnen Sie es in einem Browser über einen lokalen Server
npm start
- Führen Sie den Build for Sale aus und legen Sie den Release-Build im Ordner dist / ab
npm run build
1.4 Vermögensvorbereitung
Alle Assets für dieses Spiel werden ehrlich von
OpenGameArt (Version 61x61) heruntergeladen und haben die freundlichste der Lizenzen namens
Feel free to use , die uns auf der Seite mit dem Paket sorgfältig mitgeteilt wird. Der Code im Artikel hat übrigens die gleiche Lizenz! ;)
Ich habe das Uhrbild aus dem heruntergeladenen Satz gelöscht und den Rest der Dateien umbenannt, um einfach zu verwendende Frame-Namen zu erhalten. Die Liste der Namen und der entsprechenden Dateien wird auf dem Bildschirm unten angezeigt.
Aus den resultierenden Sprites erstellen wir einen
Phaser JSONArray
im
TexturePacker- Programm (es gibt mehr als genug kostenlose Versionen, ich habe noch keine Arbeit erhalten) und legen die generierten Dateien
spritesheet.png
und
spritesheet.json
im Projektverzeichnis
src/assets/
.

2. Szenen erstellen
2.1 Einstiegspunkt
Wir beginnen die Entwicklung mit der Erstellung des Einstiegspunkts, der in der Webpack-Konfiguration beschrieben ist.
Da das Spiel für den Desktop entwickelt wurde und den gesamten Bildschirm ausfüllt, verwenden wir mutig die gesamte Breite und Höhe des Browsers für die Felder
width
und
height
.
Das
scene
ist momentan ein leeres Array und wir werden es reparieren!
2.2 Startszene
Erstellen Sie die Klasse der ersten Szene in der
src/scripts/scenes/StartScene.ts
:
export class StartScene extends Phaser.Scene { constructor() { super('Start'); } public preload(): void { } public create(): void { } }
Für eine gültige Vererbung
Phaser.Scene
den
Phaser.Scene
als Parameter an den Konstruktor der übergeordneten Klasse.
Diese Szene kombiniert die Funktionalität des Vorladens von Ressourcen und des Startbildschirms und lädt den Benutzer zum Spiel ein.
Normalerweise durchläuft ein Spieler in meinen Projekten zwei Szenen, bevor er zur Startszene gelangt, und zwar in dieser Reihenfolge:
Boot => Preload => Start
Aber in diesem Fall ist das Spiel so einfach und es gibt so wenige Assets, dass es keinen Grund gibt, das Preload in eine separate Szene zu stellen, und noch mehr den anfänglichen separaten Bootloader.
Wir werden alle Assets in der
preload
Methode laden. Um in Zukunft mit dem erstellten Atlas arbeiten zu können, müssen wir zwei Schritte ausführen:
png
sich sowohl png
als auch json
Atlas-Dateien mit require
:
- Laden Sie sie in die
preload
Methode der Startszene:
2.3 Texte der Startszene
In der Startszene sind noch 2 Dinge zu tun:
- Sagen Sie dem Spieler, wie er das Spiel starten soll
- starte das Spiel auf Initiative des Spielers
Um den ersten Punkt zu erfüllen, erstellen wir zunächst zwei Aufzählungen am Anfang der Szenendatei, um die Texte und ihre Stile zu beschreiben:
Und erstellen Sie dann beide Texte als Objekte in der Methode
create
. Lassen Sie mich daran erinnern, dass die
create
von Szenen in
Phaser
erst aufgerufen wird, nachdem alle Ressourcen in der
preload
Methode
preload
, und dies ist für uns durchaus geeignet.
In einem anderen größeren Projekt könnten wir die Texte und Stile entweder in JSON-Locale-Dateien oder in separate Konfigurationsdateien aufnehmen. Da wir jetzt nur noch zwei Zeilen haben, halte ich diesen Schritt für überflüssig und schlage in diesem Fall vor, unser Leben nicht zu verkomplizieren. Beschränken wir uns auf Listen am Anfang der Szenendatei.
2.4 Übergang in die Spielebene
Das Letzte, was wir in dieser Szene tun werden, bevor wir weitermachen, ist das Verfolgen des Mausklick-Ereignisses, um den Spieler in das Spiel zu starten:
2,5 Level Szene
this.scene.start
Parameters
"Game"
, der an die Methode
this.scene.start
Sie bereits vermutet, dass es an der Zeit ist, eine zweite Szene zu erstellen, die die Hauptspiellogik verarbeitet. Erstelle die
src/scripts/scenes/GameScene.ts
:
export class GameScene extends Phaser.Scene { constructor() { super('Game'); } public create(): void { } }
In dieser Szene benötigen wir die Methode
preload
, weil In der vorherigen Szene haben wir bereits alle erforderlichen Ressourcen geladen.
2.6 Szenen am Einstiegspunkt einstellen
Nachdem beide Szenen erstellt wurden, fügen Sie sie unserem Einstiegspunkt hinzu
src/scripts/index.ts
:
3. Spielobjekte
Die
GameScene
Klasse implementiert also die Logik auf Spielebene. Und was erwarten wir vom Sapper-Level? Visuell erwarten wir ein Spielfeld mit geschlossenen Zellen. Wir wissen, dass das Feld eine Tabelle ist, was bedeutet, dass es eine bestimmte Anzahl von Zeilen und Spalten hat, in denen mehrere Bomben bequem platziert sind. Somit verfügen wir über genügend Informationen, um eine separate Entität zu erstellen, die das Spielfeld beschreibt.
3.1 Spielbrett
Erstellen Sie die
src/scripts/models/Board.ts
in die wir die
Board
Klasse
src/scripts/models/Board.ts
:
import { Field } from "./Field"; export class Board extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _rows: number = 0; private _cols: number = 0; private _bombs: number = 0; private _fields: Field[] = []; constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { super(); this._scene = scene; this._rows = rows; this._cols = cols; this._bombs = bombs; this._fields = []; } public get cols(): number { return this._cols; } public get rows(): number { return this._rows; } }
Machen wir die Klasse zum Nachfolger von Phaser.Events.EventEmitter, um auf die Schnittstelle zum Registrieren und Aufrufen von Ereignissen zuzugreifen, die wir in Zukunft benötigen werden.
Ein Array von Objekten der
Field
Klasse wird in der Privateigenschaft
_fields
gespeichert. Wir werden dieses Modell später implementieren.
Wir richten private numerische Eigenschaften
_rows
und
_cols
, um die Anzahl der Zeilen und Spalten des Spielfelds anzugeben. Erstellen Sie öffentliche Getter zum Lesen von
_rows
und
_cols
.
Das Feld
_bombs
die Anzahl der Bomben an, die für das Level generiert werden müssen. Und im Parameter
_scene
wir einen Verweis auf das Objekt der
GameScene
-Spielszene, in dem wir eine Instanz der
Board
Klasse erstellen.
Es ist erwähnenswert, dass wir das Szenenobjekt nur zur weiteren Übertragung in die Ansichten auf das Modell übertragen, wo wir es nur zum Anzeigen der Ansicht verwenden. Tatsache ist, dass Phaser das Szenenobjekt direkt zum Rendern von Sprites verwendet und uns daher verpflichtet, einen Link zur aktuellen Szene bereitzustellen, wenn Prefabs für Sprites erstellt werden, die wir in Zukunft entwickeln werden. Wir erklären uns damit einverstanden, dass wir den Link zur Szene nur zur weiteren Verwendung als Display-Engine übertragen und die benutzerdefinierten Methoden der Szene in Modellen und Ansichten nicht direkt aufrufen.
Nachdem wir uns für die
GameScene
entschieden haben, schlage ich vor, diese in der
GameScene
zu initialisieren und die
GameScene
Klasse
GameScene
:
Wir nehmen die Board-Parameter zu Konstanten am Anfang der Szenendatei und übergeben sie an den
Board
Konstruktor, wenn wir eine Instanz dieser Klasse erstellen.
3.2 Zellenmodell
Die Tafel besteht aus Zellen, die Sie auf dem Bildschirm anzeigen möchten. Jede Zelle muss an der entsprechenden Position platziert werden, die durch die Zeile und Spalte bestimmt wird.
Zellen werden auch als separate Einheit ausgewählt. Erstellen Sie die
src/scripts/models/Field.ts
in der die Klasse platziert wird, die die Zelle beschreibt:
import { Board } from "./Board"; export class Field extends Phaser.Events.EventEmitter { private _scene: Phaser.Scene = null; private _board: Board = null; private _row: number = 0; private _col: number = 0; constructor(scene: Phaser.Scene, board: Board, row: number, col: number) { super(); this._init(scene, board, row, col); } public get col(): number { return this._col; } public get row(): number { return this._row; } public get board(): Board { return this._board; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { this._scene = scene; this._board = board; this._row = row; this._col = col; } }
Jede Zelle sollte über Zeilen- und Spaltenmetriken verfügen, in denen sie sich befindet. Wir richten die Parameter
_board
und
_scene
um Verknüpfungen zu Objekten der Platine und der Szene
_scene
. Wir implementieren Getter zum Lesen der
_row
,
_col
und
_board
.
3.3 Zellenansicht
Die abstrakte Zelle wird erstellt und nun möchten wir sie visualisieren. Um eine Zelle auf dem Bildschirm anzuzeigen, müssen Sie ihre Ansicht erstellen. Erstellen Sie die
src/scripts/views/FieldView.ts
und
src/scripts/views/FieldView.ts
die Ansichtsklasse ein:
import { Field } from "../models/Field"; export class FieldView extends Phaser.GameObjects.Sprite { private _model: Field = null; constructor(scene: Phaser.Scene, model: Field) { super(scene, 0, 0, 'spritesheet', 'closed'); this._model = model; this._init(); this._create(); } private _init(): void { } private _create(): void { } }
Bitte beachten Sie, dass wir diese Klasse zum Nachkommen von
Phaser.GameObjects.Sprite
. In Phaser-Begriffen ist diese Klasse zu einem Sprite-Fertighaus geworden. Das heißt, ich habe die Funktionalität des Spielobjekts des Sprites, das wir mit unseren eigenen Methoden weiter ausbauen werden.
Schauen wir uns den Konstruktor dieser Klasse an. Hier müssen wir zunächst den Konstruktor der übergeordneten Klasse mit den folgenden Parametersätzen aufrufen:
- Verknüpfen mit dem Szenenobjekt (wie ich in Abschnitt 3.1 gewarnt habe: Phaser erfordert, dass wir eine Verknüpfung mit der aktuellen Szene herstellen, um Sprites zu rendern.)
x
und y
Koordinaten auf Leinwand- der String-Schlüssel, für den der Atlas verfügbar ist, den wir in der
preload
Methode der preload
geladen haben - Geben Sie den Schlüssel für die Frame-Zeichenfolge in diesem Atlas ein, den Sie auswählen möchten, um das Sprite anzuzeigen
_model
in der Eigenschaft private
_model
einen Verweis auf das Modell (
_model
eine Instanz der
Field
Klasse)
_model
.
Wir haben auch vorsichtig 2 derzeit leere
_init
und
_create
, die wir etwas später implementieren werden.
3.4 Erstellen eines Sprites in einer Ansichtsklasse
So wurde die Ansicht erstellt, aber sie weiß immer noch nicht, wie man ein Sprite zeichnet. Um das Sprite mit dem von uns benötigten Rahmen auf der
_create
, müssen Sie unsere eigene private
_create
Methode
_create
:
3.5 Sprite-Positionierung
Im Moment werden alle erstellten Sprites in den Koordinaten (0, 0) der Zeichenfläche platziert. Wir müssen auch jede Zelle an der entsprechenden Position auf dem Brett platzieren. Das heißt, an die Stelle, die der Zeile und Spalte dieser Zelle entspricht. Dazu müssen wir einen Code zur Berechnung der Koordinaten jeder Instanz der
FieldView
Klasse
FieldView
.
Fügen
_position
der Klasse die Eigenschaft
_position
, die für die endgültigen Koordinaten der Zelle auf dem Spielfeld verantwortlich ist:
Da wir die
_offset
und dementsprechend die Zellen in ihr relativ zur Mitte des Bildschirms ausrichten möchten, benötigen wir auch die Eigenschaft
_offset
, die den Versatz dieser bestimmten Zelle relativ zum linken und oberen Rand des Bildschirms angibt. Füge es mit einem privaten Getter hinzu:
So haben wir:
this._scene.cameras.main.width
die gesamte Bildschirmbreite in this._scene.cameras.main.width
.- Wir erhalten die Gesamtbreite der
this._board.cols * this.width
indem wir die Anzahl der Zellen mit der Breite einer Zelle this._board.cols * this.width
: this._board.cols * this.width
. - Indem wir die Breite der Tafel von der Breite des Bildschirms entfernen, erhalten wir einen Platz auf dem Bildschirm, der nicht von der Tafel belegt ist.
- Teilen Sie die resultierende Zahl durch 2 und Sie erhalten den Einrückungswert links und rechts von der Tafel.
- Indem wir jede Zelle um den Wert dieser Einrückung verschieben, garantieren wir die Ausrichtung der gesamten Platine entlang der
x
Achse.
Wir führen absolut ähnliche Aktionen durch, um eine vertikale Verschiebung zu erhalten.
In der
_init
Methode muss noch der erforderliche Code
_init
werden:
Die hier angegebenen Eigenschaften
this.x
,
this.y
,
this.width
und
this.height
sind die geerbten Eigenschaften der übergeordneten Klasse
Phaser.GameObjects.Sprite
. Das Ändern der Eigenschaften von
this.x
und
this.y
führt zur korrekten Positionierung des Sprites auf der Leinwand.
3.6 Erstellen einer Instanz von FieldView
Erstellen Sie eine Ansicht in der
Field
Klasse:
3.7 Anzeigetafelfelder.
Kehren wir zur
Board
Klasse zurück, die im Wesentlichen eine Sammlung von
Field
Objekten ist und Zellen erstellt.
Wir werden den Board-Erstellungscode in eine separate
_create
Methode aufnehmen und diese Methode vom Konstruktor aus aufrufen. Da wir wissen, dass wir in der
_create
Methode nicht nur Zellen erstellen, sondern auch den Code zum Erstellen von Zellen in einer separaten
_createFields
Methode
_createFields
.
In dieser Methode erstellen wir die gewünschte Anzahl von Zellen in einer verschachtelten Schleife:
Es ist Zeit, die Assembly zum Debuggen zum ersten Mal mit dem Befehl auszuführen
npm start
Stellen Sie sicher, dass in der Mitte des Bildschirms 64 Zellen in 8 Zeilen angezeigt werden.
3.8 Bomben bauen
Früher habe ich berichtet, dass in der
_create
Methode der
Board
Klasse nicht nur Felder erstellt werden. Was noch Es werden auch Bomben erzeugt und die erzeugten Zellen auf die Anzahl benachbarter Bomben eingestellt. Beginnen wir mit den Bomben.
Wir müssen N Bomben in zufälligen Zellen auf dem Brett platzieren. Wir beschreiben den Prozess der Herstellung von Bomben mit einem ungefähren Algorithmus:
Bei jeder Iteration der Schleife erhalten wir eine zufällige Zelle aus der Eigenschaft
this._fields
bis wir so viele Bomben erstellt haben, wie im Feld
this._bombs
sind. Wenn die empfangene Zelle leer ist, installieren wir eine Bombe und aktualisieren den Zähler der Bomben, die für die Erzeugung erforderlich sind.
Um eine Zufallszahl zu generieren, verwenden wir die statische Methode
Phaser.Math.Between
.
Vergessen Sie nicht, den Aufruf von
this._createBombs();
in die Datei
Board.ts
zu schreiben
this._createBombs();
am Ende der
_create
Methode
Wie Sie bereits bemerkt haben, müssen Sie die
Field
Klasse verfeinern, indem Sie den
empty
Getter und die
setBomb
Methode hinzufügen, damit dieser Code ordnungsgemäß
setBomb
.
Fügen
_value
der Field-
_value
ein privates
_value
Feld hinzu, das den Inhalt der Zelle regelt. Wir akzeptieren die folgenden Vereinbarungen.
Nach diesen Regeln entwickeln wir Methoden in der
Field
Klasse, die mit der
_value
Eigenschaft arbeiten:
3.9 Werte einstellen
Die Bomben sind angeordnet und jetzt haben wir alle Daten, um die numerischen Werte in allen Zellen festzulegen, die sie benötigen.
Lassen Sie mich daran erinnern, dass die Zelle nach den Regeln des Pioniers die Nummer haben muss, die der Anzahl der Bomben entspricht, die sich neben dieser Zelle befinden. Basierend auf dieser Regel schreiben wir den entsprechenden Pseudocode.
Erstellen Sie in der
Board
Klasse eine neue Methode und übersetzen Sie den angegebenen Pseudocode in echten Code:
Mal sehen, welche der von uns verwendeten Schnittstellen nicht implementiert sind. Sie müssen die
getClosestFields
Methode hinzufügen, um die benachbarten Zellen
getClosestFields
.
Wie erkennt man benachbarte Zellen?
Betrachten Sie beispielsweise eine beliebige Zelle der Platine, die sich nicht am Rand befindet, dh nicht in der äußersten Reihe und nicht in der äußersten Spalte. Solche Zellen haben eine maximale Anzahl von Nachbarn: 1 oben, 1 unten, 3 links und 3 rechts (einschließlich der diagonalen Zellen).
Daher unterscheiden sich die Indikatoren
_row
und
_col
in jeder der benachbarten Zellen nicht um mehr als 1. Dies bedeutet, dass wir die Differenz zwischen den Parametern
_row
und
_col
im Voraus mit dem aktuellen Feld angeben können. Fügen Sie der Klassenbeschreibung eine Konstante am Anfang der Datei hinzu:
Und jetzt können wir die fehlende Methode hinzufügen, in der wir dieses Array durchlaufen:
Vergessen Sie nicht, die Feldvariable bei jeder Iteration zu überprüfen, da nicht alle Zellen auf der Karte 8 Nachbarn haben. Beispielsweise hat die obere linke Zelle keine Nachbarn links von ihr und so weiter.
Es bleibt die Methode
getField
zu implementieren und der Methode
_create
in der Klasse
Board
alle erforderlichen Aufrufe hinzuzufügen
4. Behandlung von Eingabeereignissen
4.1 Verfolgen von Mausklickereignissen
Im Moment ist das Board vollständig initialisiert, es enthält Bomben und es gibt Zellen mit Zahlen, aber alle sind derzeit geschlossen und es gibt keine Möglichkeit, sie zu öffnen. Wir werden dies korrigieren und das Öffnen von Zellen durch Klicken mit der linken Maustaste implementieren.Zuerst müssen wir genau diesen Klick verfolgen. Fügen Sie in der Klasse FieldView
den _create
folgenden Code ganz am Ende der Methode hinzu :
In Phaser können Sie Objekte aus dem Namespace für verschiedene Ereignisse abonnieren Phaser.GameObjects
. Insbesondere abonnieren wir das click-Ereignis ( pointerdown
) als Prefab des Sprites selbst, dh als Objekt einer Klasse, von der es FieldView
geerbt wurde Phaser.GameObjects.Sprite
.Zuvor müssen wir jedoch explizit darauf hinweisen, dass das Sprite möglicherweise interaktiv ist, dh, Sie müssen im Allgemeinen die Benutzereingaben abhören. Sie müssen dies tun, indem Sie die Methode setInteractive
ohne Parameter für das Sprite selbst aufrufen , wie wir es im obigen Beispiel getan haben.Nachdem das Sprite interaktiv geworden ist, kehren wir zu der Klasse zurück, Board
in der neue Modellobjekte erstellt wurden Field
, nämlich die Methode, _createFields
und registrieren den Rückruf für die Eingabeereignisse für die Ansicht:
Sobald wir festgestellt haben, dass wir die Methode ausführen möchten, indem wir auf das Sprite klicken _onFieldClick
, müssen wir sie implementieren. Wir werden jedoch die Logik der Verarbeitung des Klicks aus der Klasse entfernen Board
. Es wird angenommen, dass es besser ist, das Modell in Abhängigkeit von der Eingabe zu verarbeiten und seine Daten entsprechend in einem separaten Controller zu ändern, dessen Ähnlichkeit die Klasse der Spielszene ist GameScene
. Daher müssen wir das Klickereignis weiterleiten, von der Klasse Board
bis zur Szene. Also machen wir:
Hier werfen wir nicht nur das Klickereignis so wie es war, sondern spezifizieren auch, welches Klickereignis es war. Dies wird in Zukunft nützlich sein, wenn wir in der Szenenklasse jede Option anders verarbeiten. Natürlich wäre es möglich, das Klick-Ereignis so zu senden, wie es ist, aber wir werden den Szenencode vereinfachen und einen Teil der Logik bezüglich des Ereignisses selbst in der Klasse belassen Field
.Kehren wir nun zur Klasse der Spielszene zurück GameScene
und fügen am Ende der Methode einen _create
Code hinzu, der Ereignisse eines Klickens auf Zellen aufzeichnet:
4.2. Linksklickverarbeitung
Wir implementieren nun die Verarbeitung von Mausklickereignissen. Und beginnen Sie mit dem Öffnen der Zellen. Zellen sollten durch Drücken der linken Taste geöffnet werden. Bevor wir mit dem Programmieren beginnen, wollen wir die Bedingungen formulieren, die erfüllt sein müssen:- Wenn Sie auf eine geschlossene Zelle klicken, sollte diese geöffnet werden
- Wenn sich eine Mine in einer offenen Zelle befindet, ist das Spiel verloren
- Befinden sich in der offenen Zelle keine Minen oder Werte, befindet sich min nicht in den benachbarten Zellen. In diesem Fall müssen Sie alle benachbarten Zellen öffnen und fortfahren, bis der Wert in der offenen Zelle angezeigt wird
- Wenn Sie auf eine offene Zelle klicken, sollten Sie überprüfen, ob alle Flaggen richtig gesetzt sind. Wenn ja, beenden Sie das Spiel mit einem Sieg
Und jetzt, um das Verständnis der erforderlichen Funktionalität zu vereinfachen, übersetzen wir die obige Logik in Pseudocode:
Jetzt verstehen wir, was programmiert werden muss. Wir implementieren die Methode _onFieldClickLeft
:
Und dann werden wir wie immer die Klassen fertigstellen Field
und Board
die Methoden implementieren, die wir im Handler aufrufen.Wir geben 3 mögliche Zustände der Zelle in der Aufzählung an States
, fügen ein Feld hinzu _state
und implementieren einen Getter für jeden möglichen Zustand:
Nachdem wir nun Zustände haben, die angeben, ob die Zelle geschlossen ist oder nicht, können wir eine Methode hinzufügen open
, die den Zustand ändert:
Jede Änderung des Modellstatus sollte ein Ereignis auslösen, das dies meldet. Daher führen wir eine zusätzliche private Methode ein, _setState
in der die gesamte Logik des Zustandswechsels implementiert wird. Diese Methode wird in allen öffentlichen Methoden des Modells aufgerufen, die ihren Status ändern sollen.Fügen Sie ein Boolesches Flag hinzu _exploded
, um genau das Feldobjekt anzugeben, das in die Luft gesprengt wurde:
Öffnen Sie nun die Klasse Board
und implementieren Sie die Methode darin openClosestFields
. Diese Methode ist rekursiv und hat die Aufgabe, alle leeren Nachbarfelder in Bezug auf die im Parameter akzeptierte Zelle zu öffnen.Der Algorithmus sieht wie folgt aus: :
Und dieses Mal haben wir bereits alle notwendigen Schnittstellen für die vollständige Implementierung dieser Methode:
completed
Board
. ? .
_fields
completed
, . ( ,
completed
Field
)
_bombs
( ),
true
, , .
, .
Board
:
completed
Der Klasse selbst muss noch ein Getter hinzugefügt werden Field
. In welchem Fall wird das Feld als erfolgreich gelöscht betrachtet? Wenn es abgebaut und markiert ist. Beide notwendigen Getter sind bereits vorhanden und wir können diese Methode hinzufügen:
Um die Verarbeitung des linken Mausklicks abzuschließen, erstellen wir eine Methode, _onGameOver
mit der wir die Verfolgung von Board-Ereignissen deaktivieren und dem Spieler das gesamte Board anzeigen. Später werden wir auch einen Rendering-Code des Statusabschlussberichts basierend auf dem Parameter hinzufügen status
.
4.3 Feldanzeige
Bevor Sie mit der Verarbeitung des Rechtsklicks beginnen, erfahren Sie, wie Sie die neu geöffneten Zellen neu zeichnen.Zu Beginn der Klasse haben Field
wir eine Methode entwickelt _setState
, die ein Ereignis auslöst, change
wenn sich der Status des Modells ändert. Wir werden dies verwenden und in der Klasse werden wir FieldView
dieses Ereignis verfolgen:
Wir haben die intermediäre Methode speziell zu einem _onStateChange
Rückruf des Modelländerungsereignisses gemacht. In Zukunft müssen wir überprüfen, wie das Modell geändert wurde, um zu verstehen, ob es durchgeführt werden muss _render
.Um das aktuelle Sprite einer Zelle in einem neuen Status anzuzeigen, müssen Sie den Frame ändern. Da wir den Atlas als Assets geladen haben, können wir die Methode aufrufen setFrame
, um den aktuellen Frame in einen neuen zu ändern.Um den Frame in eine Zeile zu bringen, haben wir geschickt den Getter verwendet _frameName
, der nun implementiert werden muss. Zunächst beschreiben wir alle möglichen Werte, die ein Zellenrahmen annehmen kann.Wir haben eine Beschreibung aller Zustände erhalten und haben bereits alle Methoden des Modells, mit denen diese Zustände erhalten werden können. Lassen Sie uns am Anfang der Datei eine kleine Konfiguration erstellen:
Die Schlüssel in diesem Objekt sind die Werte der Frames, und die Werte dieser Schlüssel sind die Rückrufe, die ein Boolesches Ergebnis zurückgeben. Basierend auf dieser Konfiguration können wir eine Methode entwickeln, um den gewünschten Frame (dh den Schlüssel aus der Konfiguration) zu erhalten:
Durch einfache Aufzählung in einer Schleife durchlaufen wir also alle Schlüssel des config-Objekts und rufen nacheinander jeden Rückruf auf. Die Funktion, die uns zuerst zurückgibt, true
gibt an, dass der Schlüssel key
bei der aktuellen Iteration der richtige Frame für den aktuellen Status des Modells ist.Wenn kein Schlüssel geeignet ist, betrachten wir als Standardstatus ein offenes Feld mit einem Wert _value
, da States
wir diesen Status in der Konfiguration nicht festgelegt haben.Jetzt können wir den Linksklick auf die Kartenfelder vollständig testen und überprüfen, wie sich die Zellen öffnen und was nach dem Öffnen angezeigt wird.4.4 Rechtsklickverarbeitung
Wie beim Erstellen des Linksklick-Handlers definieren wir zunächst die erwartete Funktionalität eindeutig. Mit einem Rechtsklick markieren wir die ausgewählte Zelle mit einem Flag. Aber es gibt bestimmte Bedingungen.- Nur ein geschlossenes Feld, das derzeit nicht markiert ist, kann markiert werden
- Wenn das Feld markiert ist, sollte ein erneuter Rechtsklick die Markierung aus dem Feld entfernen
- Beim Setzen / Entfernen eines Flags muss die Anzahl der verfügbaren Flags auf der Ebene aktualisiert und der Text mit der aktuellen Nummer angezeigt werden
Wenn wir diese Bedingungen in Pseudocode übersetzen, erhalten wir die folgenden Kommentarzeilen:
Jetzt können wir diesen Algorithmus in Aufrufe der benötigten Methoden übersetzen, auch wenn diese noch nicht entwickelt wurden:
Hier haben wir auch ein neues Feld gestartet _flags
, das zu Beginn des Spiels der Anzahl der Bomben auf dem Brett entspricht, da zu Beginn des Spiels keine einzige Flagge gesetzt wurde. Dieses Feld muss mit jedem Rechtsklick aktualisiert werden, da in diesem Fall die Flagge entweder hinzugefügt oder von der Tafel entfernt wird. Fügen Sie der Klasse einen Board
Getter hinzu countMarked
:
Das Setzen und Entfernen des Flags ändert den Status des Modells Field
. Daher implementieren wir diese Methoden in der entsprechenden Klasse ähnlich der Methode open
:
Ich möchte Sie daran erinnern, dass dadurch _setState
ein Ereignis ausgelöst change
wird, das in der Ansicht nachverfolgt wird. Daher wird das Sprite dieses Mal automatisch neu gezeichnet, wenn sich das Modell ändert.Beim Testen der entwickelten Funktionalität werden Sie mit Sicherheit feststellen, dass jedes Mal, wenn Sie mit der rechten Maustaste klicken, ein Kontextmenü geöffnet wird. Fügen Sie dem Konstruktor der Spielszene den Code hinzu, der dieses Verhalten deaktiviert:
4.5 GameSceneView-Objekt
Um die Benutzeroberfläche in der Spielszene anzuzeigen, erstellen wir eine Klasse GameSceneView
und platzieren sie in src/scripts/views/GameSceneView.ts
.In diesem Fall FieldView
verhalten wir uns anders als die Schöpfung und machen diese Klasse nicht zum Fertighaus und Erben GameObjects
.In diesem Fall müssen die folgenden Elemente aus der Szenenansicht ausgegeben werden:- Text in der Anzahl der Flags
- Exit-Taste
- Spielabschluss-Statusmeldung (Gewinn / Verlust)
Machen wir aus jedem UI-Element ein separates Feld in der Klasse GameSceneView
.Wir werden einen Stummel vorbereiten. enum Styles { Color = '#008080', Font = 'Arial' } enum Texts { Flags = 'FLAGS: ', Exit = 'EXIT', Success = 'YOU WIN!', Failure = 'YOU LOOSE' }; export class GameSceneView { private _scene: Phaser.Scene = null; private _style: {font: string, fill: string}; constructor(scene: Phaser.Scene) { this._scene = scene; this._style = {font: `28px ${Styles.Font}`, fill: Styles.Color}; this._create(); } private _create(): void { } public render() { } }
Fügen Sie Text mit der Anzahl der Flags hinzu.
Mit diesem Code wird der benötigte Text von oben und links um 50 Pixel eingerückt und auf den angegebenen Stil eingestellt. Zusätzlich setzt die Methode setOrigin
den Drehpunkt des Textes auf die Koordinaten (0, 1). Dies bedeutet, dass der Text am linken Rand ausgerichtet wird.Fügen Sie eine Statusmeldung hinzu.
Wir platzieren den Statustext in der Mitte des Bildschirms und richten ihn an der Mitte der Linie aus, indem wir setOrigin
mit Parameter 0.5 für die x-Koordinate aufrufen . Außerdem muss dieser Text standardmäßig ausgeblendet sein, da er erst nach Abschluss des Spiels angezeigt wird.Erstellen Sie eine Exit-Schaltfläche, die im Wesentlichen auch ein Textobjekt ist.
Wir platzieren die Schaltfläche in der oberen rechten Ecke des Bildschirms und verwenden sie erneut setOrigin
, um den Text dieses Mal an der rechten Kante auszurichten. Wir machen die Schaltfläche interaktiv und fügen dem Klickereignis einen Rückruf hinzu, der den Spieler zur Startszene schickt. Somit geben wir dem Spieler die Möglichkeit, das Level jederzeit zu verlassen.Es bleibt noch eine Methode zu entwickeln, mit der render
alle Elemente der Benutzeroberfläche korrekt aktualisiert und alle in erstellten Methoden aufgerufen werden können _create
.
Abhängig von der im Parameter übergebenen Eigenschaft aktualisieren wir die Benutzeroberfläche und zeigen die erforderlichen Änderungen an.Erstellen Sie in der GameScene-Klasse eine Darstellung in der Spielszene und schreiben Sie den Aufruf an die Methode _render, wo immer dies aus Gründen der Bedeutung erforderlich ist:
5. Animationen
Was für ein Fan von einem Spiel, auch wenn es so einfach ist wie das unsere, wenn es keine Animationen enthält ?! Darüber hinaus sollten wir uns, seit wir anfingen, Phaser zu studieren, mit den grundlegendsten Funktionen von Animationen vertraut machen und die Funktionalität von Zwillingen betrachten. Zwillinge werden im Framework selbst implementiert und es sind keine Bibliotheken von Drittanbietern erforderlich.Fügen Sie dem Spiel 2 Animationen hinzu: Füllen Sie das Spielfeld am Anfang mit Zellen und drehen Sie die Zelle an der Öffnung um. Beginnen wir mit dem ersten.5.1 Animation zum Füllen von Brettern
Wir stellen sicher, dass alle Zellen des Boards vom oberen linken Bildschirmrand an ihren Platz fliegen. Wenn Sie das Spiellevel starten, müssen Sie alle Zellen in die obere linke Ecke des Bildschirms verschieben und für jede Zelle die Animation der Bewegung auf die entsprechenden Koordinaten starten.Fügen Sie in der Klasse FiledView
den _create
Aufruf am Ende der Methoden hinzu _animateShow
:
B Wir implementieren die neue Methode, die wir brauchen. Darin müssen, wie oben vereinbart, zwei Dinge ausgeführt werden:- Verschieben Sie die Zelle hinter die linke obere Ecke, sodass sie auf dem Bildschirm nicht sichtbar ist
- Starten Sie die Doppelbewegung zu den gewünschten Koordinaten mit der richtigen Verzögerung
(0, 0), , , . .
_moveTo
.
tweens
.
add
:
targets
, . this
, .- .
- Die Eigenschaft
duration
ist verantwortlich für die Dauer der Animation, in unserem Fall - 600ms. - Parameter
ease
und easeParams
stellen Sie die Beschleunigungsfunktion ein. - Im Feld Verzögerung ersetzen wir den Wert aus dem zweiten Argument, das für jede einzelne Zelle unter Berücksichtigung ihrer Position auf der Platine generiert wird. Dies geschieht, damit die Zellen nicht gleichzeitig herausfliegen. Stattdessen wird jede Zelle mit einer geringen Verzögerung gegenüber der vorherigen Zelle angezeigt.
- Schließlich
onComplete
fügen wir einen Rückruf in die Eigenschaft ein , der am Ende der Tween-Aktion aufgerufen wird.
Es ist vernünftig, den Zwilling in ein Versprechen zu hüllen, damit er in Zukunft verschiedene Animationen auf wunderbare Weise andocken kann. Daher platzieren wir einen Funktionsaufruf im Rückruf resolve
, der die erfolgreiche Ausführung der Animation anzeigt.5.2 Animationen der Zellumdrehung
Es ist großartig, wenn beim Öffnen der Zelle der Effekt ihrer Umkehrung reproduziert wurde. Wie können wir das erreichen?Das Öffnen einer Zelle erfolgt derzeit durch Ändern des Frames, wenn die Methode _render
in der Ansicht aufgerufen wird . Wenn wir bei dieser Methode den Status des Modells überprüfen, sehen wir, ob die Zelle geöffnet war. Wenn die Zelle geöffnet war, starten Sie die Animation, anstatt sofort einen neuen Umkehrrahmen anzuzeigen.
Um den gewünschten Effekt zu erzielen, verwenden wir die Transformation des Sprites durch die Eigenschaft scale
. Wenn wir das Sprite entlang der Achse x
mit der Zeit auf Null skalieren , schrumpft es schließlich und verbindet die linke und rechte Seite. Und umgekehrt, wenn Sie das Sprite entlang der Achse x
von Null auf seine volle Breite skalieren , dehnen wir es auf seine volle Größe. Wir implementieren diese Logik in die Methode _animateFlip
.
In Analogie zur Methode _moveTo
implementieren wir _scaleTo
:
Bei dieser Methode nehmen wir als Parameter den Wert der Skala, mit dem wir die Größe des Sprites in beide Richtungen ändern und übergeben ihn als zweiten Parameter an das Animationskonfigurationsobjekt. Alle anderen Konfigurationsparameter sind uns bereits aus der vorherigen Animation bekannt.Jetzt werden wir das Projekt zum Testen starten und nach dem Debuggen sehen wir unser Spiel als abgeschlossen an und die Testaufgabe ist abgeschlossen! :)
Ich danke allen von Herzen, dass sie diesen Moment mit mir erreicht haben!Fazit
Kolleginnen und Kollegen, ich freue mich sehr, wenn Ihnen das im Artikel vorgestellte Material von Nutzen ist und Sie diese oder jene beschriebenen Ansätze in Ihren eigenen Projekten anwenden können. Sie können sich jederzeit an mich wenden, wenn Sie Fragen zu diesem Artikel, zur Phaser-Programmierung oder zur Arbeit in Gamedev im Allgemeinen haben. Ich begrüße die Kommunikation und freue mich auf neue Bekanntschaften und Erfahrungsaustausch!Und ich habe gerade eine Frage an Sie. Da ich Video-Tutorials zur Spieleentwicklung erstelle, habe ich natürlich ein Dutzend dieser kleinen Spiele angesammelt. Jedes Spiel öffnet das Framework auf seine Weise. Zum Beispiel haben wir in diesem Spiel das Thema Zwillinge angesprochen, aber es gibt viele andere Funktionen, wie Physik, Tilemap, Wirbelsäule usw.In diesem Zusammenhang stellt sich die Frage, ob Ihnen dieser Artikel gefallen hat und ob Sie daran interessiert sind, weiterhin Artikel wie diesen zu lesen, aber über andere kleine klassische Spiele? Wenn die Antwort ja ist, übersetze ich die Materialien meiner Video-Tutorials gerne in Textform und veröffentliche im Laufe der Zeit weiterhin neue Handbücher, aber für andere Spiele. Ich bringe die entsprechende Umfrage mit.Vielen Dank für Ihre Aufmerksamkeit! Ich freue mich auf Feedback und bis bald!