Phaser est un framework javascript qui vous permettra de créer plus facilement votre jeu 2D avec un rendu graphique WebGL ou Canvas. En effet, il intègre une gestion de plusieurs éléments nécessaires dans un jeu vidéo, ce qui vous permettra d’avancer plus rapidement dans le développement sans être obligé de tout réinventer.
Les fonctionnalités de Phaser
Voici une liste rapide de ce que ce framework met à votre disposition :
- La gestion de la boucle principale du jeu (requestAnimationFrame)
- Le choix automatique du rendu graphique selon le matériel ou le navigateur utilisé (WebGL ou Canvas)
- Un preloader qui vous permettra de charger toutes vos ressources avant le lancement de votre jeu
- Un système de physique (gravité, collisions, rebonds, vitesse…)
- Une gestion des éléments de votre jeu, les sprites (les personnages, obstacles…), sur lesquels vous pourrez y appliquer le système de physique, y ajouter des animations ou des événements
- Un système de particules, qui pourra, par exemple, apporter du réalisme lorsque vous voudrez créer des explosions
- Une gestion des boutons, des inputs et des sons
- Une gestion des tilemaps, ce qui vous permettra de créer plus facilement le terrain de votre jeu
- Une gestion de la caméra (orientation, déplacement…)
- Une adaptation de la taille du jeu selon la taille de l’écran (desktop ou mobile)
- Et bien d’autres fonctionnalités…
Pour les personnes qui ont envie de voir un exemple concret et qui, comme moi, ne sont pas bilingues (la documentation étant en anglais :D), je vous invite à continuer ce tutoriel. En effet, nous allons voir ensemble, dans les grandes lignes, comment créer une réplique du célèbre jeu « Flappy Bird » grâce à ce framework.
Avant de commencer ce tutoriel, voici les liens vers la démonstration du jeu et vers le code source sur Github :
La version utilisée de Phaser pour ce tutoriel est la 1.1.6, la version 2 étant sortie pendant la rédaction de cet article 🙁
Si vous souhaitez passer directement à la version 2 et que vous avez déjà développé votre jeu avec la 1.1.6, il existe un guide de migration qui permet de rendre votre code compatible. Vous pourrez aussi trouver un tutoriel sur notre site qui utilise la version 2 de Phaser : Création d’un Timberman avec Phaser 2.
Commençons par le commencement !
Avant toute chose, il faut télécharger la version 1.1.6 de Phaser. Elle se trouve sous la forme d’un fichier JavaScript, minimisé ou non (à vous de voir), que vous aurez juste à inclure dans votre projet et à appeler dans votre index.html. Vous pouvez le télécharger sur le Github de Phaser.
La documentation et les exemples de la version 1.1.6 n’étant plus en ligne, j’ai pris le soin de les récupérer pour vous ! Vous trouverez donc, sur notre site, les exemples et la documentation de Phaser 1.1.6.
Voici maintenant comment organiser votre projet :
- index.html
- phaser.min.js : framework Phaser
- main.js : contiendra le code de votre jeu
- img/ : dossier qui contiendra les images
- sons/ : dossier qui contiendra les sons
- data/ : dossier qui contiendra les fichiers JSON
Les fichiers index.html et style.css
Avant de toucher au main.js, le coeur du jeu, nous allons d’abord créer les fichiers HTML et CSS :
<!DOCTYPE HTML>
<html>
<head>
<title>Flappy Bird avec Phaser.js</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<script type="text/javascript" src="phaser.min.js"></script>
<script type="text/javascript" src="main.js"/></script>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div id="flappyBird">
</div>
</body>
</html>
index.html
La balise <div id="flappyBird"></div>
contiendra le Canvas de notre jeu.
body {
padding: 0; margin: 0;
}
#flappyBird {
margin: auto;
display: table;
}
style.css
Initialisation de Phaser
Maintenant que les fichiers index.html et style.css sont créés, nous allons nous concentrer sur le coeur même du projet : main.js. Pour commencer, il faut déclarer Phaser et initialiser le jeu.
// on déclare un objet Phaser qui va contenir notre jeu en 640*960px
var game = new Phaser.Game(640, 960, Phaser.AUTO, 'flappyBird');
game.transparent = true;
var gameState = {};
// On crée un objet "load" à notre objet gameState
gameState.load = function() { };
// Cet objet load va contenir des méthodes par défaut de Phaser
// Il va nous permettre de charger nos ressources avant de lancer le jeu
gameState.load.prototype = {
preload: function() {
// Méthode qui sera appelée pour charger les ressources
// Contiendra les ressources à charger (images, sons et JSON)
// Bout de code qui va permettre au jeu de se redimensionner selon la taille de l'écran
this.game.stage.scaleMode = Phaser.StageScaleMode.SHOW_ALL;
this.game.stage.scale.setShowAll();
window.addEventListener('resize', function () {
this.game.stage.scale.refresh();
});
this.game.stage.scale.refresh();
},
create: function() {
// Est appelée après la méthode "preload" afin d'appeler l'état "main" de notre jeu
}
};
// va contenir le coeur du jeu
gameState.main = function() { };
gameState.main.prototype = {
create: function() {
// Méthode qui sera appelée pour initialiser le jeu et y intégrer les différentes ressources
},
update: function() {
// Boucle principale du jeu (détection de collisions, déplacement du personnage...)
}
};
// On ajoute les 2 objet "gameState.load" et "gameState.main" à notre objet Phaser
game.state.add('load', gameState.load);
game.state.add('main', gameState.main);
// Il ne reste plus qu'à lancer l'état "load"
game.state.start('load');
main.js - Déclaration et initialisation de Phaser
Création du jeu
Comme vous pouvez le voir, la première étape est de déclarer un objet Phaser dans la variable game
:
var game = new Phaser.Game(640, 960, Phaser.AUTO, 'flappyBird')
.
Cela va créer automatiquement une balise Canvas dans le HTML. La variable game
est très importante car c’est elle qui contiendra tous les éléments de notre jeu. Dans notre cas, nous avons passé 4 arguments à l’instanciation de l’objet Phaser :
- La largeur du Canvas en pixels
- La hauteur du Canvas en pixels
- Le rendu graphique souhaité (WebGL ou Canvas). Dans notre cas, nous laissons Phaser décider
- L’id de la balise dans laquelle doit se créer le Canvas dans index.html
Les différents états
Notre code va être divisé en 2 parties. D’un côté, nous aurons le chargement des ressources et de l’autre, le coeur du jeu. Ces 2 parties vont être considérées comme des « états » pour notre objet game
. Nous aurons donc l’état gameState.load
qui permettra de charger les ressources et l’état gameState.main
qui lancera et animera le jeu. Dans chacun de ces états, nous pouvons retrouver des méthodes propres au framework Phaser, méthodes qui se lanceront toutes seules dans un ordre bien précis. Examinons, pour chacun des états, le déroulement des actions. Dans l’état gameState.load
:
preload()
: va précharger les ressources avant de lancer le jeu
create()
: se lance après que le chargement soit terminé. Dans notre cas, cette méthode va nous permettre d’appeler l’état gameState.main
Dans l’état gameState.main
:
create()
: A défaut d’une méthode preload()
dans cet état, c’est cette méthode qui se lancera en première. Elle va permettre de paramétrer et de dessiner les ressources au sein du Canvas (initialisation des variables contenant les ressources, coordonnées des sprites…), ressources qui ont été chargées dans l’état gameState.load
update()
: Boucle principale du jeu dans laquelle vous pourrez y mettre la détection de collisions, les déplacements du personnage…
Ajout et lancement d’un état
Après avoir déclaré les différents états, il est nécessaire de les ajouter à notre objet Phaser game
. C’est ce que permettent de faire les 3 dernières lignes du code ci-dessus.
Pour finir, il faut avertir Phaser de l’état qui sera lancé en premier grâce à la méthode start()
qui fait partie de l’objet state
de game
.
Affichage du background et de l’oiseau
Pour commencer, comme nous l’avons vu avant, le chargement de toutes les ressources se fera grâce à l’état gameState.load
dans la méthode preload()
. Une fois ces ressources chargées, c’est l’état gameState.main
qui va les afficher et les manipuler grâce à la méthode create()
.
Chargement des ressources
gameState.load.prototype = {
preload: function() {
// Bout de code qui va permettre au jeu de se redimensionner selon la taille de l'écran
this.game.stage.scaleMode = Phaser.StageScaleMode.SHOW_ALL;
this.game.stage.scale.setShowAll();
window.addEventListener('resize', function () {
this.game.stage.scale.refresh();
});
this.game.stage.scale.refresh();
/**** SPRITES *****/
// bird - png et json
this.game.load.atlasJSONHash('bird', 'img/bird.png', 'data/bird.json');
// background
this.game.load.image('background', 'img/background.png');
},
create: function() {
game.state.start('main');
}
};
main.js - Chargement du background et de l'oiseau
L’image « bird.png » étant un sprite (plusieurs images en une seule – voir l’article sur Stitches), il faut lui associer un fichier JSON pour indiquer la taille et les coordonnées de chaque image. En effet, dans cette image, nous pouvons remarquer 3 oiseaux avec une position différente des ailes pour chacun d’entre eux. Le but est de pouvoir facilement reproduire l’animation de l’oiseau quand il vole. Une fois que les 2 images seront chargées, create()
sera appelé automatiquement, ce qui lancera l’état gameState.main
qui va permettre de passer à l’affichage.
Affichage des ressources
gameState.main.prototype = {
create: function() {
// Création de l'arrière-plan en tant que sprite
this.background = this.game.add.sprite(0, 0, 'background');
this.background.width = this.game.width;
this.background.height = this.game.height;
// Création de l'oiseau en tant que sprite dans le jeu avec coordonnées x = 200px et y = 0
this.bird = this.game.add.sprite(200, 0, 'bird');
this.bird.width = this.bird.width / 6.5;
this.bird.height = this.bird.height / 6.5;
// On place l'oiseau au milieu, verticalement, de l'écran
this.bird.y = this.game.height / 2 - this.bird.height / 2;
// On empêche le corps physique de rebondir lors d'une collision
this.bird.body.rebound = false;
// On place le point d'origine au centre de l'oiseau afin qu'on puisse lui affecter une rotation sur lui-même
this.bird.anchor.setTo(0.5, 0.5);
// Nous permettra de savoir si l'oiseau est dans un saut ou non
this.birdInJump = false;
},
update: function() {
// Boucle principale du jeu (détection de collisions, déplacement du personnage...)
}
};
main.js - Affichage du background et de l'oiseau
Dans un premier temps, nous ajoutons les 2 images en tant que sprite à notre game
grâce à la fonction this.game.add.sprite(x,y,sprite)
avant de modifier leur taille.
Animations de l’oiseau
L’oiseau possède 3 types d’animations différentes :
- Il flotte dans les airs quand le jeu n’a pas encore débuté
- Il bat des ailes
- Il saute et il tombe, avec un effet de rotation à chaque fois
Le flottement dans les airs
gameState.main.prototype = {
create: function() {
...
// On ajoute l'animation qui va permettre à l'oiseau de flotter dans les airs
this.tweenFlap = this.game.add.tween(this.bird);
this.tweenFlap.to({ y: this.bird.y + 20}, 400, Phaser.Easing.Quadratic.InOut, true, 0, 10000000000, true);
},
...
};
main.js - animation de l'oiseau qui flotte
Ce qui est intéressant ici, c’est la possibilité d’ajouter des animations sur un sprite que l’on a ajouté à notre jeu. Cela est rendu possible en ajoutant le sprite à la collection d’objets tween
de notre game
:
this.game.add.tween(this.bird)
.
Une fois votre tween créé, il suffit de lui affecter une animation en utilisant la méthode to()
:
this.tweenFlap.to(propriétés à animer, durée, ease, départ automatique, délai avant le départ, nombre de répétitions, effet yoyo)
.
C’est grâce à cela que nous donnons à notre oiseau l’impression de flotter dans les airs.
Le battement des ailes
gameState.main.prototype = {
create: function() {
...
// On ajoute l'animation du battement des ailes, animation contenu dans le JSON
this.bird.animations.add('fly');
// On fait démarrer l'animation, avec 8 images par seconde et répétée en boucle
this.bird.animations.play('fly', 8, true);
},
...
};
main.js - le battement des ailes
Comme vous pouvez le voir, le travail d’animation est prémâché par Phaser. Il vous suffit juste de construire correctement votre fichier JSON et d’ajouter ces 2 lignes de code pour donner à votre oiseau l’impression qu’il bat des ailes.
Contrôler le saut et la chute de l’oiseau
Pour l’instant, l’oiseau fait du sur place. Il va donc falloir créer une action au « click » de l’utilisateur qui va permettre, dans un premier temps, de déclencher le processus de saut et de chute. Pour ce faire, il faut ajouter un événement « click » sur le jeu afin d’appeler la fonction dans laquelle se trouvera l’action de chute et de saut :
gameState.main.prototype = {
create: function() {
...
// Au click, on appelle la fonction "start()"
this.game.input.onTap.add(this.start, this);
},
start: function() {
// Gravité de l'oiseau
this.bird.body.gravity.y = 2000;
// Premier saut
this.bird.body.velocity.y = -600;
// On note que l'oiseau est dans l'action jump
this.birdInJump = true;
// On supprime l'événement qui se déclenchait au click sur le jeu
this.game.input.onTap.removeAll();
// Pour ajouter le jump à l'événement down sur le jeu
this.game.input.onDown.add(this.jump, this);
// On supprime l'animation de flottement
this.tweenFlap.stop();
// Et on stop l'animation de battement des ailes
this.bird.animations.stop('fly');
// Pour la rendre plus rapide
this.bird.animations.play('fly', 15, true);
},
jump: function() {
// Quand l'oiseau est encore visible (ne dépasse pas le haut de l'écran)
if(this.bird.y + this.bird.height >= 0) {
// On note que l'oiseau est dans l'action jump
this.birdInJump = true;
// Saut
this.bird.body.velocity.y = -600;
}
},
...
};
main.js - lancement du jeu sur l'événement click
Vous pouvez voir ici plusieurs fonctionnalités clés de Phaser.
Premièrement, il est possible d’ajouter et de supprimer des écouteurs d’événements très facilement sur des sprites ou sur le jeu lui-même :
this.game.input.[Evénement].add(fonction, contexte)
et this.game.input.[Evénement].removeAll()
.
Ensuite, vous pouvez ajouter de la gravité et de la vitesse (velocity
) à vos sprites sur l’axe x ou y. La gravité est permanente et l’emporte donc sur la vitesse du sprite, ce qui donne cette impression de saut à notre oiseau. En effet, dans notre exemple, à chaque fois que vous ajoutez de la vitesse à votre sprite, la gravité agit sur cette vitesse pour la diminuer de plus en plus jusqu’à la rendre nulle.
Contrôler la rotation de l’oiseau
Bon, très bien, nous avons réussi à faire voler notre oiseau tout en contrôlant ses sauts. Mais cela manque un peu de réalisme. Nous allons donc lui donner un mouvement de rotation vers le haut à chaque fois qu’il saute et un mouvement de rotation vers le bas à chaque fois qu’il tombe. Pour cela, il faut utiliser l’attribut rotation
du sprite de l’oiseau :
gameState.main.prototype = {
...
start: function() {
...
// rotation
this.bird.rotation = -Math.PI / 8;
},
jump: function() {
// Quand l'oiseau est encore visible (ne dépasse pas le haut de l'écran)
if(this.bird.y >= 0) {
// On note que l'oiseau est dans l'action jump
this.birdInJump = true;
// Saut
this.bird.body.velocity.y = -600;
// On stop l'animation de rotation quand l'oiseau tombe
if(this.tweenFall != null)
this.tweenFall.stop();
// On ajoute l'animation de rotation quand l'oiseau saute
this.tweenJump = game.add.tween(this.bird);
this.tweenJump.to({rotation: -Math.PI / 8}, 70, Phaser.Easing.Quadratic.In, true, 0, 0, true);
// On relance l'animation de battements d'ailes (coupée lorsque l'oiseau tombe)
this.bird.animations.play('fly');
this.bird.animations.frame = 0;
}
},
update: function() {
// Quand l'oiseau retombe après un jump
// Donc quand la vitesse vers le haut atteint 0 (à cause de la gravité)
if(this.bird.body.velocity.y > 0 && this.birdInJump) {
this.birdInJump = false;
// on stop l'animation de rotation quand l'oiseau saute
if(this.tweenJump != null)
this.tweenJump.stop();
// On ajoute l'animation de rotation quand l'oiseau tombe
// On la fait démarrer avec un délai de 200 ms
this.tweenFall = this.game.add.tween(this.bird);
this.tweenFall.to({rotation: Math.PI / 2}, 300, Phaser.Easing.Quadratic.In, true, 200, 0, true);
var self = this;
// Lorsque l'animation de rotation "tweenFall" commence
this.tweenFall.onStart.add(function() {
// On stop l'animation des battements d'ailes
self.bird.animations.stop('fly');
self.bird.animations.frame = 1;
});
}
}
};
main.js - rotation de l'oiseau pendant un saut et une chute
Ça commence à ressembler à quelque chose ! Voilà ce que vous devriez obtenir : Démo V1
Donner à son oiseau l’impression d’avancer
Pour donner cette impression, il faut créer le sol et le faire bouger de droite à gauche. Dans un premier temps, il faut créer le sol dans l’état gameState.load
:
gameState.load.prototype = {
preload: function() {
...
// sol
this.game.load.image('ground', 'img/ground.png');
},
...
};
main.js - chargement du sol
Il faut ensuite le créer dans le jeu et lui donner un mouvement allant de droite à gauche. Pour cela, nous allons modifier sa propriété velocity
dans l’état gameState.main
et modifier sa position au bon moment afin d’éviter qu’il sorte de l’écran :
gameState.main.prototype = {
create: function() {
// Création de l'arrière-plan en tant que sprite
this.background = this.game.add.sprite(0, 0, 'background');
this.background.width = this.game.width;
this.background.height = this.game.height;
// Création du sol
this.ground = this.game.add.sprite(0, 0, 'ground');
this.ground.width = this.game.width * 2;
this.ground.y = this.game.height - this.ground.height;
this.ground.body.immovable = true;
this.ground.body.velocity.x = -250;
this.ground.body.rebound = false;
...
},
...
update: function() {
...
// Si le centre du sol sort à gauche de l'écran
if(this.ground.x + this.ground.width / 2 <= 0) {
this.ground.x = 0;
}
}
};
main.js - création et déplacement du sol
Nous avons maintenant l’impression de voir notre oiseau avancer ! Voici le lien vers la démonstration : Démo V2
Et maintenant les tuyaux !
Pour les tuyaux, nous allons utiliser 3 images :
Pour chaque groupe de tuyaux, on va en fait répéter la première texture jusqu’à ce qu’on tombe sur l’emplacement du trou qui aura été calculé aléatoirement. C’est juste avant ou après ce trou que l’on pourra placer les images de fins des tuyaux.
gameState.load.prototype = {
preload: function() {
...
// tuyau
this.game.load.image('pipe', 'img/pipe.png');
// bout des tuyaux
this.game.load.image('pipeEndTop', 'img/pipe-end-top.png');
this.game.load.image('pipeEndBottom', 'img/pipe-end-bottom.png');
},
...
};
main.js - chargement des tuyaux
Il faut maintenant les intégrer au jeu. Pour ce faire, nous allons ajouter un group
à game
qui contiendra en fait tous les tuyaux visibles à l’écran.
gameState.main.prototype = {
create: function() {
// Création de l'arrière-plan en tant que sprite
this.background = this.game.add.sprite(0, 0, 'background');
this.background.width = this.game.width;
this.background.height = this.game.height;
// Tuyaux
this.pipes = game.add.group();
this.pipes.createMultiple(40, 'pipe');
this.pipesEndTop = game.add.group();
this.pipesEndTop.createMultiple(4, 'pipeEndTop');
this.pipesEndBottom = game.add.group();
this.pipesEndBottom.createMultiple(4, 'pipeEndBottom');
// Tuyaux à vérifier pour savoir si l'oiseau l'a dépassé (permet d'augmenter le score)
// On vérifiera toujours le premier élément seulement
this.pipesToCheckForScore = new Array();
// Tuyaux à vérifier pour savoir quand ajouter un tuyau
// On vérifiera toujours le premier élément seulement
this.pipesToCheckForAdd = new Array();
...
},
...
};
main.js - intégration des tuyaux dans le jeu
Maintenant que tout est mis en place pour la création des tuyaux, nous allons, dans un premier temps, essayer de faire apparaître un seul groupe de tuyaux qui se déplacera en même temps que le sol :
gameState.main.prototype = {
...
start: function() {
...
// Timer qui va appeler la méthode addGroupPipes au bout de 1.5 secondes
this.timer = this.game.time.events.loop(1500, this.addGroupPipes, this);
},
...
// On ajoute un groupe (une colonne) de 12 tuyaux avec un trou quelque part au milieu
addGroupPipes: function() {
// On supprime le timer qui ne nous sert plus à rien
this.game.time.events.remove(this.timer);
var nbPiecesOfPipes = 12;
var hole = Math.round(Math.random() * (nbPiecesOfPipes - 7)) + 3;
for (var i = 0; i <= nbPiecesOfPipes; i++)
if (i > hole + 1 || i < hole - 1)
this.addPieceOfPipe(this.game.world.width, this.game.world.height - this.ground.height - i * this.game.world.height / nbPiecesOfPipes, i, hole);
},
// Permet d'ajouter un morceau de tuyau
addPieceOfPipe: function(x, y, i, hole, nbPipe) {
// Si le trou est juste avant ou juste après, on place les pipeEnd
if(i == hole + 2 || i == hole - 2) {
var yDiff = 15;
var pipeEnd;
var yPipe;
if(i == hole + 2) {
// On prend le premier élément "mort" du groupe pipesEndTop
pipeEnd = this.pipesEndTop.getFirstDead();
yPipe = y + yDiff;
} else {
// On prend le premier élément "mort" du groupe pipesEndBottom
pipeEnd = this.pipesEndBottom.getFirstDead();
yPipe = y - yDiff;
}
// On change la position du bout de tuyau
pipeEnd.reset(x - 4, yPipe);
// On change la vitesse pour qu'il se déplace en même temps que le sol
pipeEnd.body.velocity.x = -250;
// On supprime ce bout de tuyau s'il sort du terrain
pipeEnd.outOfBoundsKill = true;
pipeEnd.body.immovable = true;
}
// On prend le premier élément "mort" du groupe pipes
var pipe = this.pipes.getFirstDead();
// On change la position du bout de tuyau
pipe.reset(x, y);
// On change la vitesse pour qu'il se déplace en même temps que le sol
pipe.body.velocity.x = -250;
// On supprime ce bout de tuyau s'il sort du terrain
pipe.outOfBoundsKill = true;
pipe.body.immovable = true;
// si on se trouve sur le premier morceau de tuyau du groupe
if(i == 0) {
// On enregistre le tuyau pour connaître la position de ce dernier et savoir quand augmenter le score
this.pipesToCheckForScore.push(pipe);
// Idem pour savoir quand ajouter un nouveau groupe de tuyau
this.pipesToCheckForAdd.push(pipe);
}
},
...
};
main.js - affichage d'un tuyau sur le terrain
Maintenant, une colonne de tuyaux devrait apparaître 1.5 secondes après le premier click du joueur. Mais comment faire apparaître des tuyaux à intervalle régulier comme dans le vrai Flappy Bird ?
C’est en fait très simple. A chaque création d’un tuyau, le premier morceau de ce dernier est enregistré dans le tableau pipesToCheckForAdd
. Cela nous permet donc de connaître à n’importe quel moment la position du dernier tuyau affiché. Il suffit donc d’attendre que le premier tuyau du tableau pipesToCheckForAdd
arrive vers la moitié de l’écran pour en faire apparaître un nouveau.
gameState.main.prototype = {
...
update: function() {
...
// Quand le premier tuyau se trouve au milieu du terrain
if(this.pipesToCheckForAdd.length != 0 && this.pipesToCheckForAdd[0].x + this.pipesToCheckForAdd[0].width / 2 < this.game.world.width / 2) {
this.pipesToCheckForAdd.splice(0, 1);
// On ajoute un nouveau tuyau
this.addGroupPipes();
}
}
};
main.js - ajout de nouveaux tuyaux
Voici une démonstration de notre avancement : Démo V3
Et les collisions dans tout ça ??
Détection des collisions
Maintenant que le sol et les tuyaux sont créés, il va falloir mettre en place le système de collisions entre ces derniers et l’oiseau. Encore une fois, Phaser va énormément nous faciliter la tâche :
gameState.main.prototype = {
...
update: function() {
...
// Si l'oiseau touche le sol
this.game.physics.overlap(this.bird, this.ground, this.restart, null, this);
// Si l'oiseau touche un tuyau
this.game.physics.overlap(this.bird, this.pipes, this.restart, null, this);
// Si l'oiseau touche le bout d'un tuyau
this.game.physics.overlap(this.bird, this.pipesEndTop, this.restart, null, this);
this.game.physics.overlap(this.bird, this.pipesEndBottom, this.restart, null, this);
},
restart: function() {
// On redémarre la partie
this.game.state.start('main');
}
};
main.js - détection des collisions avec l'oiseau
Comme vous pouvez le voir, grâce à la fonction overlap()
une ligne de code suffit pour détecter une collision entre 2 sprites :
this.game.physics.overlap(sprite1, sprite2, overlapCallback, processCallback, contextCallback)
.
Cette méthode prend en paramètres les 2 sprites à surveiller et une fonction de callback qui se déclenche au moment de la collision. Dans notre exemple, on redémarre la partie à chaque collision.
Redessiner la zone de collision de l’oiseau
La détection des collisions fonctionne mais il reste un problème à régler. En effet, parfois, quand l’oiseau est proche d’un tuyau, le jeu considère qu’il y a collision et redémarre la partie.
Heureusement pour nous, Phaser vous permet de voir la zone de collisions des sprites grâce à la méthode render()
. Par contre, cela nécessite de forcer le rendu de votre jeu en Canvas :
// On force le rendu en Canvas
var game = new Phaser.Game(640, 960, Phaser.CANVAS, 'flappyBird');
...
gameState.main.prototype = {
...
render: function() {
// Affichage de la zone de collision de l'oiseau
this.game.debug.renderPhysicsBody(this.bird.body);
}
};
main.js - afficher la zone de collision d'un sprite
Le problème est maintenant visible ! La zone de collision de l’oiseau est en fait une zone rectangulaire qui ne suit pas la véritable forme de ce dernier :
Il faut donc modifier le « polygone » qui définit cette zone. C’est le rôle de la fonction setPolygon()
de la classe Phaser.Physics.Arcade.Body :
gameState.main.prototype = {
create: function() {
...
// On change la zone de collision (le polygone) de l'oiseau
this.bird.body.setPolygon( /* x = */ 39,/* y = */ 129,
127,42,
188,0,
365,0,
425,105,
436,176,
463,182,
495,219,
430,315,
285,345,
152,341,
6,228 );
// Rotation du polygone de l'oiseau
this.birdRotatePolygon = 0;
...
},
start: function() {
...
// Rotation
this.bird.rotation = -Math.PI / 8;
// Rotation du polygone de l'oiseau
this.bird.body.translate(-this.bird.width/2, -this.bird.height/2);
this.bird.body.polygon.rotate(-Math.PI / 8);
this.birdRotatePolygon = -Math.PI / 8;
this.bird.body.translate(this.bird.width/2, this.bird.height/2);
},
...
update: function() {
...
// Rotation du polygone de l'oiseau
this.bird.body.translate(-this.bird.width/2, -this.bird.height/2);
this.bird.body.polygon.rotate(this.bird.rotation - this.birdRotatePolygon);
this.birdRotatePolygon += this.bird.rotation - this.birdRotatePolygon;
this.bird.body.translate(this.bird.width/2, this.bird.height/2);
...
},
...
};
main.js - redéfinition du polygone de l'oiseau
Voilà ce qu’on doit obtenir :
Comme vous pouvez le voir, en plus de redéfinir ce polygone, il faut aussi changer sa rotation en même temps que celle de l’oiseau.
Maintenant, le jeu détecte correctement les collisions entre l’oiseau et les obstacles !
Ajout du score
Dans Flappy Bird, à chaque fois que l’oiseau dépasse un tuyau, un nombre représentant le score et situé au milieu de l’écran s’incrémente.
Dans notre exemple, nous allons simplement créer une zone de texte qui contiendra le score du joueur. Dans la démonstration que je vous ai proposée de tester au début du tutoriel, le système est un peu plus complexe car le score est construit avec des images représentant des chiffres allant de 0 à 9. Si vous voulez donc plus de précision, je vous invite à étudier de plus près le code source que je vous ai mis à disposition sur Github.
gameState.main.prototype = {
create: function() {
...
// Score
this.score = 0;
this.scoreText = this.game.add.text(0, 100, "0", { font: "60px Arial", fill: "#ffffff" });
// On replace le score au centre de l'écran
this.scoreText.x = this.game.width / 2 - this.scoreText.width / 2;
},
...
update: function() {
...
// Si l'oiseau dépasse un tuyau
if(this.pipesToCheckForScore.length != 0 && this.pipesToCheckForScore[0].x + this.pipesToCheckForScore[0].width / 2 < this.bird.center.x) {
this.pipesToCheckForScore.splice(0, 1);
this.score++;
this.scoreText.content = this.score;
// On replace le score au centre de l'écran
this.scoreText.x = this.game.width / 2 - this.scoreText.width / 2;
}
},
...
};
main.js - ajout du score
Voici la démonstration finale de ce tutoriel : Démo V4
Si vous souhaitez aller plus loin, n’oubliez pas que nous mettons à votre disposition la démonstration et le téléchargement d’une version bien plus complète de ce jeu (sons, images supplémentaires…).