Comment écrire un sapeur dans Phaser et exécuter une tâche de test de développeur HTML5

Bonjour, chers collègues!

Je m'appelle Alexander, je suis développeur de jeux HTML5.

Dans l'une des entreprises où j'ai envoyé mon CV, on m'a demandé de terminer une tâche de test. J'ai accepté et, après 1 jour, j'ai envoyé le jeu développé conformément aux TOR HTML5.



Étant donné que je m'entraîne à la programmation de jeux, ainsi qu'à une utilisation plus efficace de mon code, j'ai décidé qu'il serait utile d'écrire un article de formation sur le projet terminé. Et puisque le test terminé a reçu une évaluation positive et a conduit à une invitation à un entretien, ma décision a probablement le droit d'exister et, peut-être, aidera quelqu'un à l'avenir.

Cet article donnera une idée de la quantité de travail suffisante pour réussir la tâche de test moyenne pour le poste de développeur HTML5. Le matériel peut également intéresser quiconque souhaite se familiariser avec le cadre Phaser. Et si vous travaillez déjà avec Phaser et que vous écrivez en JS - voyez comment développer un projet en TypeScript.

Donc, sous cat, il y a beaucoup de code TypeScript!

Présentation


Nous donnons un bref exposé du problème.

  1. Nous allons développer un jeu HTML5 simple - un sapeur classique.
  2. Comme outils principaux, nous utiliserons phaser 3, dactylographié et webpack.
  3. Le jeu sera conçu pour le bureau et exécuté dans le navigateur.

Nous fournissons des liens vers le projet final.

Liens vers la démo et la source

Et rappelez-vous la mécanique du sapeur, si tout à coup quelqu'un oublie les règles du jeu. Mais comme il s'agit d'un cas peu probable, les règles sont placées sous le spoiler :)

Règles du sapeur
Le terrain de jeu se compose de cellules disposées dans une table. Par défaut, lorsque le jeu démarre, toutes les cellules sont fermées. Des bombes sont placées dans certaines cellules.

Lorsque vous cliquez avec le bouton gauche sur une cellule fermée, elle s'ouvre. S'il y avait une bombe dans une cellule ouverte, le jeu se termine par la défaite.

S'il n'y avait pas de bombe dans la cellule, un nombre s'affiche à l'intérieur, indiquant le nombre de bombes qui se trouvent dans les cellules voisines par rapport à l'ouverture actuelle. S'il n'y a pas de bombes à proximité, la cellule semble vide.

Un clic droit sur une cellule fermée définit un indicateur dessus. La tâche du joueur est d'organiser tous les drapeaux à sa disposition afin qu'ils marquent toutes les cellules minées. Après avoir placé tous les drapeaux, le joueur appuie sur le bouton gauche de la souris sur l'une des cellules ouvertes pour vérifier s'il a gagné.

Ensuite, nous allons directement au manuel lui-même. Tout le matériel est divisé en petites étapes, chacune décrivant la mise en œuvre d'une tâche spécifique en peu de temps. Ainsi, en effectuant de petits objectifs étape par étape, nous allons finalement créer un jeu à part entière. Utilisez la table des matières si vous décidez de passer rapidement à une étape spécifique.


1. Préparation


1.1 Modèle de projet


Téléchargez le modèle de projet phaser par défaut . Il s'agit du modèle recommandé par l'auteur du framework et il nous offre la structure de répertoires suivante:
index.htmlPage HTML de lancement du jeu
webpack /base.jsconstruire la configuration pour l'environnement de test
prod.jsconstruire la configuration pour la production
src /atouts /ressources du jeu (sprites, sons, polices)
index.jspoint d'entrée
Pour notre projet, nous n'avons pas besoin du fichier index.js actuel, donc supprimez-le. Créez ensuite le répertoire /src/scripts/ et placez-y le fichier index.ts vide. Nous ajouterons tous nos scripts à ce dossier.
Il convient également de garder à l'esprit que lors de la construction d'un projet pour la production, un répertoire dist sera créé à la racine, dans lequel la version sera placée.

1.2 Configuration de la construction


Nous utiliserons le webpack pour l'assemblage. Étant donné que notre modèle a été initialement préparé pour fonctionner avec JavaScript et que nous écrivons en TypeScript, nous devons apporter de petites modifications à la configuration du collecteur.

Dans le webpack/base.js ajoutez la clé d' entry , qui indique le point d'entrée lors de la construction de notre projet, ainsi que la configuration ts-loader qui décrit les règles de construction des scripts TS:

 // 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/ }, //... 

Nous devrons également créer le fichier tsconfig.json à la racine du projet. Pour moi, il a le contenu suivant:

 { "compilerOptions": { "module": "commonjs", "lib": [ "dom", "es5", "es6", "es2015", "es2017", "es2015.promise" ], "target": "es5", "skipLibCheck": true }, "exclude": ["node_modules", "dist"] } 

1.3 Installation de modules


Installez toutes les dépendances de package.json et ajoutez-y les modules typescript et ts-loader:

 npm i npm i typescript --save-dev npm i ts-loader --save-dev 

Le projet est maintenant prêt à démarrer le développement. Nous avons 2 commandes à notre disposition qui sont déjà définies dans la propriété scripts du fichier package.json .

  1. Construire un projet de débogage et ouvrir dans un navigateur via un serveur local

     npm start 
  2. Exécutez la version à vendre et placez la version finale dans le dossier dist /

     npm run build 

1.4 Préparation des actifs


Tous les actifs de ce jeu sont honnêtement téléchargés depuis OpenGameArt (version 61x61) et ont la plus conviviale des licences appelée N'hésitez pas à utiliser , ce que la page avec le pack nous dit soigneusement). Soit dit en passant, le code présenté dans l'article a la même licence! ;)

J'ai supprimé l'image d'horloge de l'ensemble téléchargé et renommé le reste des fichiers afin d'obtenir des noms de trame faciles à utiliser. La liste des noms et des fichiers correspondants s'affiche sur l'écran ci-dessous.

À partir des sprites résultants, nous allons créer un atlas au format Phaser JSONArray dans le programme TexturePacker (il y a plus qu'assez d'une version gratuite, je n'ai encore reçu aucun travail) et placer les fichiers spritesheet.png et spritesheet.json dans le répertoire du projet src/assets/ .



2. Création de scènes


2.1 Point d'entrée


Nous commençons le développement en créant le point d'entrée décrit dans la configuration du webpack.

 // 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: [] }); 

Étant donné que le jeu que nous avons est conçu pour le bureau et remplira tout l'écran, nous utilisons hardiment toute la largeur et la hauteur du navigateur pour les champs de width et de height .
Le champ de scene est actuellement un tableau vide et nous allons le corriger!

2.2 Scène de départ


Créez la classe de la première scène dans le src/scripts/scenes/StartScene.ts :

 export class StartScene extends Phaser.Scene { constructor() { super('Start'); } public preload(): void { } public create(): void { } } 

Pour un héritage valide de Phaser.Scene nous transmettons le nom de la scène en tant que paramètre au constructeur de la classe parente.

Cette scène combinera la fonctionnalité de préchargement des ressources et l'écran de démarrage, invitant l'utilisateur au jeu.

Habituellement, dans mes projets, un joueur passe par deux scènes avant d'arriver à la première, dans cet ordre:

 Boot => Preload => Start 

Mais dans ce cas, le jeu est si simple, et il y a si peu de ressources qu'il n'y a aucune raison de mettre la précharge dans une scène distincte et encore plus de faire le chargeur de Boot séparé initial.

Nous chargerons tous les actifs dans la méthode de preload . Afin de pouvoir travailler avec l'atlas créé à l'avenir, nous devons effectuer 2 étapes:

  1. obtenir les fichiers atlas png et json en utilisant require :

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

  2. chargez-les dans la méthode de preload de la scène de départ:

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


2.3 Textes de la scène de départ


Il reste 2 choses à faire dans la scène de départ:

  1. dire au joueur comment démarrer le jeu
  2. lancer le jeu Ă  l'initiative du joueur

Pour répondre au premier point, nous créons d'abord deux énumérations au début du fichier de scène pour décrire les textes et leurs styles:

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

Et puis créez les deux textes en tant qu'objets dans la méthode create . Permettez-moi de vous rappeler que la méthode de create de scènes dans Phaser ne sera appelée qu'après le chargement de toutes les ressources dans la méthode de preload et cela nous convient parfaitement.

 // 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); } //... 

Dans un autre projet plus vaste, nous pourrions prendre les textes et les styles soit dans des fichiers locaux json soit dans des configurations distinctes, mais étant donné que nous n'avons maintenant que 2 lignes, je considère cette étape redondante et dans ce cas, je suggère de ne pas compliquer nos vies, nous limiter aux listes au début du fichier de scène.

2.4 Transition au niveau du jeu


La dernière chose que nous ferons dans cette scène avant de continuer est de suivre l'événement de clic de souris pour lancer le joueur dans le jeu:

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

Scène de niveau 2.5


A en juger par le paramètre "Game" passé à la méthode this.scene.start vous avez déjà deviné qu'il était temps de créer une deuxième scène, qui traiterait la logique principale du jeu. Créez le src/scripts/scenes/GameScene.ts :

 export class GameScene extends Phaser.Scene { constructor() { super('Game'); } public create(): void { } } 

Dans cette scène, nous n'avons pas besoin de la méthode de preload car nous avons déjà chargé toutes les ressources nécessaires dans la scène précédente.

2.6 Réglage des scènes au point d'entrée


Maintenant que les deux scènes sont créées, ajoutez-les à notre point d'entrée
src/scripts/index.ts :

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

3. Objets de jeu


Ainsi, la classe GameScene implémentera la logique au niveau du jeu. Et qu'attendons-nous du niveau de jeu des sapeurs? Visuellement, nous nous attendons à voir un terrain de jeu avec des cellules fermées. Nous savons que le champ est une table, ce qui signifie qu'il a un nombre donné de lignes et de colonnes, dans plusieurs desquelles les bombes sont confortablement placées. Ainsi, nous avons suffisamment d'informations pour créer une entité distincte qui décrit le terrain de jeu.

3.1 Plateau de jeu


Créez le src/scripts/models/Board.ts dans lequel nous src/scripts/models/Board.ts la classe Board :

 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; } } 

Faisons de la classe le successeur de Phaser.Events.EventEmitter afin d'accéder à l'interface d'enregistrement et d'appel des événements, dont nous aurons besoin à l'avenir.

Un tableau d'objets de la classe Field sera stocké dans la propriété privée _fields . Nous implémenterons ce modèle plus tard.

Nous avons configuré des propriétés numériques privées _rows et _cols pour indiquer le nombre de lignes et de colonnes du terrain de jeu. Créez des getters publics pour lire les _rows et les _cols .

Le champ _bombs nous indique le nombre de bombes qui devront être générées pour le niveau. Et dans le paramètre _scene nous passons une référence à l'objet de la scène de jeu GameScene , dans lequel nous allons créer une instance de la classe Board .

Il convient de noter que nous transférons l'objet de scène au modèle uniquement pour une transmission ultérieure aux vues, où nous ne l'utiliserons que pour afficher la vue. Le fait est que le phaseur utilise directement l'objet scène pour rendre les sprites et nous oblige donc à fournir un lien vers la scène actuelle lors de la création de préfabriqués sprite, que nous développerons à l'avenir. Et pour nous-mêmes, nous accepterons que nous transférions le lien vers la scène uniquement pour son utilisation ultérieure en tant que moteur d'affichage et convenons que nous n'appellerons pas directement les méthodes personnalisées de la scène dans les modèles et les vues.

Une fois que nous avons décidé de l'interface de création de la carte, je propose de l'initialiser dans la scène de niveau, en finalisant la classe 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); } } 

Nous prenons les paramètres de la carte en constantes au début du fichier de scène et les transmettons au constructeur de la carte lors de la création d'une instance de cette classe.

3.2 Modèle de cellule


Le tableau se compose de cellules que vous souhaitez afficher à l'écran. Chaque cellule doit être placée à la position correspondante, déterminée par la ligne et la colonne.

Les cellules sont également sélectionnées en tant qu'entité distincte. Créez le src/scripts/models/Field.ts dans lequel nous src/scripts/models/Field.ts la classe qui décrit la cellule:

 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; } } 

Chaque cellule doit avoir des métriques de ligne et de colonne dans lesquelles elle se trouve. Nous avons configuré les paramètres _board et _scene pour définir des liens vers les objets du tableau et de la scène. Nous implémentons des getters pour lire les _row , _col et _board .

3.3 Vue des cellules


La cellule abstraite est créée et maintenant nous voulons la visualiser. Pour afficher une cellule à l'écran, vous devez créer sa vue. Créez le src/scripts/views/FieldView.ts et placez-y la classe de vue:

 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 { } } 

Veuillez noter que nous avons fait de cette classe le descendant de Phaser.GameObjects.Sprite . En termes de phaser, cette classe est devenue un préfabriqué sprite. Autrement dit, j'ai obtenu la fonctionnalité de l'objet de jeu du sprite, que nous développerons davantage avec nos propres méthodes.

Regardons le constructeur de cette classe. Ici, tout d'abord, nous devons appeler le constructeur de la classe parente avec les ensembles de paramètres suivants:

  • lien vers l'objet scène (comme je l'avais prĂ©venu dans la section 3.1: phaser nous oblige Ă  crĂ©er un lien vers la scène actuelle afin de rendre les sprites)
  • CoordonnĂ©es x et y sur toile
  • la clĂ© de chaĂ®ne pour laquelle l'atlas est disponible, que nous avons chargĂ©e dans la mĂ©thode de preload de la scène de dĂ©part
  • la clĂ© de chaĂ®ne de trame dans cet atlas que vous souhaitez sĂ©lectionner pour afficher le sprite

Définissez une référence au modèle (c'est-à-dire une instance de la classe Field ) dans la propriété _model privée.

Nous avons également prudemment démarré 2 _create _init et _create actuellement vides, que nous implémenterons un peu plus tard.

3.4 Création d'un sprite dans une classe de vue


Ainsi, la vue a été créée, mais elle ne sait toujours pas comment dessiner un sprite. Pour placer l'image-objet avec le cadre dont nous avons besoin sur la toile, vous devrez modifier notre propre méthode _create privée:

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

3.5 Positionnement des sprites


Pour le moment, tous les sprites créés seront placés dans les coordonnées (0, 0) du canevas. Nous devons également placer chaque cellule dans sa position correspondante sur la carte. C'est-à-dire à l'endroit qui correspond à la ligne et à la colonne de cette cellule. Pour ce faire, nous devons écrire un code pour calculer les coordonnées de chaque instance de la classe FieldView .

Ajoutez la propriété _position à la classe, qui est responsable des coordonnées finales de la cellule sur le terrain de jeu:

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

Puisque nous voulons aligner la carte et, par conséquent, les cellules qu'elle _offset , par rapport au centre de l'écran, nous avons également besoin de la propriété _offset , indiquant le décalage de cette cellule particulière par rapport aux bords gauche et supérieur de l'écran. Ajoutez-le avec un getter privé:

 // 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 }; } //... 

Ainsi, nous:

  1. Vous avez la largeur totale de l'écran dans this._scene.cameras.main.width .
  2. Nous avons obtenu la largeur totale de la planche en multipliant le nombre de cellules par la largeur d'une cellule: this._board.cols * this.width .
  3. En enlevant la largeur de la planche à la largeur de l'écran, nous avons obtenu une place sur l'écran, non occupée par la planche.
  4. En divisant le nombre résultant par 2, nous avons obtenu la valeur de retrait à gauche et à droite de la carte.
  5. En décalant chaque cellule de la valeur de cette indentation, nous garantissons l'alignement de la carte entière le long de l'axe x .

Nous effectuons des actions absolument similaires pour obtenir un déplacement vertical.

Il reste à ajouter le code nécessaire dans la méthode _init :

 // 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; } // ... 

Les propriétés this.x , this.y , this.width et this.height sont les propriétés héritées de la classe parente Phaser.GameObjects.Sprite . La modification des propriétés de this.x et this.y conduit au positionnement correct du sprite sur la toile.

3.6 Création d'une instance de FieldView


Créez une vue dans la classe Field :

 // 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 Afficher les champs de la carte.


Revenons à la classe Board , qui est essentiellement une collection d'objets Field et va créer des cellules.

Nous allons retirer le code de création de la carte dans une méthode _create distincte et appeler cette méthode depuis le constructeur. Sachant que dans la méthode _create , nous ne créerons pas seulement des cellules, nous _createFields le code de création de cellules dans une méthode _createFields distincte.

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

C'est dans cette méthode que nous allons créer le nombre de cellules souhaité dans une boucle imbriquée:

 // 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)); } } } //... 

Il est temps pour la première fois d'exécuter l'assembly pour le débogage avec la commande

 npm start 

Assurez-vous qu'au centre de l'écran, nous nous attendons à voir 64 cellules sur 8 rangées.

3.8 Fabrication de bombes


Plus tôt, j'ai signalé que dans la méthode _create de la classe Board , nous ne créerons pas seulement des champs. Quoi d'autre? Il y aura également la création de bombes et le réglage des cellules créées au nombre de bombes voisines. Commençons par les bombes elles-mêmes.

Nous devons placer N bombes sur le plateau dans des cellules aléatoires. Nous décrivons le processus de création de bombes avec un algorithme approximatif:

                          

À chaque itération de la boucle, nous obtiendrons une cellule aléatoire de la propriété this._fields jusqu'à ce que nous créons autant de bombes que celles indiquées dans le champ this._bombs ,. Si la cellule reçue est vide, nous y installerons une bombe et mettrons à jour le compteur des bombes nécessaires à la génération.

Pour générer un nombre aléatoire, nous utilisons la méthode statique 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; //    } } } 

N'oubliez pas d'écrire l'appel à this._createBombs(); dans le fichier Board.ts this._createBombs(); à la fin de la méthode _create

Comme vous l'avez déjà remarqué, pour que ce code fonctionne correctement, vous devez affiner la classe Field en y ajoutant le getter empty et la méthode setBomb .

Ajoutez un champ _value privé à la _value Field, qui réglementera le contenu de la cellule. Nous acceptons les accords suivants.
_value === 0la cellule est vide et ne contient ni mines ni valeurs
_value === -1il y a une mine dans la cellule
_value > 0dans la cellule est le nombre de mines situées à côté de la cellule actuelle

En suivant ces règles, nous développerons des méthodes dans la classe Field qui fonctionnent avec la propriété _value :

 // 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 Réglage des valeurs


Les bombes sont disposées et nous avons maintenant toutes les données afin de définir les valeurs numériques dans toutes les cellules qui en ont besoin.

Permettez-moi de vous rappeler que selon les règles du sapeur, la cellule doit avoir le nombre qui correspond au nombre de bombes situées à côté de cette cellule. Sur la base de cette règle, nous écrivons le pseudocode correspondant.

                    

Dans la classe Board , créez une nouvelle méthode et traduisez le pseudocode spécifié en code réel:

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

Voyons laquelle des interfaces que nous utilisons n'est pas implémentée. Vous devez ajouter la méthode getClosestFields pour obtenir les cellules voisines.

Comment identifier les cellules voisines?

Par exemple, considérez toute cellule de la carte qui n'est pas sur le bord, c'est-à-dire pas dans la ligne extrême et pas dans la colonne extrême. Ces cellules ont un nombre maximum de voisins: 1 en haut, 1 en bas, 3 à gauche et 3 à droite (y compris les cellules en diagonale).

Ainsi, dans chacune des cellules voisines, les indicateurs _row et _col ne diffèrent pas de plus de 1. Cela signifie que nous pouvons spécifier à l'avance la différence entre les paramètres _row et _col avec le champ courant. Ajoutez une constante au début du fichier à la description de la classe:

 // 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} //   ]; //... 

Et maintenant, nous pouvons ajouter la méthode manquante, dans laquelle nous allons parcourir ce tableau:

 // 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; }; //... 

N'oubliez pas de vérifier la variable de field à chaque itération, car toutes les cellules de la carte n'ont pas 8 voisins. Par exemple, la cellule supérieure gauche n'aura pas de voisins à sa gauche, etc.

Il reste à implémenter la méthode getField et à ajouter tous les appels nécessaires à la méthode _create dans la classe Board

 // 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. Gestion des événements d'entrée


4.1 Suivi des événements de clic de souris


Pour le moment, le tableau est complètement initialisé, il a des bombes et il y a des cellules avec des numéros, mais toutes sont actuellement fermées et il n'y a aucun moyen de les ouvrir. Nous allons corriger cela et implémenter l'ouverture des cellules en cliquant sur le bouton gauche de la souris.

Tout d'abord, nous devons suivre ce clic. Dans la classe, FieldViewajoutez le _createcode suivant à la toute fin de la méthode :

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

Dans phaser, vous pouvez vous abonner à des objets de l'espace de noms pour différents événements Phaser.GameObjects. En particulier, nous souscrirons à l'événement click ( pointerdown) le préfabriqué du sprite lui-même, c'est-à-dire un objet d'une classe FieldViewhéritée de Phaser.GameObjects.Sprite.

Mais avant de le faire, nous devons explicitement indiquer que le sprite est potentiellement interactif, c'est-à-dire que vous devez généralement écouter les entrées de l'utilisateur à ce sujet. Vous devez le faire en appelant la méthode setInteractivesans paramètres sur le sprite lui-même, ce que nous avons fait dans l'exemple ci-dessus.

Maintenant que notre image-objet est devenue interactive, revenons à la classe Boardà l'endroit où de nouveaux objets de modèle sont créés Field, à savoir la méthode _createFieldset enregistrons le rappel des événements d'entrée pour la vue:

 // 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); } } } //... 

Une fois que nous avons établi qu'en cliquant sur le sprite que nous voulons exécuter la méthode _onFieldClick, nous devons l'implémenter. Mais nous supprimerons la logique de traitement du clic de la classe Board. On pense qu'il est préférable de traiter le modèle en fonction de l'entrée et de modifier en conséquence ses données dans un contrôleur séparé, dont la similitude est la classe de la scène du jeu GameScene. Nous devons donc transmettre l'événement de clic plus loin, de la classe Boardà la scène elle-même. Nous allons donc faire:

 // 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); } } //... 

Ici, nous ne jetons pas simplement l'événement click tel qu'il était, mais nous spécifions également de quel clic il s'agissait. Cela sera utile à l'avenir, lorsque dans la classe de scène, nous traiterons chaque option différemment. Bien sûr, il serait possible d'envoyer l'événement click tel quel, mais nous simplifierons le code de la scène, en laissant une partie de la logique concernant l'événement lui-même dans la classe Field.

Eh bien, revenons maintenant à la classe de la scène de jeu GameSceneet ajoutons un _createcode à la fin de la méthode qui suit les événements d'un clic sur les cellules:

 // 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. Traitement du clic gauche


Nous procédons à la mise en œuvre du traitement des événements de clic de souris. Et commencez par ouvrir les cellules. Les cellules doivent être ouvertes en appuyant sur le bouton gauche. Et avant de commencer la programmation, parlons des conditions qui doivent être remplies:

  1. lorsque vous cliquez sur une cellule fermée, elle doit être ouverte
  2. s'il y a une mine dans une cellule ouverte - le jeu est perdu
  3. s'il n'y a pas de mines ou de valeurs dans la cellule ouverte, alors min n'est pas dans les cellules voisines, dans ce cas, vous devez ouvrir toutes les cellules voisines et continuer jusqu'Ă  ce que la valeur apparaisse dans la cellule ouverte
  4. lorsque vous cliquez sur une cellule ouverte, vous devez vérifier si tous les drapeaux sont définis correctement et si oui, puis terminer le jeu avec une victoire

Et maintenant, pour simplifier la compréhension des fonctionnalités requises, nous traduisons la logique ci-dessus en pseudo-code:

                           

Nous comprenons maintenant ce qui doit être programmé. Nous mettons en œuvre la méthode _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); //   } } } //... 

Et puis, comme toujours, nous finaliserons les classes Fieldet y Boardimplémenterons ces méthodes que nous appelons dans le gestionnaire.

Nous indiquons 3 états possibles de la cellule dans l'énumération States, ajoutons un champ _stateet implémentons un getter pour chaque état possible:

 // 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; } //... 

Maintenant que nous avons des états indiquant si la cellule est fermée ou non, nous pouvons ajouter une méthode openqui changera l'état:

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

Chaque modification de l'état du modèle doit déclencher un événement qui le signale. Par conséquent, nous introduisons une méthode privée supplémentaire _setStatedans laquelle toute la logique du changement d'état sera implémentée. Cette méthode sera appelée dans toutes les méthodes publiques du modèle, ce qui devrait changer son état.

Ajoutez un indicateur booléen _explodedpour indiquer explicitement exactement l'objet Field qui a explosé:

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

Ouvrez maintenant la classe Boardet implémentez-y la méthode openClosestFields. Cette méthode est récursive et sa tâche sera d'ouvrir tous les champs voisins vides par rapport à la cellule acceptée dans le paramètre.
L'algorithme sera le suivant:

  :                 

Et cette fois, nous avons déjà toutes les interfaces nécessaires pour la mise en œuvre complète de cette méthode:

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

Ajoutez un getter completedà la classe Boardpour indiquer l'emplacement correct des drapeaux sur le tableau. Comment pouvons-nous déterminer si une planche a été effacée avec succès? Le nombre de champs correctement marqués doit être égal au nombre total de bombes sur le plateau.

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

Cette méthode filtre le tableau _fieldspar getter completed, ce qui devrait indiquer la validité de la marque de champ. Si la longueur du tableau filtré (dans lequel seuls les champs correctement marqués tombent, pour lesquels le getter est completeddéjà responsable de la classe Field) est égale à la valeur du champ _bombs(c'est-à-dire le nombre de bombes sur le plateau), alors nous revenons true, en d'autres termes, nous considérons que le jeu est gagné.
Cela ne nous dérange pas non plus d'avoir la possibilité d'ouvrir tout le tableau en un seul appel, ce que nous devons faire à la fin du niveau. Nous ajouterons également cette fonctionnalité à la classe Board:

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

Il reste à ajouter un getter completedà la classe elle-même Field. Dans quel cas le champ sera-t-il considéré comme effacé avec succès? S'il est extrait et signalé. Les deux getters nécessaires sont déjà là et nous pouvons ajouter cette méthode:

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

Pour terminer le traitement du clic gauche de la souris, nous allons créer une méthode _onGameOverdans laquelle nous désactiverons le suivi des événements du plateau et montrerons au joueur l'ensemble du plateau. Plus tard, nous y ajouterons également un code de rendu du rapport d'achèvement d'état basé sur le paramètre 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 Affichage sur le terrain


Avant de commencer le traitement du clic droit, nous apprendrons Ă  redessiner les cellules nouvellement ouvertes.

Plus tôt dans la classe, Fieldnous avons développé une méthode _setStatequi déclenche un événement changelorsque l'état du modèle change. Nous l'utiliserons et dans la classe, nous FieldViewtracerons cet événement:

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

Nous avons spécifiquement fait de la méthode intermédiaire un _onStateChangerappel de l'événement de changement de modèle. À l'avenir, nous devrons vérifier comment le modèle a été modifié afin de comprendre s'il est nécessaire d'effectuer _render.

Pour afficher l'image-objet actuelle d'une cellule dans un nouvel état, vous devez modifier son cadre. Puisque nous avons chargé l'atlas en tant qu'actifs, nous pouvons appeler la méthode setFrameafin de changer le cadre courant en un nouveau.

Pour obtenir le cadre sur une seule ligne, nous avons astucieusement utilisé le getter _frameName, qui doit maintenant être implémenté. Tout d'abord, nous décrivons toutes les valeurs possibles qu'une trame de cellule peut prendre.
CadreCondition
closedLe champ est fermé
flagChamp marqué
emptyLe champ est ouvert, non extrait ou rempli de valeur
exploded
le champ est ouvert, miné et explosé
mined
le champ est ouvert, miné, mais pas explosé
1...9
le champ est ouvert et affiche une valeur de 1 à 9, indiquant le nombre de bombes à côté de ce champ

Nous avons obtenu une description de tous les états et avons déjà toutes les méthodes du modèle, grâce auxquelles ces états peuvent être obtenus. Obtenons une petite configuration au début du fichier:

 // 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 } //... 

Les clés de cet objet seront les valeurs des trames et les valeurs de ces clés sont les rappels qui renvoient un résultat booléen. Sur la base de cette configuration, nous pouvons développer une méthode pour obtenir la trame souhaitée (c'est-à-dire la clé de la configuration):

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

Ainsi, par simple énumération dans une boucle, nous parcourons toutes les clés de l'objet config et appelons chaque rappel tour à tour. La fonction qui nous renvoie en premier trueindiquera que la clé keyà l'itération actuelle est le cadre correct pour l'état actuel du modèle.

Si aucune clé ne convient, alors pour l'état par défaut, nous considérerons un champ ouvert avec une valeur _value, car Statesnous n'avons pas défini cet état dans la configuration .

Maintenant, nous pouvons tester complètement le clic gauche sur les champs du tableau et vérifier comment les cellules s'ouvrent et ce qui s'affiche après leur ouverture.

4.4 Traitement du clic droit


Comme dans le cas de la création du gestionnaire de clic gauche, nous définissons d'abord clairement la fonctionnalité attendue. En cliquant avec le bouton droit, nous devons marquer la cellule sélectionnée avec un drapeau. Mais il y a certaines conditions.

  1. Seul un champ fermé qui n'est pas actuellement marqué peut être marqué
  2. Si le champ est coché, un clic droit à nouveau devrait supprimer le drapeau du champ
  3. Lors de la définition / suppression d'un indicateur, il est nécessaire de mettre à jour le nombre d'indicateurs disponibles au niveau et d'afficher le texte avec le numéro actuel

En traduisant ces conditions en pseudo-code, nous obtenons les lignes de commentaires suivantes:

                                

Nous pouvons maintenant traduire cet algorithme en appels aux méthodes dont nous avons besoin, même si elles n'ont pas encore été développées:

 // 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; //... } //... 

Ici, nous avons également commencé un nouveau champ _flags, qui au début du niveau de jeu est égal au nombre de bombes sur le plateau, car au début du jeu, aucun drapeau n'a été défini. Ce champ est obligé d'être mis à jour à chaque clic droit, car dans ce cas, le drapeau est ajouté ou supprimé du tableau. Ajoutez un Boardgetter à la classe countMarked:

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

La définition et la suppression de l'indicateur est un changement dans l'état du modèle Field, nous implémentons donc ces méthodes dans la classe correspondante de manière similaire à la méthode open:

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

Permettez-moi de vous rappeler que cela _setStatedéclenchera un événement changequi est suivi dans la vue et, par conséquent, le sprite sera redessiné automatiquement cette fois lorsque le modèle change.

Lors du test des fonctionnalités développées, vous constaterez certainement qu'à chaque fois que vous cliquez sur le bouton droit de la souris, un menu contextuel s'ouvre. Ajoutez le code qui désactive ce comportement au constructeur de la scène de jeu:

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

4.5 Objet GameSceneView


Pour afficher l'interface utilisateur sur la scène du jeu, nous allons créer une classe GameSceneViewet la placer src/scripts/views/GameSceneView.ts.

Dans ce cas, nous agirons d'une manière différente de la création FieldViewet ne ferons pas de cette classe un préfabriqué et un héritier GameObjects.
Dans ce cas, nous devons sortir les éléments suivants de la vue de la scène:

  • texte dans le nombre de drapeaux
  • bouton de sortie
  • Message d'Ă©tat de fin de partie (gagnant / perdant)

Faisons de chaque élément de l'interface utilisateur un champ distinct dans la classe GameSceneView.
Nous allons préparer un talon.

 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() { } } 

Ajoutez du texte avec le nombre de drapeaux.

 // 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); } //... 

Ce code mettra le texte dont nous avons besoin dans une position en retrait de 50 pixels à partir des côtés supérieur et gauche et le définira dans le style spécifié. De plus, la méthode setOrigindéfinit le point de pivot du texte sur les coordonnées (0, 1). Cela signifie que le texte s'alignera sur sa bordure gauche.

Ajoutez un message d'état.

 // 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; } //... 

Nous plaçons le texte d'état au centre de l'écran et l'alignons avec le milieu de la ligne en appelant setOriginavec le paramètre 0.5 pour la coordonnée x. De plus, par défaut, ce texte doit être masqué, car nous ne l'afficherons qu'à la fin du jeu.

Créez un bouton de sortie, qui dans son essence est également un objet texte.

 // 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'); }); } //... 

Nous plaçons le bouton dans le coin supérieur droit de l'écran et l'utilisons à nouveau setOriginpour aligner le texte cette fois avec son bord droit. Nous rendons le bouton interactif et ajoutons un rappel à l'événement click, qui envoie le joueur à la scène de départ. Ainsi, nous donnons au joueur la possibilité de quitter le niveau à tout moment.

Il reste à développer une méthode renderpour mettre à jour correctement tous les éléments de l'interface utilisateur et ajouter des appels à toutes les méthodes créées dans _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; } } //... 

En fonction de la propriété passée dans le paramètre, nous mettons à jour l'interface utilisateur, en affichant les modifications nécessaires.
Créez une représentation dans la scène du jeu dans la classe GameScene et écrivez l'appel à la méthode _render partout où cela est requis en signifiant:

 // 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. Animations


Quel genre de fan de créer un jeu, même aussi simple que le nôtre, s'il n'y a pas d'animations?!! De plus, depuis que nous avons commencé à étudier le phaser, familiarisons-nous avec les fonctionnalités les plus élémentaires des animations et considérons la fonctionnalité des jumeaux. Les jumeaux sont implémentés dans le cadre lui-même et aucune bibliothèque tierce n'est requise.

Ajoutez 2 animations au jeu: remplir le tableau de cellules au début et retourner la cellule à l'ouverture. Commençons par le premier.

5.1 Animation de remplissage du tableau




Nous nous assurons que toutes les cellules de la carte volent en place à partir du bord supérieur gauche de l'écran. Lors du démarrage du niveau de jeu, nous devons déplacer toutes les cellules dans le coin supérieur gauche de l'écran et pour chaque cellule démarrer l'animation du mouvement à ses coordonnées correspondantes.

Dans la classe, FiledViewajoutez l' _createappel à la fin des méthodes _animateShow:

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

B nous mettons en œuvre la nouvelle méthode dont nous avons besoin. Dans ce document, comme nous l'avons convenu ci-dessus, il est nécessaire d'effectuer 2 choses:

  1. déplacer la cellule derrière le coin supérieur gauche afin qu'elle ne soit pas visible à l'écran
  2. démarrer le mouvement jumeau aux coordonnées souhaitées avec le retard correct

 // 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); } //... 

Étant donné que le coin supérieur gauche du canevas a des coordonnées (0, 0), si nous définissons la cellule sur les coordonnées égales à ses valeurs négatives de largeur et de hauteur, cela placera la cellule derrière le coin supérieur gauche et la masquera à l'écran. Ainsi, nous avons terminé notre première tâche.

Vous atteindrez le deuxième objectif en appelant la méthode _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(); } }); }); } //... 

Pour créer une animation, nous utilisons la propriété scene tweens. Dans sa méthode, addnous passons l'objet de configuration avec les paramètres:

  • La propriĂ©tĂ© targetsici doit contenir comme valeur les objets de jeu auxquels vous souhaitez appliquer des effets d'animation. Dans notre cas, il s'agit d'un lien thisvers l'objet courant, car il s'agit d'un prĂ©fabriquĂ© de l'image-objet.
  • Les deuxième et troisième paramètres nous passent les coordonnĂ©es de la destination.
  • La propriĂ©tĂ© durationest responsable de la durĂ©e de l'animation, dans notre cas - 600ms.
  • Paramètres easeet easeParamsdĂ©finir la fonction d'accĂ©lĂ©ration.
  • Dans le champ de dĂ©lai, nous substituons la valeur du deuxième argument, qui est gĂ©nĂ©rĂ© pour chaque cellule individuelle, en tenant compte de sa position sur la carte. Ceci est fait pour que les cellules ne volent pas en mĂŞme temps. Au lieu de cela, chaque cellule apparaĂ®tra avec un lĂ©ger retard par rapport Ă  la prĂ©cĂ©dente.
  • Enfin, onCompletenous mettons un rappel dans la propriĂ©tĂ© , qui sera appelĂ© Ă  la fin de l'action d'interpolation.

Il est raisonnable d'envelopper le jumeau dans une promesse afin qu'à l'avenir il puisse magnifiquement ancrer différentes animations, nous allons donc placer un appel de fonction dans le rappel resolveindiquant la bonne exécution de l'animation.

5.2 Animations de retournement de cellule




Ce sera formidable si, lors de l'ouverture de la cellule, l'effet de son inversion se reproduisait. Comment pouvons-nous y parvenir?

L'ouverture d'une cellule s'effectue actuellement en changeant le cadre lors de l'appel de la méthode _renderdans la vue. Si nous vérifions l'état du modèle dans cette méthode, nous verrons si la cellule était ouverte. Si la cellule était ouverte, démarrez l'animation au lieu d'afficher instantanément un nouveau cadre d'inversion.

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

Pour obtenir l'effet souhaité, nous utiliserons la transformation du sprite à travers la propriété scale. Si nous redimensionnons le sprite le long de l'axe xà zéro au fil du temps , il finira par rétrécir, reliant les côtés gauche et droit. Et vice versa, si vous redimensionnez le sprite le long de l'axe xde zéro à sa pleine largeur, nous l'étirons à sa taille maximale. Nous implémentons cette logique dans la méthode _animateFlip.

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

Par analogie avec la méthode, nous mettons en _moveToœuvre _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() } }); }); } //... 

Dans cette méthode, en tant que paramètre, nous prenons la valeur de l'échelle, que nous utiliserons pour changer la taille du sprite dans les deux sens et la passer comme second paramètre à l'objet de configuration d'animation. Tous les autres paramètres de configuration nous sont déjà familiers depuis l'animation précédente.

Nous allons maintenant commencer le projet pour les tests et après le débogage, nous considérerons notre jeu terminé et la tâche de test terminée! :)

Je remercie sincèrement tout le monde d'avoir atteint ce moment avec moi!

Conclusion


Chers collègues, je serai très heureux si le matériel présenté dans l'article vous est utile et vous pouvez utiliser ces approches ou celles décrites dans vos propres projets. Vous pouvez toujours vous adresser à moi pour toute question, à la fois sur cet article, et sur la programmation phaser ou travailler dans gamedev en général. J'accueille la communication et serai heureux de faire de nouvelles connaissances et d'échanger des expériences!

Et j'ai une question pour vous en ce moment. Depuis que je crée des tutoriels vidéo sur le développement de jeux, j'ai naturellement accumulé une douzaine de ces petits jeux. Chaque jeu ouvre le framework à sa manière. Par exemple, dans ce jeu, nous avons abordé le sujet des jumeaux, mais il existe de nombreuses autres fonctionnalités, telles que la physique, le tilemap, la colonne vertébrale, etc.
À cet égard, la question est: avez-vous aimé cet article et, si oui, seriez-vous intéressé à continuer à lire des articles comme celui-ci, mais à propos d'autres petits jeux classiques? Si la réponse est oui, je me ferai un plaisir de traduire le matériel de mes didacticiels vidéo au format texte et de continuer à publier de nouveaux manuels au fil du temps, mais pour d'autres jeux. J'apporte l'enquête correspondante.

Merci Ă  tous pour votre attention! Je serai heureux de vos commentaires et Ă  bientĂ´t!

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


All Articles