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.
- Nous allons développer un jeu HTML5 simple - un sapeur classique.
- Comme outils principaux, nous utiliserons phaser 3, dactylographié et webpack.
- 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 sapeurLe 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:
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:
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
.
- Construire un projet de débogage et ouvrir dans un navigateur via un serveur local
npm start
- 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.
É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:
- obtenir les fichiers atlas
png
et json
en utilisant require
:
- chargez-les dans la méthode de
preload
de la scène de départ:
2.3 Textes de la scène de départ
Il reste 2 choses à faire dans la scène de départ:
- dire au joueur comment démarrer le jeu
- 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:
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.
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:
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
:
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
:
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:
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:
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é:
Ainsi, nous:
- Vous avez la largeur totale de l'écran dans
this._scene.cameras.main.width
. - 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
. - 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.
- En divisant le nombre résultant par 2, nous avons obtenu la valeur de retrait à gauche et à droite de la carte.
- 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
:
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
:
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.
C'est dans cette méthode que nous allons créer le nombre de cellules souhaité dans une boucle imbriquée:
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
.
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.
En suivant ces règles, nous développerons des méthodes dans la classe
Field
qui fonctionnent avec la propriété
_value
:
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:
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:
Et maintenant, nous pouvons ajouter la méthode manquante, dans laquelle nous allons parcourir ce tableau:
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
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, FieldView
ajoutez le _create
code suivant à la toute fin de la méthode :
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 FieldView
hé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 setInteractive
sans 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 _createFields
et enregistrons le rappel des événements d'entrée pour la vue:
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:
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 GameScene
et ajoutons un _create
code à la fin de la méthode qui suit les événements d'un clic sur les cellules:
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:- lorsque vous cliquez sur une cellule fermée, elle doit être ouverte
- s'il y a une mine dans une cellule ouverte - le jeu est perdu
- 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
- 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
:
Et puis, comme toujours, nous finaliserons les classes Field
et y Board
implé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 _state
et implémentons un getter pour chaque état possible:
Maintenant que nous avons des états indiquant si la cellule est fermée ou non, nous pouvons ajouter une méthode open
qui changera l'état:
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 _setState
dans 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 _exploded
pour indiquer explicitement exactement l'objet Field qui a explosé:
Ouvrez maintenant la classe Board
et 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:
Ajoutez un getter completed
Ă la classe Board
pour 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.
Cette méthode filtre le tableau _fields
par 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 completed
dé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
:
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:
Pour terminer le traitement du clic gauche de la souris, nous allons créer une méthode _onGameOver
dans 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
.
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, Field
nous avons développé une méthode _setState
qui déclenche un événement change
lorsque l'état du modèle change. Nous l'utiliserons et dans la classe, nous FieldView
tracerons cet événement:
Nous avons spécifiquement fait de la méthode intermédiaire un _onStateChange
rappel 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 setFrame
afin 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.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:
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):
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 true
indiquera 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 States
nous 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.- Seul un champ fermé qui n'est pas actuellement marqué peut être marqué
- Si le champ est coché, un clic droit à nouveau devrait supprimer le drapeau du champ
- 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:
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 Board
getter Ă la classe countMarked
:
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
:
Permettez-moi de vous rappeler que cela _setState
déclenchera un événement change
qui 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:
4.5 Objet GameSceneView
Pour afficher l'interface utilisateur sur la scène du jeu, nous allons créer une classe GameSceneView
et la placer src/scripts/views/GameSceneView.ts
.Dans ce cas, nous agirons d'une manière différente de la création FieldView
et 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.
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 setOrigin
dé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.
Nous plaçons le texte d'état au centre de l'écran et l'alignons avec le milieu de la ligne en appelant setOrigin
avec 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.
Nous plaçons le bouton dans le coin supérieur droit de l'écran et l'utilisons à nouveau setOrigin
pour 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 render
pour mettre à jour correctement tous les éléments de l'interface utilisateur et ajouter des appels à toutes les méthodes créées dans _create
.
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:
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, FiledView
ajoutez l' _create
appel à la fin des méthodes _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:- déplacer la cellule derrière le coin supérieur gauche afin qu'elle ne soit pas visible à l'écran
- démarrer le mouvement jumeau aux coordonnées souhaitées avec le retard correct
É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
.
Pour créer une animation, nous utilisons la propriété scene tweens
. Dans sa méthode, add
nous passons l'objet de configuration avec les paramètres:- La propriété
targets
ici 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 this
vers 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é
duration
est responsable de la durée de l'animation, dans notre cas - 600ms. - Paramètres
ease
et easeParams
dé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,
onComplete
nous 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 resolve
indiquant 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 _render
dans 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.
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 x
de zéro à sa pleine largeur, nous l'étirons à sa taille maximale. Nous implémentons cette logique dans la méthode _animateFlip
.
Par analogie avec la méthode, nous mettons en _moveTo
œuvre _scaleTo
:
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!