So schreiben Sie einen Pionier in Phaser und führen eine HTML5-Entwicklertestaufgabe aus

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.

  1. Wir werden ein einfaches HTML5-Spiel entwickeln - einen klassischen Pionier.
  2. Als Haupttools werden wir Phaser 3, Typoscript und Webpack verwenden.
  3. 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-Regeln
Das 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:
index.htmlHTML-Seite, die das Spiel startet
Webpack /base.jsbuild config für die Testumgebung
prod.jsbuild config für die Produktion
src /Vermögenswerte /Spiele-Assets (Sprites, Sounds, Schriften)
index.jsEinstiegspunkt
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:

 // webpack/base.js //... module.exports = { entry: './src/scripts/index.ts', // ... resolve: { extensions: [ '.ts', '.tsx', '.js' ] }, module: { rules: [{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ }, //... 

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.

  1. Erstellen Sie ein Projekt zum Debuggen und öffnen Sie es in einem Browser über einen lokalen Server

     npm start 
  2. 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.

 // src/scripts/index.ts import * as Phaser from "phaser"; new Phaser.Game({ type: Phaser.AUTO, parent: "minesweeper", width: window.innerWidth, height: window.innerHeight, backgroundColor: "#F0FFFF", scene: [] }); 

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:

  1. png sich sowohl png als auch json Atlas-Dateien mit require :

     // StartScene.ts const spritesheetPng = require("./../../assets/spritesheet.png"); const spritesheetJson = require("./../../assets/spritesheet.json"); // ... 

  2. Laden Sie sie in die preload Methode der Startszene:

     // StartScene.ts // ... public preload(): void { this.load.atlas("spritesheet", spritesheetPng, spritesheetJson); } // ... 


2.3 Texte der Startszene


In der Startszene sind noch 2 Dinge zu tun:

  1. Sagen Sie dem Spieler, wie er das Spiel starten soll
  2. 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:

 // StartScene.js enum Texts { Title = 'Minesweeper HTML5', Message = 'Click anywhere to start' } enum Styles { Color = '#008080', Font = 'Arial' } //... 

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.

 // StartScene.js //... public create(): void { this.add.text( this.cameras.main.centerX, this.cameras.main.centerY - 100, Texts.Title, {font: `52px ${Styles.Font}`, fill: Styles.Color}) .setOrigin(0.5); this.add.text( this.cameras.main.centerX, this.cameras.main.centerY + 100, Texts.Message, {font: `28px ${Styles.Font}`, fill: Styles.Color}) .setOrigin(0.5); } //... 

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:

 // StartScene.js //... public create(): void { //... this.input.once('pointerdown', () => { this.scene.start('Game'); }); } //... 

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 :

 //... import { StartScene } from "./scenes/StartScene"; import { GameScene } from "./scenes/GameScene"; //... new Phaser.Game({ // ... scene: [StartScene, GameScene] }); 

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 :

  // GameScene.ts import { Board } from "../models/Board"; const Rows = 8; const Cols = 8; const Bombs = 8; export class GameScene extends Phaser.Scene { private _board: Board = null; //... public create(): void { this._board = new Board(this, Rows, Cols, Bombs); } } 

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 :

 // FieldView.js //... private _create(): void { this.scene.add.existing(this); //      this.setOrigin(0.5); //  pivot point    } //... 

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:

 // FieldView.ts //... interface Vec2 {x: number, y: number}; export class FieldView extends Phaser.GameObjects.Sprite { private _position: Vec2 = {x: 0, y: 0}; //... 

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:

 // FieldView.ts //... private get _offset(): Vec2 { return { x: (this.scene.cameras.main.width - this._model.board.cols * this.width) / 2, y: (this.scene.cameras.main.height - this._model.board.rows * this.height) / 2 }; } //... 

So haben wir:

  1. this._scene.cameras.main.width die gesamte Bildschirmbreite in this._scene.cameras.main.width .
  2. 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 .
  3. 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.
  4. Teilen Sie die resultierende Zahl durch 2 und Sie erhalten den Einrückungswert links und rechts von der Tafel.
  5. 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:

 // FieldView.ts // ... private _init(): void { const offset = this._offset; this.x = this._position.x = offset.x + this.width * this._model.col + this.width / 2; this.y = this._position.y = offset.y + this.height * this._model.row + this.height / 2; } // ... 

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:

 // Field.ts // ... private _view: FieldView = null; public get view(): FieldView { return this._view; } private _init(scene: Phaser.Scene, board: Board, row: number, col: number): void { //... this._view = new FieldView(this._scene, this); } // ... 

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 .

 // Board.ts constructor(scene: Phaser.Scene, rows: number, cols: number, bombs: number) { // ... this._create(); } private _create(): void { this._createFields(); } private _createFields(): void { } 

In dieser Methode erstellen wir die gewünschte Anzahl von Zellen in einer verschachtelten Schleife:

 // Board.ts // ... private _createFields(): void { for (let row = 0; row < this._rows; row++) { for (let col = 0; col < this._cols; col++) { this._fields.push(new Field(this._scene, this, row, col)); } } } //... 

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 .

 // Board.ts //... private _createBombs(): void { let count = this._bombs; //      while (count > 0) { //       let field = this._fields[Phaser.Math.Between(0, this._fields.length - 1)]; //    if (field.empty) { //     field.setBomb(); //     --count; //    } } } 

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.
_value === 0Die Zelle ist leer und enthält keine Minen oder Werte
_value === -1In der Zelle ist eine Mine
_value > 0In der Zelle befindet sich die Anzahl der Minen neben der aktuellen Zelle

Nach diesen Regeln entwickeln wir Methoden in der Field Klasse, die mit der _value Eigenschaft arbeiten:

 // Field.ts // ... private _value: number = 0; // ... public get value(): number { return this._value; } public set value(value) { this._value = value; } public get empty(): boolean { return this._value === 0; } public get mined(): boolean { return this._value === -1; } public get filled(): boolean { return this._value > 0; } public setBomb(): void { this._value = -1; } // ... 

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:

 // Board.ts //... private _createValues() { //      this._fields.forEach(field => { //      if (field.mined) { //     field.getClosestFields().forEach(item => { //      if (item.value >= 0) { ++item.value; } }); } }); } //... 

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:

 // Field.ts const Positions = [ {row : 0, col : 1}, //  {row : 0, col : -1}, //  {row : 1, col : 0}, //  {row : 1, col : 1}, //   {row : 1, col : -1}, //   {row : -1, col : 0}, //  {row : -1, col : 1}, //   {row : -1, col : -1} //   ]; //... 

Und jetzt können wir die fehlende Methode hinzufügen, in der wir dieses Array durchlaufen:

 // Field.ts //... public getClosestFields(): Field[] { let results = []; //      Positions.forEach(position => { //      let field = this._board.getField(this._row + position.row, this._col + position.col); //       if (field) { //     results.push(field); } }); return results; }; //... 

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

 // Board.ts //... public getField(row: number, col: number): Field { return this._fields.find(field => field.row === row && field.col === col); } //... private _create(): void { this._createFields(); this._createBombs(); this._createValues(); } //... 

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 FieldViewden _createfolgenden Code ganz am Ende der Methode hinzu :

 // FielView.ts //... private _create(): void { // ... this.setInteractive(); } //... 

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 FieldViewgeerbt 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 setInteractiveohne 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, Boardin der neue Modellobjekte erstellt wurden Field, nämlich die Methode, _createFieldsund registrieren den Rückruf für die Eingabeereignisse für die Ansicht:

 // Board.ts //... private _createFields(): void { for (let row = 0; row < this._rows; row++) { for (let col = 0; col < this._cols; col++) { const field = new Field(this._scene, this, row, col) field.view.on('pointerdown', this._onFieldClick.bind(this, field)); this._fields.push(field); } } } //... 

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 Boardbis zur Szene. Also machen wir:

 // Board.ts //... private _onFieldClick(field: Field, pointer: Phaser.Input.Pointer): void { if (pointer.leftButtonDown()) { this.emit(`left-click`, field); } else if (pointer.rightButtonDown()) { this.emit(`right-click`, field); } } //... 

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 GameSceneund fügen am Ende der Methode einen _createCode hinzu, der Ereignisse eines Klickens auf Zellen aufzeichnet:

 // Board.ts //... import { Field } from "../models/Field"; //... public create(): void { this._board = new Board(this, Rows, Cols, Bombs); this._board.on('left-click', this._onFieldClickLeft, this); this._board.on('right-click', this._onFieldClickRight, this); } private _onFieldClickLeft(field: Field): void { } private _onFieldClickRight(field: Field): void { } //... 

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:

  1. Wenn Sie auf eine geschlossene Zelle klicken, sollte diese geöffnet werden
  2. Wenn sich eine Mine in einer offenen Zelle befindet, ist das Spiel verloren
  3. 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
  4. 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:

 // GameScene.ts //... private _onFieldClickLeft(field: Field): void { if (field.closed) { //    field.open(); //   if (field.mined) { //    field.exploded = true; this._onGameOver(false); //   } else if (field.empty) { //    this._board.openClosestFields(field); //   } } else if (field.opened) { //    if (this._board.completed) { //       this._onGameOver(true); //   } } } //... 

Und dann werden wir wie immer die Klassen fertigstellen Fieldund Boarddie 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 _stateund implementieren einen Getter für jeden möglichen Zustand:

 // Field.ts enum States { Closed = 'closed', Opened = 'opened', Marked = 'flag' }; export class Field extends Phaser.Events.EventEmitter { private _state: string = States.Closed; //... public get marked(): boolean { return this._state === States.Marked; } public get closed(): boolean { return this._state === States.Closed; } public get opened(): boolean { return this._state === States.Opened; } //... 

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:

 // Field.ts //... public open(): void { this._setState(States.Opened); } private _setState(state: string): void { if (this._state !== state) { this._state = state; this.emit('change'); } } //... 

Jede Änderung des Modellstatus sollte ein Ereignis auslösen, das dies meldet. Daher führen wir eine zusätzliche private Methode ein, _setStatein 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:

 // Field.ts private _exploded: boolean = false; //... public set exploded(exploded: boolean) { this._exploded = exploded; this.emit('change'); } public get exploded(): boolean { return this._exploded; } //... 

Öffnen Sie nun die Klasse Boardund 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:

 // Board.ts //... public openClosestFields(field: Field): void { field.getClosestFields().forEach(item => {//     if (item.closed) {//    item.open();//   if (item.empty) {//    this.openClosestFields(item);//     } } }); } //... 

completed Board . ? .

 // Board.ts //... public get completed(): boolean { return this._fields.filter(field => field.completed).length === this._bombs; } //... 

_fields completed , . ( , completed Field ) _bombs ( ), true , , .
, . Board :

 // Board.ts //... public open(): void { this._fields.forEach(field => field.open()); } //... 

completedDer 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:

 // Field.ts //... public get completed(): boolean { return this.marked && this.mined; } //... 

Um die Verarbeitung des linken Mausklicks abzuschließen, erstellen wir eine Methode, _onGameOvermit 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.

 // GameScene.ts //... private _onGameOver(status: boolean) { this._board.off('left-click', this._onFieldClickLeft, this); this._board.off('right-click', this._onFieldClickRight, this); this._board.open(); } //... 

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 Fieldwir eine Methode entwickelt _setState, die ein Ereignis auslöst, changewenn sich der Status des Modells ändert. Wir werden dies verwenden und in der Klasse werden wir FieldViewdieses Ereignis verfolgen:

 // FieldView.ts //... private _init(): void { //... this._model.on('change', this._onStateChange, this); } private _onStateChange(): void { this._render(); } private _render(): void { this.setFrame(this._frameName); } //... 

Wir haben die intermediäre Methode speziell zu einem _onStateChangeRü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.
RahmenZustand
closedDas Feld ist geschlossen
flagMarkiertes Feld
emptyDas Feld ist offen, nicht vermint oder mit Wert gefüllt
exploded
Das Feld ist offen, vermint und gesprengt
mined
Das Feld ist offen, vermint, aber nicht gesprengt
1...9
Das Feld ist geöffnet und zeigt einen Wert von 1 bis 9 an, der die Anzahl der Bomben neben diesem Feld angibt

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:

 // FieldView.ts const States = { 'closed': field => field.closed, 'flag': field => field.marked, 'empty': field => field.opened && !field.mined && !field.filled, 'exploded': field => field.opened && field.mined && field.exploded, 'mined': field => field.opened && field.mined && !field.exploded } //... 

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:

 // FieldView.ts //... private get _frameName(): string { for (let key in States) { if (States[key](this._model)) { return key; } } return this._model.value.toString(); } 

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, truegibt an, dass der Schlüssel keybei 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 Stateswir 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.

  1. Nur ein geschlossenes Feld, das derzeit nicht markiert ist, kann markiert werden
  2. Wenn das Feld markiert ist, sollte ein erneuter Rechtsklick die Markierung aus dem Feld entfernen
  3. 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:

 // GameScene.ts private _flags: number = 0; //... private _onFieldClickRight(field: Field): void { if (field.closed && this._flags > 0) { //        field.addFlag(); //     } else if (field.marked) { //     field.removeFlag(); //   } this._flags = Bombs - this._board.countMarked; } //... public create(): void { this._flags = Bombs; //... } //... 

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 BoardGetter hinzu countMarked:

 // Board.ts //... public get countMarked(): number { return this._fields.filter(field => field.marked).length; } //... 

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:

 // Field.ts //... public addFlag(): void { this._setState(States.Marked); } public removeFlag(): void { this._setState(States.Closed); } //... 

Ich möchte Sie daran erinnern, dass dadurch _setStateein Ereignis ausgelöst changewird, 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:

 // GameScene.ts //... constructor() { super('Game'); //        document.querySelector("canvas").oncontextmenu = e => e.preventDefault(); } //... 

4.5 GameSceneView-Objekt


Um die Benutzeroberfläche in der Spielszene anzuzeigen, erstellen wir eine Klasse GameSceneViewund platzieren sie in src/scripts/views/GameSceneView.ts.

In diesem Fall FieldViewverhalten 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.

 // GameSceneView.ts //... private _txtFlags: Phaser.GameObjects.Text = null; //... private _createTxtFlags(): void { this._txtFlags = this._scene.add.text( 50, 50, Texts.Flags, this._style ).setOrigin(0, 1); } //... 

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 setOriginden Drehpunkt des Textes auf die Koordinaten (0, 1). Dies bedeutet, dass der Text am linken Rand ausgerichtet wird.

Fügen Sie eine Statusmeldung hinzu.

 // GameSceneView.ts //... private _txtStatus: Phaser.GameObjects.Text = null; //... private _createTxtStatus(): void { this._txtStatus = this._scene.add.text( this._scene.cameras.main.centerX, 50, Texts.Success, this._style ).setOrigin(0.5, 1); this._txtStatus.visible = false; } //... 

Wir platzieren den Statustext in der Mitte des Bildschirms und richten ihn an der Mitte der Linie aus, indem wir setOriginmit 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.

 // GameSceneView.ts //... private _btnExit: Phaser.GameObjects.Text = null; //... private _createBtnExit(): void { this._btnExit = this._scene.add.text( this._scene.cameras.main.width - 50, 50, Texts.Exit, this._style ).setOrigin(1); this._btnExit.setInteractive(); this._btnExit.once('pointerdown', () => { this._scene.scene.start('Start'); }); } //... 

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 renderalle Elemente der Benutzeroberfläche korrekt aktualisiert und alle in erstellten Methoden aufgerufen werden können _create.

 // GameSceneView.ts //... private _create(): void { this._createTxtFlags(); this._createTxtStatus(); this._createBtnExit(); } public render(data: {flags?: number, status?: boolean}) { if (typeof data.flags !== 'undefined') { this._txtFlags.text = Texts.Flags + data.flags.toString(); } if (typeof data.status !== 'undefined') { this._txtStatus.text = data.status ? Texts.Success : Texts.Failure; this._txtStatus.visible = true; } } //... 

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:

 // GameScene.ts //... import { GameSceneView } from "../views/GameSceneView"; //... export class GameScene extends Phaser.Scene { private _view: GameSceneView = null; //... private _onGameOver(status: boolean) { //... this._view.render({status}); } //... private _onFieldClickRight(field: Field): void { //... this._flags = Bombs - this._board.countMarked; this._view.render({flags: this._flags}); } //... public create(): void { //... this._view = new GameSceneView(this); this._view.render({flags: this._flags}); } //... } 

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 FiledViewden _createAufruf am Ende der Methoden hinzu _animateShow:

 // FieldView.ts //... private _create(): void { //... this._animateShow(); } //... 

B Wir implementieren die neue Methode, die wir brauchen. Darin müssen, wie oben vereinbart, zwei Dinge ausgeführt werden:

  1. Verschieben Sie die Zelle hinter die linke obere Ecke, sodass sie auf dem Bildschirm nicht sichtbar ist
  2. Starten Sie die Doppelbewegung zu den gewünschten Koordinaten mit der richtigen Verzögerung

 // FieldView.ts //... private _animateShow(): Promise<void> { this.x = -this.width; this.y = -this.height; const delay = this._model.row * 50 + this._model.col * 10; return this._moveTo(this._position, delay); } //... 

(0, 0), , , . .

_moveTo .

 // FieldView.ts //... private _moveTo(position: Vec2, delay: number): Promise<void> { return new Promise(resolve => { this.scene.tweens.add({ targets: this, x: position.x, y: position.y, duration: 600, ease: 'Elastic', easeParams: [1, 1], delay, onComplete: () => { resolve(); } }); }); } //... 

tweens . add :

  • targets , . this , .
  • .
  • Die Eigenschaft durationist verantwortlich für die Dauer der Animation, in unserem Fall - 600ms.
  • Parameter easeund easeParamsstellen 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 onCompletefü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 _renderin 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.

 // FieldView.ts //... private _onStateChange(): void { if (this._model.opened) { this._animateFlip(); } else { this._render(); } } //... 

Um den gewünschten Effekt zu erzielen, verwenden wir die Transformation des Sprites durch die Eigenschaft scale. Wenn wir das Sprite entlang der Achse xmit 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 xvon Null auf seine volle Breite skalieren , dehnen wir es auf seine volle Größe. Wir implementieren diese Logik in die Methode _animateFlip.

 // FieldView.ts //... private _animateFlip(): void { this._scaleXTo(0).then(() => { this._render(); this._scaleXTo(1); }) } //... 

In Analogie zur Methode _moveToimplementieren wir _scaleTo:

 // FieldView.ts //... private _scaleXTo(scaleX: number): Promise<void> { return new Promise(resolve => { this.scene.tweens.add({ targets: this, scaleX, ease: 'Elastic.easeInOut', easeParams: [1, 1], duration: 150, onComplete: () => { resolve() } }); }); } //... 

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!

Source: https://habr.com/ru/post/de476682/


All Articles