Ce premier TP portera sur la mise en place d'un environnement de travail JavaScript moderne dans le cadre de la présentation de la bibliothèque de création de jeu Phaser.
Ce sera l'occasion de mettre en application les bonnes pratiques et les outils associés, largement plébiscités dans le monde du développement JavaScript moderne.
Le code étant relativement simpliste, nous nous focaliserons sur les bonnes pratiques et les outils.
Les notes des TP seront obtenues à partir de vos dépôts Git, vous devrez donc prendre grand soin de la qualité de ces dépôts et de leurs « commits ».
De manière similaire, les descriptions de vos « commits » devront être claires et informatives, afin de permettre l'évaluation de la progression de votre travail.
La gestion du projet sera réalisée au travers d'un paquet npm.
Comme pour les projets Composer, votre projet va générer de nombreux petits fichiers qui seront stockés localement dans le répertoire « node_modules » de votre projet.
Les infrastructures des systèmes d'information de l'université et du département informatique compliquent l'accès rapide à de très nombreuses ressources. Afin d'obtenir des temps de réaction convenables pour les différents outils que vous utiliserez, vous êtes encouragé à travailler dans le répertoire local « /working » de votre poste de travail.
Afin de ne pas laisser votre travail uniquement sur la machine, vous devrez impérativement réaliser un push en fin de séance afin de le sauver sur le serveur Gitlab.
Vous pourrez facilement réinitialiser votre projet sur une nouvelle machine à l'aide de la commande git clone suivie de npm install dans le répertoire du projet. Cette dernière commande permet de réinstaller tous les modules npm dont dépend votre projet.
Comme nous le verrons dans les TP suivants, la mise en place du projet peut être réalisée à l'aide de la commande vite que vous utiliserez plus tard, mais dans un but pédagogique, vous allez réaliser la mise en place des éléments, à la main, un par un.
Vous commencerez par créer un répertoire de travail pour votre projet nommé r301-js-introduction.
Dans ce répertoire, vous allez créer un nouveau paquet npm à l'aide de la commande : npm init --yes, permettant d'initialiser le paquet npm en créant un fichier « package.json » dans le répertoire courant. Cette commande est normalement interactive, mais l'option --yes initialise le fichier « package.json » avec les valeurs par défaut.
Vous pouvez modifier ces valeurs en éditant directement le fichier « package.json », une liste exhaustive des différentes options est disponible dans la documentation de npm.
Dans notre cas :
main
) du projet,
private
),
type
).
La gestion de versions et l'archivage seront réalisés à l'aide de Git. Pour un rapide tour d'horizon de Git, vous pouvez vous référer au site de Monsieur Nourrit.
Vous commencerez par créer un dépôt local à l'aide de la commande : git init. Cette commande provoque la création d'un répertoire "caché" « .git » contenant les données du dépôt local.
Git propose de définir dans un fichier « .gitignore » les fichiers ou répertoires à ignorer lors de son suivi de version. En vous basant sur la documentation ou le paragraphe relatif du livre « Pro Git », vous ignorerez le répertoire « node_modules » et éventuellement les répertoires de votre éditeur.
Vous pourrez ensuite indiquer à Git de suivre tous les fichiers actuellement dans votre répertoire : git add ..
Vous pourrez contrôler les fichiers suivis (« package.json » et « .gitignore ») à l'aide de la commande git status et corriger le problème sinon. Si tout va bien, vous pourrez réaliser le premier « commit » de votre dépôt : git commit -m "Initial commit".
Vous associerez votre dépôt local à un dépôt distant nommé « r301-js-introduction ». Vous veillerez à ajouter votre intervenant de TP comme membre du projet avec un rôle de « Reporter ».
Vous pourrez ensuite associer votre dépôt local au dépôt distant à l'aide de la commande :git remote add origin URL_DU_DÉPÔT, puis pousser la réplication de votre dépôt local grâce à la commande : git push.
npm est l'acronyme de Node Package Manager, un gestionnaire de paquet pour Node. Il permet simplement d'ajouter des outils ou des bibliothèques à vos projets dans l'environnement Node.
Dans le cadre du département Informatique, les paquets npm ajoutés à votre projet sont installés en local, car vous n'avez pas les droits d'écriture sur les machines. Ils sont donc installés dans le sous répertoire « node_modules » de votre projet. Bien que vous travailliez dans le répertoire « /working » de votre poste de travail, npm conserve un cache sur votre compte des paquets installés. Vous êtes invité à configurer npm pour qu'il utilise un répertoire temporaire afin d'économiser de la place sur votre compte et de gagner en performance d'installation.
Dans le cadre des bibliothèques JavaScript pour le web, le support des imports de fichiers dans le navigateur n'est pas encore parfait, il est donc courant d'avoir recours à un « bundler » permettant de regrouper un fichier et ses dépendances en un seul fichier, optimisé, importable dans le navigateur.
Dans notre cas, nous utiliserons le « bundler » vite. Il s'agit d'un outil permettant de mettre en place rapidement des projets JavaScript pour le web, comprenant la compilation du code JavaScript et un serveur web de développement avec rafraîchissement automatique.
Dans ce TP, vous n'utiliserez pas vite pour mettre en place le projet, que vous avez déjà commencé à configurer.
Vous allez installer le paquet npm de vite manuellement, comme dépendance de développement, à l'aide de la commande suivante :
npm install vite --save-dev
Vous venez d'installer l'outil vite dans votre projet, que vous pouvez lancer à l'aide de la commande npx vite.
Cependant, vite lance un serveur web de développement, mais il ne trouve rien à vous fournir pour l'instant.
Par défaut, vite cherche un script JavaScript « index.js » ou une page HTML « index.html » dans le répertoire courant comme point d'entrée de votre projet. Comme nous souhaitons ranger tout notre code dans un répertoire « src », vous ajouterez le fichier de configuration de vite suivant (« vite.config.js ») à la racine de votre projet :
Ce script de configuration importe une fonction de node : join
du module path
permettant de concaténer des chemins de répertoire indépendamment du système. Et il expose un object JavaScript contenant les options de configuration de vite :
import.meta.dirname
est une constante contenant le chemin du répertoire contenant votre projet npm (« package.json »).
Pour finir, vous créerez les répertoires « src » et « public » qui accueilleront respectivement les sources de votre projet et les ressources statiques.
Dans le répertoire « src », vous ajouterez une page HTML 5 « index.html », vide pour l'instant, mais valide, et un script JavaScript « index.js » contenant une alerte affichant un message.
Dans le répertoire « public », vous ajouterez un script CSS qui se contentera d'un peu de mise en forme qui puisse être visible dans la page, comme changer la couleur du fond, par exemple.
Vous référencerez ces deux scripts JavaScript et CSS dans votre page HTML. Pour le script JavaScript, vous prendrez soin d'importer votre script comme un module JavaScript à l'aide de l'attribut type
.
Vous pouvez maintenant réaliser la construction de votre projet à l'aide de la commande npx vite build qui créera un répertoire « build » (que vous ignorerez dans votre versionnage) contenant votre projet. Ce répertoire contient toutes les données du site que vous allez créer, c'est lui que vous pouvez distribuer ou qu'il suffit de copier sur un serveur web pour le rendre accessible.
Avant de le distribuer, il peut être utile de contrôler que tout est bien fonctionnel. vite proposant un serveur web, il est possible de le lancer sur ce répertoire à l'aide de la commande npx vite preview, puis de vérifier dans le navigateur.
La construction du projet et le serveur de prévisualisation fonctionnent bien, mais il faut relancer la construction du projet et recharger la page pour chaque modification, ce qui entraîne une piètre expérience de développement. Une expérience plus agréable repose sur l'utilisation du serveur de développement à l'aide de la commande npx vite qui lance un serveur web local (http://localhost:5173/) servant votre page HTML à partir du répertoire « src ». Vous contrôlerez le résultat, en vérifiant que votre JavaScript et votre CSS sont bien executés. Vous constaterez au passage le fonctionnement du « hot reloading », qui actualise la page en direct lors de la modification du code source (dans « src ») mais pas pour les ressources statiques (dans « public »).
Pour finir, la configuration initiale de votre projet, afin de ne pas retenir et/ou de centraliser toutes les commandes disponibles pour le projet, il est courant de les centraliser dans la section scripts
du « package.json » sous forme de scripts npm. Vous remplacerez la section scripts
actuelle par celle-ci :
Vous pouvez maintenant lancer vos scripts npm à l'aide de la commande npm run NOM_DU_SCRIPT
Sur le même principe que précédemment, vous allez installer comme dépendance de développement le paquet npm de la bibliothèque Phaser.
Contrairement au paquet npm vite qui installe un outil JavaScript à utiliser depuis la console, Phaser installe une bibliothèque que vous allez pouvoir exploiter dans votre code. Une base simple d'utilisation que l'on retrouve facilement sur le web, pourrait être le code suivant :
Vous remplacerez le contenu du fichier « index.js » par le code précédent.
Le code commence par importer la bibliothèque Phaser dans une variable nommée Phaser
. Il crée ensuite la configuration de l'instance de jeu sous la forme d'un objet JavaScript recevant des couples clé/valeur. Et enfin, il crée l'instance de jeu à l'aide de la classe Game
de la bibliothèque et de la configuration définie précédemment.
L'objet de configuration du jeu supporte de nombreuses options. Ici :
type
correspond au moteur de rendu employé par Phaser, nous laissons Phaser choisir en fonction du support logiciel. Généralement le rendu sera réalisé en WebGL, et en cas de non-support, il se repliera sur un canvas HTML.width
et height
définissent la taille du jeu en pixels.scene
correspond à la scène du jeu qui est affichée au démarrage du jeu. Nous y reviendrons bientôt, mais pour l'instant, vous définissez une scène ne réalisant rien.Avant de commencer à expérimenter la bibliothèque Phaser, vous allez mettre en place des outils de « qualité de code » couramment utilisés par la communauté JavaScript. Ces outils ont pour vocation d'améliorer la « qualité du code » de votre projet en réalisant une analyse statique du code lors de sa production, permettant d'éviter de nombreuses erreurs ou mauvaises pratiques très rapidement, sans avoir à faire de nombreux aller-retours entre le code et le résultat produit. De plus, l'analyse statique de votre code permet d'imposer une mise en forme, qui uniformise votre base de code et simplifie le suivi du versionnage, notamment lors d'un projet collaboratif.
Pour l'analyse de votre code, vous allez installer un linter permettant de contrôler votre code lors de son édition. ESLint est un linter largement employé dans la communauté JavaScript. Il est intégralement configurable, au point qu'il est très complexe et fastidieux de le faire soi-même. Dans notre cas, nous utiliserons un ensemble de règles pré-configuré.
Pour installer ESLint, vous utiliserez l'outil interactif d'ESLint à l'aide de la commande suivante :
npx eslint --init
ESLint vous pose quelques questions pour configurer l'environnement :
Une fois les paquets npm installés, vous disposez de l'outil ESLint configuré et prêt dans votre projet. Cependant, avant de l'utiliser, vous allez ajouter quelques modifications à la configuration par défaut.
Pour commencer, vous allez exclure le répertoire « node_module » des répertoires de travail d'ESLint. La configuration à « plat » d'ESLint se compose d'un tableau contenant une liste de configuration à appliquer successivement. Dans l'état, vous devriez pouvoir ajouter, à la première configuration de votre script de configuration (« eslint.config.js »), une propriété ignores
contenant le répertoire « node_module ».
Ensuite, la configuration par défaut d'ESLint est plutôt permissive, vous allez donc ajouter un peu de contraintes sur votre code. Mais afin de ne pas configurer vous-même toutes les règles, vous allez utiliser comme base la configuration ESLint de l'équipe de développement d'ESLint. Vous installerez le paquet npm et vous l'ajouterez à la suite de votre configuration d'ESLint comme précisé dans la documentation.
Vous devriez maintenant être en mesure de constater des erreurs dans votre code à l'aide de la commande
npx eslint *.js **/*.js
Mais avant de résoudre ces erreurs, vous allez lui adjoindre un formateur de code. En effet, même si ESLint dispose de quelques règles sur la mise en forme du code, pour uniformiser le rendu du code d'un projet, il est courant de lui ajouter Prettier.
Cependant, comme il est vite fastidieux de gérer les deux outils en parallèle et que ceux-ci fonctionnent très bien de concert, il est courant d'installer un paquet désactivant les règles d'ESLint en conflit avec Prettier et le plugin ESLint permettant d'exécuter Prettier comme un ensemble de règles d'ESLint.
Comme précédemment, vous ajouterez à la suite de votre configuration d'ESLint conformément à la documentation
En relançant ESLint, vous devriez constater de nouvelles erreurs cette fois-ci concernant la mise en forme de votre code. En étudiant le message d'erreur, vous devriez voir qu'ESLint propose de régler une grosse partie de ces problèmes lui-même, à l'aide de l'option --fix
:
npx eslint *.js **/*.js --fix
Vous ajouterez un script npm permettant de fixer tous les problèmes du projet en une fois et vous vérifierez qu'il fonctionne bien.
Lors du développement, il est encore fastidieux de devoir exécuter une commande pour controller son code, vous allez donc configurer votre éditeur pour qu'il utilise ESLint pour vous montrer les erreurs directement dans votre code.
WebStorm/PhpStorm doivent fonctionner directement avec ESLint si vous l'activer dans les paramètres. VS Code nécessite l'installation du greffon ESLint, si celui-ci n'est pas présent. Vous prendrez soin de vérifié que l'option « Use Flat Config » est bien cochée afin qu'il puisse trouver votre fichier de configuration. Vos erreurs devraient maintenant être soulignées dans votre code. Il est important que vous les corrigiez toutes au fur et à mesure de votre développement.
Pour éviter de fixer vous-même toutes les erreurs qu'ESLint peut régler pour vous, vous pouvez configurer votre éditeur pour qu'il tente de fixer toutes les erreurs à la sauvegarde. WebStorm/PhpStorm propose une case à cocher « run eslint --fix on save » dans la configuration et pour VS Code, vous devez ajouter le « hook » suivant dans votre configuration :
Intéressons-nous maintenant à la dernière erreur qu'il doit vous rester, dans le fichier « index.js », qu'ESLint ne veut pas résoudre tout seul. Il s'agit d'une violation de la règle no-new
, interdisant l'utilisation de l'opérateur new
hors d'une affectation ou d'une comparaison. En effet, vous construisez ici une instance de jeu dont vous ne faites rien, celle-ci semble être détruite juste après. En réalité, l'instance de jeu est référencée dans la page web lors de la création de l'instance, il aurait été plus explicite de déporter cet enregistrement dans une méthode d'instance. Cependant, comme nous ne pouvons pas modifier la bibliothèque Phaser, vous allez définir une exception pour cette règle pour cette ligne (vous pouvez le faire directement depuis votre éditeur en survolant l'erreur avec votre curseur). Une nouvelle erreur devrait être apparue sur votre commentaire. En effet, vous venez de désactiver un contrôle, il est de bon aloi d'en précisé la raison en commentaire pour vos collègues ou votre futur vous-même.
Phaser n'est pas la seule solution pour réaliser des jeux en JavaScript, plusieurs alternatives sont disponibles, certaines plus petites, efficaces ou bas niveau... Mais la bibliothèque Phaser dispose de certains avantages, notamment, elle est fournie avec de nombreuses fonctionnalités déjà incluses, et surtout, elle dispose d'une grande communauté et d'une documentation exhaustive.
Vous avez dû constater que la surface de votre jeu ne correspond pas aux dimensions définies dans les options de configuration (800×600). En effet, par défaut, Phaser construit bien une surface de jeu à la bonne dimension, mais il l'étend ensuite pour qu'elle prenne tout l'espace, sans tenir compte du ratio largeur/hauteur.
Vous en profiterez pour vous familiariser avec la documentation pour trouver dans les options de configuration comment conserver le ratio initial de la surface de jeu.
Vous pourrez faire un tour rapide du site web de Phaser afin de découvrir les possibilités de la bibliothèque, notamment la documentation des concepts, les exemples ou les tutoriels de la communauté.
Maintenant que tout est en place, vous allez pouvoir ajouter des éléments de jeu à l'écran.
Phaser organise les différentes phases de jeu en scènes. Vous commencerez par créer un nouvel objet JavaScript en début de script qui contiendra la scène de jeu.
Une scène dans Phaser peut prendre de nombreuses formes, mais fondamentalement, il s'agit d'un conteneur contenant différentes fonctions que le jeu va invoquer à différents moments du jeu. C'est un patron de développement classique permettant d'injecter du code dans un processus, on parle généralement de « hook ».
Pour commencer, vous ajouterez à la scène une méthode create
avant de fournir cette scène à la configuration de votre jeu. Il s'agit de la méthode invoquée par Phaser, permettant de créer tous les éléments de la scène.
Conformément aux concepts Objet, vous pouvez accéder, depuis les méthodes de l'instance de scène, à l'instance de scène à l'aide de la variable this
. Depuis cette instance, vous pouvez obtenir le générateur d'objets à l'aide de la propriété add
. Ce générateur permet d'ajouter toutes sortes d'objets dans la scène, pour commencer simplement, vous ajouterez 2 disques de rayon 25 pixels, centrés respectivement en (200, 200) et (600, 200).
De manière similaire, vous ajouterez à la zone de jeu :
"A first scene"
en haut au centre,Pour positionner les éléments, tout en vous évitant des calculs fastidieux, vous pourrez redéfinir l'origine des éléments de jeu.
Vous venez d'ajouter des éléments graphiques à votre jeu, vous allez maintenant, donner un peu de vie à votre jeu, en ajoutant de l'interaction grâce à la gestion du clavier.
Tous les outils de gestion de l'interaction de Phaser sont regroupés dans la propriété input
de la scène. Nous nous focaliserons pour le moment sur la gestion du clavier.
Phaser propose plusieurs solutions pour gérer le clavier, vous utiliserez la méthode addKeys
du greffon de gestion de clavier. Vous enregistrerez l'état des touches : gauche, droite et espace.
Dans un premier temps, vous vous contenterez d'enregistrer une fonction de rappel sur l'événement "down"
de chaque touche :
Pour provoquer le flash de la caméra, vous utiliserez la méthode flash
de la caméra principale (main
) de la scène.
addKeys
pour enregistrer l'état des touches : gauche, droite et espace.
Le déplacement du rectangle est saccadé, car il est réalisé sans étape. Plusieurs solutions pourraient être envisagées pour rendre le déplacement plus fluide, nous allons utiliser le moteur physique pour laisser Phaser réaliser un déplacement visuellement réaliste.
Phaser propose 2 moteurs physiques :
Vous allez ajouter aux options de configuration les propriétés permettant à Phaser d'utiliser arcade comme moteur physique par défaut.
Maintenant que le moteur physique est défini pour le jeu, vous allez pouvoir ajouter un « corps » physique au rectangle à l'aide de la méthode existing
de l'usine à « corps » physique obtenue depuis le moteur physique de la scène.
L'objet retourné est un élément de jeu disposant d'une propriété body
référençant le « corps » physique de l'élément de jeu. Lors de l'appui sur la touche gauche, vous affecterez sa vitesse horizontale à -500. Lors de l'appui sur la touche droite, vous l'affecterez à 500. Et lorsqu'elles sont lâchées, vous la réinitialiserez à 0.
Le mouvement est maintenant plus fluide, mais le comportement n'est pas trés intuitif lorsque vous appuyez sur les touches gauche et droite simultanément. Cependant, avant de résoudre ce problème, vous allez réorganiser votre base de code pour qu'il soit plus facile à gérer. En effet, la solution consistant à mettre tout votre code dans un même fichier montre vite ses limites, et on imagine facilement qu'il va être compliqué de travailler collaborativement sur un unique fichier.
Pour commencer simplement, vous allez externaliser la configuration du jeu dans un module JavaScript « src/config.js » qui exportera par défaut la configuration du jeu, que vous importerez par défaut dans « src/index.js » pour l'utiliser lors de l'instanciation du jeu. Cependant, afin de définir la scène hors de la configuration, vous extrairez la scène de la configuration et vous l'ajouterez à l'aide de la méthode add
du gestionnaire de scènes du jeu.
Une fois votre projet de nouveau fonctionnel, vous allez créer un nouveau module JavaScript « src/object/Player.js » qui exposera par défaut une classe étendant la classe Phaser.GameObjects.Rectangle
.
Son constructeur correspondra au prototype suivant : constructor(scene, width = 50, height = 20, color = 0xffffff)
et
scene.game.canvas
).
Vous pourrez ensuite remplacer le rectangle par une instance de cette classe dans votre scène.
Vous intégrerez ensuite la gestion des touches gauche et droite dans la classe Player
, au sein d'une méthode handleKeys(keys)
qui :
keys
un objet JavaScript contenant les propriétés left
et right
avec le code touche à associer.addKeys
pour enregistrer l'état des touches gauche et droite
down
et up
. Ces écouteurs invoqueront la méthode privée #handleMove()
, vous penserez donc à définir le context.#handleMove()
qui modifiera la vitesse horizontale du rectangle en fonction de la touche appuyée :
-500
lorsque seule la touche gauche est pressée,500
lorsque seule la touche droite est pressée,0
lorsqu'aucune ou les deux sont pressées.Pour finaliser la classe du joueur, vous activerez la propriété de gestion de collision du joueur avec le monde pour empêcher le joueur de sortir de la zone de jeu.
Sur le même principe que pour le joueur, vous allez externaliser la scène dans un module JavaScript « src/scene/Play.js ». Ce module exportera par défaut une classe étendant la classe Phaser.Scene
, comportant une méthode create
.
Vous devriez maintenant avoir retrouvé un projet fonctionnel, le déplacement du joueur devrait être plus intuitif et limité à la zone de jeu.
Vous allez maintenant ajouter la possibilité pour le joueur de tirer des salves de lasers.
La création et la suppression d'objets, lorsqu'elle survient régulièrement au cours du jeu, n'est pas efficace en termes de gestion des ressources. Dans notre cas, le joueur va tirer de nombreux lasers qui vont devoir être supprimés en quittant l'écran. Pour résoudre ce problème, Phaser propose d'utiliser un groupe d'objets, qui peuvent être activés/désactivés afin de réutiliser les objets non-utilisés.
Vous commencerez par créer une nouvelle classe pour représenter un tir de laser. Cette classe étendra la classe Phaser.GameObjects.Ellipse
et sera initialisée avec une dimension (10x20 par exemple), une couleur 0xff0000
et une origine par défaut. Vous lui ajouterez un « corps » physique et vous l'ajouterez à la scène.
Vous allez maintenant pouvoir utiliser cette classe dans une nouvelle classe de gestion d'un groupe de tirs de laser. Cette classe étendra Phaser.GameObjects.Group
. Son constructeur prendra en paramètre la scene et la taille du groupe. L'instanciation du parent sera réalisée avec la scène et les options de configuration du groupe. Vous ferez en sorte que le groupe soit constitué d'instances de votre classe de tir de laser et qu'il comporte 5 éléments au maximum.
Vous pourrez utiliser une instance de votre classe à la construction de votre scene pour gérer les tirs de laser. Et lors de la gestion de la touche du clavier, vous pourrez utiliser la méthode getfirstdead
du groupe pour obtenir le premier élément disponible, ou en créer un si possible. Si un élément est disponible, vous le positionnerez au niveau du joueur, vous initialiserez sa vitesse (à -500 par exemple), vous le rendrez visible et actif.
De manière similaire à la classe Player
, vous allez extraire la gestion au clavier du tir pour l'intégrer à la classe de gestion du groupe de tirs.
Vous devriez constater qu'une fois vos 5 tirs réalisés, il n'est plus possible de tirer, en effet, les tirs ne sont jamais désactivés. Vous allez résoudre ce problème en ajoutant une méthode preUpdate
à la classe représentant un tir de laser. Cette méthode est invoquée par Phaser avant chaque mise à jour d'une instance. Vous testerez la position de l'ellipse pour la cacher et la désactiver si elle est sortie de la zone de jeu.
Sur le même principe, vous allez créer des ennemis descendant progressivement la zone de jeu.
Vous commencerez par créer une classe pour les ennemis étendant la classe Phaser.GameObjects.Arc
. Vous ferez en sorte que l'ennemi soit représenté par un cercle, et vous tracerez son périmètre avec une couleur visible.
Vous créerez une classe de gestion d'un groupe d'ennemis, cette classe recevra en paramètre la scène, la taille du groupe et la taille d'un ennemi. Cette classe devra créer un certain nombre d'ennemis régulièrement. JavaScript propose nativement plusieurs outils pour interagir avec le temps (setInterval
par exemple), mais Phaser propose ses propres solutions qui sont intégrées avec son moteur, permettant l'arrêt ou la pause automatique en relation avec le jeu.
Vous ajouterez une méthode start
à la classe de gestion d'un groupe d'ennemis qui enregistrera un nouvel événement auprès de l'horloge de la scene. Cet événement devra exécuter une fonction de rappel toutes les 2000ms de manière infinie.
La fonction de rappel devra créer de 2 à 6 ennemis. Chaque ennemi devra être positionné aléatoirement sur la largeur de la zone de jeu, sa largeur sera initialisée et sa couleur sera fixée aléatoirement. Dans un premier temps, vous pouvez les placer en haut de l'écran pour les voir apparaître.
Vous utiliserez la classe de gestion d'un groupe d'ennemis dans la scène avec une taille de 60 éléments et vous démarrerez l'apparition des ennemis après son invocation.
Les ennemis apparaissent en haut de la zone de jeu, mais ils ne descendent pas. Pour ne pas utiliser encore le moteur physique, vous allez faire descendre les ennemis par à-coup, en utilisant des « tweens » pour animer la position des ennemis.
Dans la fonction de rappel de création des ennemis, vous ajouterez un nouveau « tween » aux « tweens » de la scène. Les « tweens » sont des objets permettant d'animer les propriétés d'un objet. Dans notre cas, nous allons animer une propriété y
de 0
à 1.25 * taille_d_un_ennemi
sur 1 seconde avec une interpolation de type Quadratic.InOut
. Malheureusement, un groupe n'a pas de position, il faut donc répercuter les évolutions de y
aux éléments du groupe à l'aide de la méthode du groupe incY
dans la fonction de rappel onUpdate
de la définition du « tween ». Pour avoir le détail des paramètres de la fonction de rappel, vous devrez cliquer sur le lien vers le code de sa définition, les paramètres qui vous intéressent particulièrement sont current
et previous
.
Vous ferez en sorte que les ennemis soient désactivés en arrivant en bas afin qu'ils puissent être réutilisés.
Vous allez maintenant faire en sorte que vos tirs de laser détruisent les ennemis. Pour cela, vous commencerez par détecter la collision entre les lasers et les ennemis à l'aide de la méthode
overlap
de l'usine à physique de la scène. Pour organiser votre code, vous pourrez utiliser une méthode privée de la scène comme fonction de rappel, mais vous veillerez bien à transmettre le context de la fonction de rappel (callbackContext
). Dans cette fonction de rappel, si le laser est actif, vous désactiverez le laser et l'ennemi transmis en paramètre. Le test est nécessaire, pour ne supprimer qu'un ennemi à la fois lorsqu'ils sont superposés. La fonction de rappel est toujours appelée pour chaque collision, mais seul le premier ennemi est désactivé en même temps que le laser.
Si vous êtes attentif, comme dans les tests de collisions, nous n'avons testé que les lasers, vous devriez constater que les lasers actifs entrent en collision avec les ennemis désactivés. Pour éviter les tests de collisions inutiles, lors de la désactivation des ennemis et des lasers, vous les déplacerez hors de la zone de jeu, au-dessus pour les ennemis et en-dessous pour les lasers.
Pour finaliser le mouvement des ennemis, vous allez les faire se déplacer de gauche à droite. Dans ce but, vous commencerez par ajouter le groupe de gestion des ennemis à la scène dans le constructeur (scene.add.existing
), la scène tentera maintenant d'invoquer différents « hooks », et dans notre cas la méthode preUpdate
.
Dans la méthode preUpdate
, vous utiliserez la fonction sinus sur le temps écoulé depuis la précédente étape de rendu en seconde, pour obtenir la valeur à ajouter à l'abscisse de chaque ennemi.
Vous ajouterez à votre scène deux nouvelles propriétés, l'une pour gérer le score et l'autre pour tenir le texte affichant le score. Vous placerez l'affichage du score dans le coin supérieur gauche. Vous prendrez soin de vous assurer que le texte est affiché au-dessus des ennemis et des lasers. Pour finir, vous ferez en sorte que le score augmente pour chaque ennemi détruit.
De manière similaire, vous ajouterez une propriété pour gérer le nombre de vies restantes et l'autre pour tenir le texte affichant ce nombre. Vous voulez décrémenter ce nombre lorsqu'un ennemi atteint le bas de la zone de jeu, mais contrairement au score, vous ne disposez pas de cette information dans la scène.
Plutôt que d'ajouter du code concernant le score dans la classe de gestion des ennemis, celle-ci va émettre un événement personnalisé "bottomReached"
signalant qu'un ennemi a atteint le bas de la zone de jeu. Dans la scène, vous ajouterez un écouteur pour cet événement qui décrémentera le nombre de vies restantes.
Dans la classe représentant un ennemi, vous émettrez un événement "bottomReached"
lorsque l'ennemi parvient en bas de la zone de jeu.
En l'état, le jeu se poursuit, même avec un nombre de vies négatif. Vous ajouterez un test lors de la modification du nombre de vies pour tester si ce nombre est positif. Sinon, vous bornerez la vie à 0, vous stopperez le jeu et vous afficherez un message indiquant la fin du jeu.
Pour finaliser le jeu, vous allez ajouter des bonus, représenté par des étoiles.
Comme précédemment, vous ajouterez une classe représentant un bonus et une classe de gestion d'un groupe de bonus.
Dans la classe gérant un groupe de bonus, vous ajouterez une méthode privée #setTimeout
dont le rôle est de créer un nouveau bonus dans 1 à 4 secondes. Le bonus sera initialisé aléatoirement au-dessus de la zone de jeu, avec une vitesse verticale (500 par exemple). Vous ajouterez aussi une méthode start
qui invoquera la méthode #setTimeout
pour démarrer l'émission des bonus.
Vous ferez en sorte de désactivé le bonus lorsqu'il sortira de la zone de jeu. Lors de la désactivation vous émettrez un message personnalisé. Dans la méthode start
de la classe des groupes de bonus, vous ajouterez un gestionnaire d'événements qui invoquera la méthode #setTimeout
lorsqu'un bonus est désactivé.
Lorsque vous utiliserez et démarrerez le groupe de bonus dans la scène, vous devriez maintenant voir des bonus apparaitre régulièrement et traverser la zone de jeu de bas en haut.
Dans la scene, vous mettrez en place la détection de collision entre le joueur et les bonus. Pour réagir à la collision, dans la méthode preUpdate
d'un bonus, vous allez émettre un événement personalisé lorsque le bonus rentre en collision, ainsi que désactivé le bonus. Vous pouvez maintenant améliorer le score (de 10 par exemple), lors de l'interseption du bonus par le joueur.