Ce TP consistera à approfondir vos connaissances de la bibliothèque de création de jeu Phaser au travers de la réalisation d'un petit jeu d'arcade, similaire au jeu space invaders.
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.
Comme pour le TP précédent, vous partirez de la structure du projet du dépôt r301-js-template
. Vous commencerez donc par cloner le projet en local dans un répertoire « r301-js-invaders ».
Vous modifierez les fichiers nécessitants une mise à jour des informations (« package.json », « README.md », « src/index.html »...).
Vous re-initialiserez le dépôt Git local avant de réaliser votre premier commit.
Vous créerez un dépôt Git distant sur GitLab « r301-js-invaders », que vous associerez à votre dépôt local. Vous penserez à ajouter votre intervenant de TP comme membre du projet avec un rôle de « Reporter ».
Et pour finir, vous installerez toutes les dépendances du projet à l'aide de npm.
Vous devriez maintenant avoir un projet fonctionnel, dans lequel vous allez pouvoir créer votre jeu.
r301-js-template
.
Maintenant que votre projet est en place, vous allez ajouter le joueur sur la surface de jeu. Il sera représenté par un rectangle en bas de la surface de jeu. Le rectangle pourra déplacer le rectangle sur toutes la largeur de la surface de jeu afin de pouvoir défendre toute la zone de jeu.
Comme vous l'avez vu dans la mise en place du projet, par soucis de simplicité, les exemples de Phaser centralisent tout le code en un même fichier. Cependant, pour conserver un projet maintenable, nous allons séparer notre code en plusieurs fichiers.
Vous commencerez par créer un nouveau module JavaScript « object/Player.js » qui exposera par défaut la classe du joueur héritant de la classe Rectangle
de Phaser
Son constructeur correspondra au prototype suivant : constructor(scene, width = 50, height = 20, color = 0xffffff)
et il
existing
de l'usine à objet de la scène,Pour obtenir les dimensions de la zone de jeu, vous pourrez utiliser les propriétés de l'élément canvas du jeu (scene.game.canvas
).
Vous pourrez ensuite ajouter une instance du joueur dans votre scène, pour voir apparaître un rectangle au centre en bas de la zone de jeu.
Vous allez maintenant donner un peu de vie au joueur, en ajoutant de l'interaction grâce à la gestion du clavier. Les outils de gestion de l'interaction de Phaser sont regroupés dans la propriété input
de la scène. Nous nous focaliserons ici 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 et droite.
Vous ajouterez deux méthodes moveLeft
et moveRight
à la classe Player
permettant de soustraire ou ajouter 10 à l'abscisse du joueur.
Puis, vous ajouterez une méthode update
à la scène, dans laquelle vous testerez l'état des touches gauche et droite. Lorsqu'une seule des touches est pressée, vous invoquerez en fonction de la touche pressée, la methode moveLeft
ou moveRight
du joueur.
Joueur
.
addKeys
pour enregistrer l'état des touches : gauche et droite.
Vous avez dû constater que le rectangle du joueur peut librement sortir de la surface de jeu. Il serait assés facile de contraindre les déplacements du joueur. Mais il est encore plus facile de faire appel au gestionnaire de collisions d'un des moteurs physiques de Phaser.
En effet Phaser propose 2 moteurs physiques :
Pour le moment, vous n'allez utiliser le moteur physique que pour gérer des collisions entre les éléments du jeu. Le moteur arcade sera largement suffisant pour notre cas.
Vous ajouterez 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 joueur à 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.
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.
Le jeu propose de se défendre, contre une horde d'envahisseurs spatiaux apparissant en haut de l'écran, en tirant des salves laser. Vous allez maintenant ajouter la possibilité pour le joueur de tirer ces 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. Cette option permet en plus de limiter le nombre d'objets simultanés dans la scène.
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 (0xffaa33
par exemple) et une origine, au centre en bas 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 (à 5 par défaut). 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 la taille du groupe passé en paramètre comme taille maximum.
Vous ajouterez ensuite une méthode d'instance fire(x, y)
à cette classe qui permettra d'initialiser un tir de laser en position (x,y), si un laser est disponible.
Dans cette méthode, vous commencerez par obtenir le premier laser disponible du groupe à l'aide de la méthode d'instance getFirstDead
. Cette méthode, avec le paramètre createIfNull
à true
, retourne le premier élément inactif du groupe (créé au besoin) ou null
si aucun laser n'est disponible.
Une fois obtenu un laser disponible, vous initialiserez sa position puis vous ajouterez ensuite un « tween » pour animer le déplacement du laser vers le haut de l'écran. Ce « tween » animera la propriété y
du laser de manière linéaire pour amener sa valeur à 0 en 1s.
Vous ajouterez la gestion d'une nouvelle touche pour le tir du joueur et vous ajouterez une fonction de rappel qui provoquera le tir. Le tir se composera d'un flash de la caméra suivi de la création du laser à l'aide de la methode d'instance fire
de la classe LaserSalve
. Pour le flash de la caméra, vous utiliserez la méthode flash
de la caméra principale (main
) de la scène.
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 fonction de rappel à la propriété onComplete
du « tween » animant le laser. Cette fonction de rappel sera invoquée à la fin du « tween » et devra désactiver le laser. Pour désactiver le laser, vous utiliserez la méthode d'instance remove
avec le paramètre removeFromScene
à vrai. Supprimer les éléments de la scène vous simplifiera le travail plus tard lors de la gestion des collisions.
Sur un principe similaire, vous allez créer une vague d'ennemis descendant progressivement la zone de jeu.
Vous commencerez donc 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 disque, et vous tracerez son périmètre avec une couleur visible et vous positionnerez son origine en bas au centre. Vous lui ajouterez un « corps » physique et vous l'ajouterez à la scène.
Vous créerez ensuite une classe de gestion de vague d'ennemis, le constructeur recevra en paramètre la scène, la taille du groupe et le rayon d'un ennemi. Cette classe devra créer un certain nombre d'ennemis régulièrement.
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, le rayon de l'ennemi sera initialisé à partir de la valeur fournie au constructeur et sa couleur de remplissage sera fixée aléatoirement. Au final, leur ordonnée devra être initialisée à 0, mais dans un premier temps, vous les placerez à 50 pixels du haut 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 20 comme rayon pour les ennemis, puis vous démarrerez l'apparition des ennemis après son instanciation.
Les ennemis apparaissent progressivement dans la zone de jeu, mais ils ne se déplacent pas. Vous allez faire descendre les ennemis par à-coup, en utilisant à nouveau un « tween » pour animer la position des ennemis. Vous ajouterez ce nouveau « tween » dans la fonction de rappel de création des ennemis, aprés l'activation des nouveaux ennemis.
La propriété targets
du TweenBuilderConfig
peut recevoir un tableau de cible pour le « tween ». Pour obtenir un tableau des ennemis, vous pourrez utiliser la méthode d'instance getChildren
.
L'animation pourra être réalisée en 1s à l'aide d'une fonction d'interpolation « Quadratic.InOut ». La propriété à modifier sera l'ordonnée des ennemies, mais ici, vous souhaitez partir de sa « valeur actuelle » jusqu'à sa « valeur actuelle + rayon d'un ennemi * 3 ». Pour cela, vous utiliserez la syntaxe « '+=deltaValue'
» de la définition de l'animation d'une propriété du « tween ».
Vous devriez constater, encore une fois, qu'une fois généré les 60 ennemis, plus aucun n'est généré, car vous ne les désactivez jamais. Comme précédemment, vous utiliserez la fonction de rappel de fin du « tween » pour désactiver les ennemis. Dans cette fonction, vous parcourrez tous les ennemis du groupe, pour tester si leur ordonnée est supérieur à la hauteur de la zone d'affichage, et si oui, vous les supprimerez du groupe.
Cette fonction modifiant le groupe, en supprimant ses éléments, tout en parcourant ces éléments, peut entrainer des comportements étranges. Pour résoudre ce problème, vous allez réaliser une copie du tableau contenant les éléments du groupe avant la création du « tween » et c'est ce tableau que vous utiliserez. JavaScript propose de nombreuses solutions pour obtenir une copie d'un tableau, une solution simple et explicite consiste à utiliser l'opérateur de décomposition pour créer un nouveau tableau.
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. Cette méthode permet d'associer une fonction de rappel qui sera appelée lorsque deux éléments de jeu se chevauchent. Dans notre cas, vous utiliserez les deux groupes (lasers et ennemis) comme éléments de jeu.
La fonction de rappel recevra en paramètre les éléments de jeu en collision dans l'ordre de déclaration de la méthode overlap
. Un laser en premier paramètre si le groupe de laser a été passé en premier paramètre de overlap
, par exemple.
Lors de la collision, vous supprimerez les deux éléments en de leur groupe en veillant à les supprimer de la scène en même temps.
Pour finir, vous allez ajouter une gestion d'un score, qui comptera les ennemis détruits, ainsi qu'un système de vie, qui diminuera lorsque les ennemis parviennent en bas de l'écran. La partie s'arrêtera lorsque la vie arrivera à 0.
Vous commencerez par ajouter à la scène un nouvel élément de jeu de type texte, il permettra d'afficher le score du joueur. Vous ajouterez aussi une nouvelle propriété pour tenir la valeur du score.
Vous initialiserez le score à 0 lors de la création de la scène et lors de la déstruction d'un ennemi par un laser, vous incrémenterez le score et mettrez à jour l'affichage du texte du score.
De manière similaire, vous ajouterez une propriété pour gérer le nombre de vies restantes et un élément de jeu de type 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é "enemy:bottomReached"
signalant qu'un ennemi a atteint le bas de la zone de jeu.
Dans la classe gérant la vague d'ennemis, vous émettrez un événement "enemy:bottomReached"
lorsqu'un ennemi parvient en 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.
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.
Vous allez ajouter des bonus tombant du haut de l'écran, qui rapporteront des points lorsque le joueur les rattrape.
Comme précédemment, vous ajouterez une classe représentant un bonus, sous la forme d'une étoile, 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 2 à 4 secondes, à l'aide de la méthode d'instance addEvent
. Le bonus sera initialisé aléatoirement au-dessus de la zone de jeu et traversera l'écran de manière linéaire, vers le bas en 1s, par exemple. Vous ajouterez aussi une méthode start
qui invoquera la méthode #setTimeout
pour démarrer l'émission des bonus.
Vous penserez à supprimer les bonus parvenant en bas de l'écran. 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 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 augmenter le score (de 50 par exemple), lors de l'interception du bonus par le joueur.
Bien que les ennemis avancent par à coups, leur progression est un peu rigide. Pour ajouter un peu plus de variation dans la vague d'ennemis, vous allez ajouter un mouvement latéral aux ennemis.
Dans la classe de gestion de la vague d'ennemis, lors de la création d'un ennemi, vous lui ajouterez un « tween », qui animera sa propriété x
au cours de la descente. Ce « tween » interpolera la valeur de x
depuis la position aléatoire de l'ennemi décalée aléatoirement à gauche vers cette même position décalée aléatoirement à droite. L'interpolation sera réalisée en utilisant une interpolation Quadratic.InOut
.
Afin que cette animation se reproduise sans pause tout au long de la descente, vous rendrez ce « tween » infini, en mode « yoyo » (c'est-à-dire que l'animation comprend un retour à la valeur initiale) et sur une durée correspondant à l'intervalle de génération des ennemis (2000ms normalement).
Vous prendrez soin de faire attention que les ennemis ne puissent pas sortir de la zone de jeu.
Si vous en voulez encore, vous pouvez par exemple :