Bienvenue sur le blog de l'agence Soluka !

Retrouvez des articles dédiés aux nouveautés dans le développement web, rédigés par des passionnés, ainsi que les dernières actualités de l'agence.

Recherche

Articles populaires

Jeux Phaser

17/03/14

Phaser : créer un Flappy Bird en HTML5 Canvas

par Kevin le 17 mars 2014
Image Phaser : créer un Flappy Bird en HTML5 Canvas

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 :

Features de Phaser

  • 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

bird actions

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 :

tuyaux

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.

Construction d'un tuyau avec des morceaux

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.

collision entre l'oiseau et le tuyau

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 :

zone de collision carree de l'oiseau

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 :

zone de collision qui suit la forme du sprite

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…).

  • Bonjour, merci pour ce tuto, j’ai juste un petit souci au niveau du son sur ma tablette sm tab 2. Que ce soit avec votre jeu http://flappybird.soluka.fr/ ou le mien. Je n’est que le son du flap et par intermittence.
    Bravo encore pour ce tutoriel 😉

    • Bonjour gwenm !
      Je vous avoue ne pas avoir de réponse à vous donner.. Cela est peut-être due à la version de Phaser qui est un peu vieille sur ce tutoriel (1.6). En effet, Phaser est actuellement à sa version 2.2.
      Par contre, la migration entre la version 1.6 et la 2.2 n’est pas si évidente que ça à mettre en place..

      • Pas de problème 😉 j’aurais éventuellement une idée de tuto à vous proposer, dès fois que..c’est « comment ajouter de la publicité dans vos jeux » comme vous avez fait dans Flappy Bird. Elle n’est pas intrusif et je trouve ca bien 😉 Merci en tous cas pour votre réactivité. C’est très pro et ca manque sur pas mal de site 😉

      • Merci du compliment =)
        C’est effectivement une bonne idée, je l’intégrerai peut-être dans mon prochain tutoriel Phaser =D

  • guer

    a 24 le jeux c’est bloquer sur la démo

    • Bonjour guer,
      Comment ça c’est bloqué à 24 ?

  • test

  • Cool ce tuto… Si vous êtes intéressé par l’utilisation de Phaser 2.0, j’ai commencé à adapté une série de tuto fait par Jeremy Dowell : http://blog.robomatix.net/phaser-2-tutoriel-flappy-bird-part-1

    Bien à vous.

    • ChDUP

      C’est sympa Robomatix ce tuto.
      Malheureusement le systeme de commentaire ne semble pas fonctionner sur votre site.

  • Michel

    Excellent tuto merci beaucoup, par contre j’ai une question le rendu sur mobile ça donne quoi ? Une fois le jeu terminé c’est possible de « transformer » ça proprement en application ?

    • Kevin

      Alors, pour tout vous avouer, je n’ai pas beaucoup travaillé cet aspect de Phaser. Par contre, je me suis quand même renseigné. Il existe un outil du type PhoneGap qui permet de transformer une application HTML5 Canvas : CocoonJS -> https://www.ludei.com/cocoonjs/.
      Cet outil est beaucoup plus performant que PhoneGap en ce qui concerne les jeux, c’est vraiment le jour et la nuit.
      Pour faire simple, il suffit de se créer un compte sur le site, de créer son projet en fournissant toutes les sources. Vous compilez le projet pour n’importe quelle plateforme (IOS, Android…) et CocoonJS vous envoie le projet compilé par mail. Il n’y a plus qu’à le déployer sur le mobile !
      J’espère vous avoir aidé, n’hésitez pas si vous avez d’autres questions ! =)

    • Anthony Gée

      Comme dit par Kevin, il y a cocoonjs
      Sinon, il y a aussi http://www.mosync.com/

      C’est un très bon SDK qui permet de voir en temps réel sur les devices quand on développe (tout les devices synchronisé sont rafraichit en même temps)
      Et dans ce SDK il y a aussi de quoi déployer (Windows Phone, Android, iOS, BlackBerry (pas Bada…)) sur téléphone et tablette.

      • Merci du conseil, je testerai pour voir ce que ça donne =) ! (et peut-être même un petit tuto en prime)

  • Ozh

    Excellent tuto, merci. Cela fait quelques temps que l’idée d’un remake en JS d’un jeu que j’aime bien me trotte dans la tête, ceci va peut-être me faire passer à l’étape suivante 🙂

    • Kevin

      De rien, ça me fait plaisir de vous avoir aidé ! 😀