Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice pratique : le PACMAN

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par Monsieur Spi, le 08 juillet 2013

Bonjour,

Aujourd'hui on attaque le PACMAN avec des classes. Il existe déjà un modèle de base monté par Gnicos (voir : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/jeux_a_base_de_tuiles ), son approche est différente et repose plus sur la construction d'un jeu à base de tuiles en POO et la structuration du code et des classes, allez y faire un tour c'est très instructif et complémentaire. Dans cet exercice je vais vous proposer une autre méthode un peu plus brute, mais essentiellement concentrée sur le comportement des fantôme et leur Intelligence Artificielle.

Tout d'abord le résultat :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Les sources sont disponibles en fin d'exercice

Les pré-requis

Etude préliminaire

Tout d'abord le PACMAN c'est quoi ? (merci Wikipedia)

Pac-Man, personnage emblématique de l’histoire du jeu vidéo, est un personnage en forme de rond jaune doté d’une bouche. Il doit manger des pac-gommes et des bonus (sous forme de fruits, et d’une clé à 5 000 points) dans un labyrinthe hanté par quatre fantômes. Quatre pac-gommes spéciales (super pac-gommes) rendent les fantômes vulnérables pendant une courte période au cours de laquelle Pac-Man peut les manger. Les fantômes deviennent alors bleus et affichent une expression de peur signalée par des petits yeux et une bouche en ligne cassée. Le jeu original comprend 255 labyrinthes différents (le jeu était considéré comme allant à l’infini, mais le 256e niveau est injouable à cause d’un bug qui noie la moitié droite du niveau sous un gros amas de symboles, ce bogue vient du fait que le nombre de niveaux était codé sur un seul octet).

Bon ça ne nous en apprend pas beaucoup plus sur la technique, heureusement certains se sont “amusé” à nous dépiauter le jeu en profondeur, je vous recommande donc d'aller faire un tour sur ces liens :

http://donhodges.com/pacman_pinky_explanation.htm
http://gameinternals.com/post/2072558330/understanding-pac-man-ghost-behavior
http://everything2.com/title/Pac-Man+Ghost+Personalities
http://www.webpacman.com/ghosts.html

Dans la suite je ne vais pas vous proposer un jeu complet mais une base de départ jouable. En effet la partie la plus conséquente concerne le comportement des fantômes, assez complexe et mêlant tout un tas de techniques, qui sont expliquées ici : http://gameinternals.com/post/2072558330/understanding-pac-man-ghost-behavior

A vous de prendre le jeu en main et de le faire évoluer pour recréer un comportement proche de l'original.

Préparation

On va commencer par préparer le travail, ouvrez un nouveau projet Flash et créez :

Un MovieClip “Tuiles” comportant une tuile différente pour chaque frame, dans cet exemple j'en utilise 4, une pour les murs, une pour les sols vierges, une pour les sols avec pastille et une pour les sols avec grosse pastille.

Un MovieClip “Pacman” comportant sur sa première frame un autre clip que vous nommez “anim” et qui contient l'animation du Pacman.

Un MovieClip “Fantome” comportant sur sa première frame un autre clip que vous nommez “anim” et qui contient les différentes animations des fantômes.

Tous les clips sont exportés pour AS et les clips “Pacman” et “Fantome” ont pour classe de base “Pacman” et “Fantome”.

Pour la préparation c'est tout, passons au code.

Programme principal

Sur la première frame de votre projet tapez le code suivant. Pour ceux qui n'utilisent pas du tout l'IDE faites une classe de document “Main” avec le même code converti en classe.

var map:Array = [
	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
	1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 1,
	1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 3, 1,
	1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 3, 1,
	1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
	1, 3, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 3, 1,
	1, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 1, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 1,
	1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1,
	1, 1, 1, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 1, 1, 1, 1,
	1, 1, 1, 1, 1, 3, 1, 0, 1, 1,-1,-1,-1, 1, 1, 0, 1, 3, 1, 1, 1, 1, 1,
	2, 0, 0, 0, 0, 3, 0, 0, 1,-1,-1,-1,-1,-1, 1, 0, 0, 3, 0, 0, 0, 0, 2,
	2, 0, 0, 0, 0, 3, 0, 0, 1,-1,-1,-1,-1,-1, 1, 0, 0, 3, 0, 0, 0, 0, 2,
	1, 1, 1, 1, 1, 3, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 3, 1, 1, 1, 1, 1,
	1, 1, 1, 1, 1, 3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 1, 1, 1, 1, 1,
	1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1,
	1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
	1, 3, 1, 1, 1, 3, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 3, 1, 1, 1, 3, 1,
	1, 3, 3, 3, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 3, 3, 3, 1,
	1, 1, 1, 3, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 3, 1, 1, 1,
	1, 3, 3, 3, 3, 3, 1, 3, 3, 3, 3, 1, 3, 3, 3, 3, 1, 3, 3, 3, 3, 3, 1,
	1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1,
	1, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 1,
	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
];
 
 
const C:int = 23;
const T:int = 20;
var i:int;
var stockTuiles:Array;
var stockFantomes:Array;
var pacman:Pacman;
var t:Tuiles;
 
init();
 
function init():void{
 
	stockTuiles = [];
	stockFantomes = [];	
 
	for(i=0;i<C*C;i++){
		t = new Tuiles();
		t.x = i%C*T;
		t.y = int(i/C)*T;
		t.gotoAndStop(map[i]+1);
		stockTuiles.push(t);
		addChild(t)
	}
 
	pacman = new Pacman(T,C,map,stockTuiles);
	pacman.addEventListener(EventPerso.BONUS, changeEtat);
	addChild(pacman);
 
	for(i=0; i<3;i++){
		var f:Fantome = new Fantome(T,C,map,pacman);
		addChild(f);
		stockFantomes.push(f);
	}
 
	addEventListener(Event.ENTER_FRAME, toucher );
}
 
function changeEtat(e:EventPerso):void{
	for each(var f in stockFantomes) f.changeEtat();
}
 
function toucher(e:Event):void{
	for each(var f:Fantome in stockFantomes){	
		if(Collisions.hitObjet(f,pacman)){
			if(f.etat == 0) {
				pacman.x = 11*T;
				pacman.y = 17*T;
				for each(var fan:Fantome in stockFantomes){
					fan.replacer();
				}
			}
			if(f.etat == 4) {
				f.etat = 9;
				f.retour = true;
			}
		}
	}
}

Ceci est le coeur du jeu, son programme principal, on commence par créer une map (à vous de créer une petite classe pour faire différents level et les charger). Si vous avez fait les exercices précédent vous ne devez pas être dépaysés, on crée quelques variables utiles et trois tableaux de stockage, puis on lance la fonction “init”.

“init” permet d'initaliser la partie, on vide les tableaux, on construit la grille, et on arrive au premier point qui demande des explications :

pacman = new Pacman(T,C,map,stockTuiles);
pacman.addEventListener(EventPerso.BONUS, changeEtat);
addChild(pacman);

Ok, on crée un pacman à partir de la classe “Pacman” que nous n'avons pas encore écrit, elle contiendra tout ce qui est utile au joueur. On lui passe quatre paramètres, la taille des tuiles, le nombre de colonnes de la grille, la map du level et le stock des tuiles créées. On va également lui ajouter un écouteur personnalisé, il sert au pacman à signaler lorsqu'il mange une grosse pastille et que les fantômes doivent changer d'état, nous reviendrons là dessus.

for(i=0; i<3;i++){
	var f:Fantome = new Fantome(T,C,map,pacman);
	addChild(f);
	stockFantomes.push(f);
}

Ici on crée les trois fantômes, là aussi à partir d'une classe que nous n'avons pas encore écrit et qui contiendra tout ce qui est utile au fantôme pour intervenir dans le jeu, on lui passe à lui aussi quatre paramètres qui sont la taille des tuiles, le nombre de colonnes de la grille, la carte du level et le pacman.

Notez qu'ici on pourrait créer une classe générique “joueur” pour à la fois le pacman et les fantômes, c'est d'ailleurs ce que fait Gnicos dans sa version, ce serait en effet plus cohérent, mais pour cet exemple on va éviter de multiplier les classes.

Tous les fantômes sont stockés dans un tableau, afin de pouvoir y accéder facilement par la suite.

addEventListener(Event.ENTER_FRAME, toucher );

Un petit écouteur général qui permet de savoir lorsqu'un fantome touche le pacman, nous allons y revenir.

function changeEtat(e:EventPerso):void{
	for each(var f in stockFantomes) f.changeEtat();
}

Lorsque le pacman signale qu'il à mangé une grosse pastille, tous les fantômes changent d'état et passent de agressifs à effrayés, on parcours donc le tableau de stockage des fantômes et on change l'état de chacun d'eux.

function toucher(e:Event):void{
	for each(var f:Fantome in stockFantomes){	
		if(Collisions.hitObjet(f,pacman)){
			if(f.etat == 0) {
				pacman.x = 11*T;
				pacman.y = 17*T;
				for each(var fan:Fantome in stockFantomes){
					fan.replacer();
				}
			}
			if(f.etat == 4) {
				f.etat = 9;
				f.retour = true;
			}
		}
	}
}

Pour chaque fantôme on teste la collision avec le pacman, selon son état soit le fantôme tue pacman, dans ce cas tout le monde revient à son point de départ, soit le fantôme est mangé par pacman, il change alors d'état pour passer en “désincarné” et retourne à son point de départ.

C'est tout pour le fonctionnement du code principal, on va maintenant s'intéresser aux différentes classes utilisées.

Pacman

Créez un nouveau fichier AS et tapez :

package {
 
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.events.EventDispatcher;
 
	public class Pacman extends MovieClip { 
 
		private var T:int;
		private var C:int;
		private var i:int;
		private var g:int;
		private var d:int;
		private var h:int;
		private var b:int;
		private var v:int;
		private var cX:Number;
		private var cY:Number;
		private var map:Array;
		private var oldX:Number;
		private var oldY:Number;
		private var stock:Array;
		private var bouge:Array;
 
		public function Pacman(taille:int, colonne:int, carte:Array, tuiles:Array) { 
			T = taille;
			C = colonne;
			v = 4;
			x = 11*T;
			y = 17*T;
			cX = width*.5;
			cY = height*.5;
			map = carte;
			stock = tuiles;
			this.addEventListener(Event.ADDED_TO_STAGE, init);
		};
 
		private function init(e:Event):void {
			this.removeEventListener(Event.ADDED_TO_STAGE, init);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, appuyer);
			stage.addEventListener(KeyboardEvent.KEY_UP, relacher);
			stage.addEventListener(Event.ENTER_FRAME, update);
		}
 
		private function appuyer (e:KeyboardEvent):void{
			if (e.keyCode == 37) g = 1;
			if (e.keyCode == 39) d = 1;
			if (e.keyCode == 38) h = 1;
			if (e.keyCode == 40) b = 1;
		}
 
		private function relacher (e:KeyboardEvent):void{
			if (e.keyCode == 37) g = 0;
			if (e.keyCode == 39) d = 0;
			if (e.keyCode == 38) h = 0;
			if (e.keyCode == 40) b = 0;
		}
 
		private function update(e:Event):void {
 
			bouge = Collisions.Decor(d-g,b-h,x,y,map,T,C,v);
			oldX = x;
			oldY = y;
 
			x = bouge[0];
			y = bouge[1];
 
			i = int((x+cX)/T)+int((y+cY)/T)*C; // où se trouve le centre
 
			// mange pastille ou bonus
			if(map[i]==3 || map[i]==4){
				if(map[i]==4) dispatchEvent( new EventPerso ( EventPerso.BONUS ) );	
				map[i] = 0;
				stock[i].gotoAndStop(1);
			}
 
			// orientation
			if(oldY != y) {
				if (h) anim.rotation = 270;
				if (b) anim.rotation = 90;
			}
			if(oldX != x){
				if (d) anim.rotation = 0;
				if (g) anim.rotation = 180;
			}
 
			Utiles.limite(this);
		}
	}
}

On a ici toute la gestion du Pacman (joueur), je vous fait grâce de la description des variables, vous verrez à quoi elles correspondent au fur et à mesure.

public function Pacman(taille:int, colonne:int, carte:Array, tuiles:Array) { 
	T = taille;
	C = colonne;
	v = 4;
	x = 11*T;
	y = 17*T;
	cX = width*.5;
	cY = height*.5;
	map = carte;
	stock = tuiles;
	this.addEventListener(Event.ADDED_TO_STAGE, init);
};

Dans le constructeur on initialise les variables privées, notez que cX et cY servent à trouver le centre du Pacman, on va s'en servir plus tard, et on ajoute un écouteur pour savoir quand le pacman est ajouté sur la scène.

private function init(e:Event):void {
	this.removeEventListener(Event.ADDED_TO_STAGE, init);
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuyer);
	stage.addEventListener(KeyboardEvent.KEY_UP, relacher);
	stage.addEventListener(Event.ENTER_FRAME, update);
}

Lorsque le pacman est ajouté sur la scène, on ajoute les écouteurs du clavier, pour le déplacer, et un écouteur “update” qui le met à jour.

private function appuyer (e:KeyboardEvent):void{
	if (e.keyCode == 37) g = 1;
	if (e.keyCode == 39) d = 1;
	if (e.keyCode == 38) h = 1;
	if (e.keyCode == 40) b = 1;
}
 
private function relacher (e:KeyboardEvent):void{
	if (e.keyCode == 37) g = 0;
	if (e.keyCode == 39) d = 0;
	if (e.keyCode == 38) h = 0;
	if (e.keyCode == 40) b = 0;
}

Un grand classique, la gestion du clavier pour chaque direction.

private function update(e:Event):void {
 
	bouge = Collisions.Decor(d-g,b-h,x,y,map,T,C,v);
	oldX = x;
	oldY = y;
 
	x = bouge[0];
	y = bouge[1];
 
	i = int((x+cX)/T)+int((y+cY)/T)*C; // où se trouve le centre
 
	// mange pastille ou bonus
	if(map[i]==3 || map[i]==4){
		if(map[i]==4) dispatchEvent( new EventPerso ( EventPerso.BONUS ) );	
		map[i] = 0;
		stock[i].gotoAndStop(1);
	}
 
	// orientation
	if(oldY != y) {
		if (h) anim.rotation = 270;
		if (b) anim.rotation = 90;
	}
	if(oldX != x){
		if (d) anim.rotation = 0;
		if (g) anim.rotation = 180;
	}
 
	Utiles.limite(this);
}

Et on attaque la fonction principale du joueur, on commence par vérifier la collision avec le décor en fonction des déplacements insufflés par les touches du clavier, pour en savoir plus sur la détection de collision voyez la classe “Collisions” un peu plus loin dans l'exercice, sachez simplement pour l'instant qu'elle renvoie un tableau contenant la position du pacman après le déplacement et le test de collision.

On enregistre ensuite l'ancienne position du pacman, puis on le place à sa nouvelle position, puis on repère où se trouve son centre, rappelez-vous que les objets ont leur point de repère en haut à gauche de l'objet, il faut donc faire un petit calcul pour obtenir le centre réel de l'objet en question.

On vérifie ensuite si la tuile où se trouve le centre du pacman correspond à une pastille (petite ou grosse), si c'est le cas la pastille doit disparaître, on la remplace donc dans le stock des tuiles par une tuile vierge et on supprime sa référence dans la map du level. Au préalable on a vérifié si la pastille mangée était une grosse, auquel cas on lance un événement perso qui signale au programme principal qu'une grosse pastille vient d'être mangée, au programme principal de se débrouiller à répercuter ça sur les fantômes pour changer leur état.

Enfin, on oriente le pacman dans le bon sens si il a changé de direction.

Puisqu'on utilise un événement perso uniquement ici, on va regarder tout de suite à quoi correspond la classe de cet événement.

EventPerso

Créez un nouveau fichier AS nommé “EventPerso” et écrivez :

package {
 
	import flash.events.Event;
 
	public class EventPerso extends Event {
		public static const BONUS:String = "bonus";		
		public function EventPerso ( T:String, B:Boolean=false, C:Boolean=false )	{
			super ( T, B, C );
		}
	}
}

Rien de bien compliqué, on crée un simple événement qui réagit au mot clé “BONUS” calé sur le modèle d'un Event de base. Ainsi pour déclencher l'événement le pacman le dispatche, et dans le programme principal on s'en sert comme d'un événement classique.

Notez que si vous avez plusieurs événements à diffuser sur le même modèle il vous suffit d'ajouter des mots clés sur le modèle de BONUS, ainsi vous disposerez de tout un tas d'événements personnalisés utilisables à volonté pour signaler ce que vous voulez, il suffit de dispatcher l'événement au bon moment depuis n'importe quelle classe.

Utiles

Une autre classe qui est plutôt générique, créez un nouveau fichier AS nommé “Utiles” et écrivez :

package {
 
	public class Utiles { 
 
		public function Utiles() { }
 
		static public function limite(ob:Object) {
 
			var W:int = 460;
			var H:int = 460;
 
			if (ob.x < 0) ob.x = W;
			if (ob.x > W) ob.x = 0;
			if (ob.y < 0) ob.y = H;
			if (ob.x > H) ob.y = 0;
 
		}
	}
}

Vous pouvez coller là dedans tout un tas de méthodes diverses et utiles, ici par exemple on à la méthode “limite” qui permet de limiter n'importe quel objet à la taille du level, si il sort d'un côté il est replacé de l'autre côté.

Allez on attaque les fantômes.

Fantome

Créez un nouveau fichier AS nommé “Fantome” et écrivez :

package {
 
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.utils.Timer;
	import flash.events.TimerEvent;
	import flash.display.Sprite;
 
	public class Fantome extends MovieClip { 
 
		public static var nombreFantome:int = 0;
		public var etat:int = 0;
		public var retour:Boolean;
 
		private var D:int;
		private var V:int = 4;
		private var M:Array = [];
		private var C:int;
		private var T:int;
		private var P:Pacman;
		private var path:Pathfinder;
		private var chemin:Array;
		private var cible:int;
		private var timer:Timer;
		private var traque:Boolean;
		private var place:int;
		private var replace:Boolean;
 
		public function Fantome(taille:int, colonnes:int, carte:Array, pacman:Pacman) { 
			T = taille;
			x = 10*T+nombreFantome*T
			y = 10*T;
			M = carte;
			C = colonnes;
			P = pacman;
			place = nombreFantome;
			retour = false;
			traque = false;
			replace = false;
			chemin = [];
			path = new Pathfinder(M, C, 1, 2);
			nombreFantome++;	
			this.addEventListener(Event.ADDED_TO_STAGE, init);
		};
 
		private function init(e:Event):void {
			this.removeEventListener(Event.ADDED_TO_STAGE, init);
			nouvelleCible();
			stage.addEventListener(Event.ENTER_FRAME, suivreChemin);
		}
 
		private function suivreChemin(e:Event):void {
 
			if (replace) return;
 
			if (etat == 9) {
				if (retour) { 
					chemin = [];
					nouvelleCible();
					return;
				}
				if (M[int(x/T)+int(y/T)*C]==-1) etat = 0;
			}
 
			var X:int = cible%C*T;
			var Y:int = int(cible/C)*T;
 
			if (x<X) x+=V, anim.gotoAndStop(2+etat);
			else if (x>X) x-=V, anim.gotoAndStop(1+etat);
			else if (y<Y) y+=V, anim.gotoAndStop(3+etat);
			else if (y>Y) y-=V, anim.gotoAndStop(4+etat);
			else nouvelleCible();	
		}
 
		private function nouvelleCible():void {
 
			replace = false;
			if(chemin.length>0) {
				cible = chemin.pop().id;
			} else {
				if (etat == 0) traquer();
				// sinon 
				var cib:int = 0;
				var dep:int = int(x/T)+int(y/T)*C;
 
				if(retour){
					cib = 10+11*C;
					retour = false;
				} else if(traque){
					cib = int(P.x/T)+int(P.y/T)*C;
					traque = false;
				} else {
					while (M[cib] == 1 || M[cib] == 2 || M[cib] == -1) {
						cib = Math.random() * C*C
					}
				}
				path.chercheChemins(cib, dep);
				chemin = path.chemin;
				cible = chemin.pop().id;
			}
		}
 
		public function changeEtat():void {
			etat = 4;
			timer = new Timer(10000,1);
			timer.addEventListener(TimerEvent.TIMER_COMPLETE, stopEtat);
			timer.start();
		}
 
		private function stopEtat(e:TimerEvent):void{
			if(etat!=9) etat = 0;
		}
 
		private function traquer():void {
			var dx:Number = P.x-x;					
			var dy:Number = P.y-y;					
			var distance:Number = dx*dx + dy*dy;
			if (distance < 20000) traque = true;
		}
 
		public function replacer():void {
			chemin = [];
			cible = 10+11*C;
			replace = true;
			x = (11+place)*T;
			y = 10*T;
			etat = 0;
			nouvelleCible();
		}
	}
}

Bon, j'avoue c'est assez indigeste vu comme ça, mais on va détailler un peu. Vous avez déjà compris qu'on avait là tout ce qui va être utile à chaque fantôme pour interagir dans le jeu, une fois encore je vous fait grâce de la description détaillée des variables, notez simplement celle-ci :

public static var nombreFantome:int = 0;

Cette variable est publique (donc accessible en dehors de la classe) et static, cela veut dire qu'elle est unique pour la classe, peu importe le nombre d'occurences que je vais tirer de cette classe cette variable restera la même, autrement dit si à chaque fois que je crée un fantôme j'incrémente cette variable, pour chaque fantôme elle portera un numéro différent.

Voyons le constructeur de la classe :

public function Fantome(taille:int, colonnes:int, carte:Array, pacman:Pacman) { 
	T = taille;
	x = 10*T+nombreFantome*T
	y = 10*T;
	M = carte;
	C = colonnes;
	P = pacman;
	place = nombreFantome;
	retour = false;
	traque = false;
	replace = false;
	chemin = [];
	path = new Pathfinder(M, C, 1, 2);
	nombreFantome++;	
	this.addEventListener(Event.ADDED_TO_STAGE, init);
};

On initialise les variables, on place le fantôme à sa position de départ, et surtout on crée un objet “path” qui n'est autre que le pathfinder qu'on étudiera un peu plus tard, son objectif sera de déterminer le chemin que doit suivre le fantôme dans la grille.

private function init(e:Event):void {
	this.removeEventListener(Event.ADDED_TO_STAGE, init);
	nouvelleCible();
	stage.addEventListener(Event.ENTER_FRAME, suivreChemin);
}

Lorsque le fantôme est ajouté sur la scène, on tire une nouvelle cible et on lui demande de suivre le chemin vers cette cible. Voyons tout d'abord comment on choisi une cible.

private function nouvelleCible():void {
 
	replace = false;
	if(chemin.length>0) {
		cible = chemin.pop().id;
	} else {
		if (etat == 0) traquer();
		var cib:int = 0;
		var dep:int = int(x/T)+int(y/T)*C;
 
		if(retour){
			cib = 10+11*C;
			retour = false;
		} else if(traque){
			cib = int(P.x/T)+int(P.y/T)*C;
			traque = false;
		} else {
			while (M[cib] == 1 || M[cib] == 2 || M[cib] == -1) {
				cib = Math.random() * C*C
			}
		}
		path.chercheChemins(cib, dep);
		chemin = path.chemin;
		cible = chemin.pop().id;
	}
}

Une cible est une tuile de la carte qui n'est pas un mur. Si le fantôme est en train de suivre un chemin vers une case de la carte, la cible est la tuile immédiatement après sur le chemin (qui est un tableau renvoyé par le pathfinder dont le but est justement de trouver le chemin le plus court entre deux cases sur la carte).

Si le fantôme n'a pas de chemin à suivre il va devoir en choisir un, c'est là que sont comportement entre en jeu, ici par exemple si il est dans son état normal il commence par regarder si il doit “traquer” le pacman (nous verrons comment après). Puis il doit choisir deux cases, celle de départ “dep”, normalement il s'agit de la case où il se trouve, et celle d'arrivée “cib” qui sera la case finale à atteindre. Si le fantôme doit retourner à son point de départ car il a été mangé, la cible est simple à trouver, il s'agit d'une case dans la partie centrale de la carte. Sinon, si il doit traquer le pacman, la cible correspond à la case où se trouve le pacman actuellement. Et enfin, si aucune des conditions n'est remplie, il choisi une cible aléatoirement dans la map du moment que la case choisie n'est ni un mur, ni un bord ouvert, ni la partie centrale (ce qui l'empêche de retourner au centre de la carte n'importe quand).

Une fois la cible choisie il faut encore déterminer le chemin le plus court pour se rendre à cette case, c'est là que le pathfinder entre en jeu, son rôle est de choisir le chemin le plus court entre deux cases de la carte. Une fois le chemin choisi on l'enregistre pour que le fantôme puisse commencer à le suivre et on détermine quelle est la prochaine case à atteindre (nouvelle cible) sur le chemin.

Je vous propose de continuer à regarder ce que fait le fantôme avant d'aller étudier la classe “Pathfinder” et la manière dont on trouve un chemin dans un labyrinthe.

public function changeEtat():void {
	etat = 4;
	timer = new Timer(10000,1);
	timer.addEventListener(TimerEvent.TIMER_COMPLETE, stopEtat);
	timer.start();
}

Cette fonction publique est utilisée par le programme principal pour modifier l'état d'un fantôme lorsque cela s'avère nécessaire. Elle sert à passer de l'état “normal” à l'état “effrayé”, à ce moment là un timer se lance pour permettre de revenir à l'état normal au bout d'un certain laps de temps.

private function stopEtat(e:TimerEvent):void{
	if(etat!=9) etat = 0;
}

Le chrono est arrivé au bout, si le fantôme n'a pas été mangé (état 9) il peut revenir à son état normal.

private function traquer():void {
	var dx:Number = P.x-x;					
	var dy:Number = P.y-y;					
	var distance:Number = dx*dx + dy*dy;
	if (distance < 20000) traque = true;
}

Il existe différentes méthodes pour déterminer le moment où un fantôme se met à traquer le pacman, donc à le suivre de près. J'ai choisi de faire ça en fonction de la distance, lorsque le fantôme est assez près du pacman il le suit, on peut considérer qu'il est assez près pour le voir ou l'entendre à travers les murs par exemple, mais à vous de peaufiner à ce niveau.

public function replacer():void {
	chemin = [];
	cible = 10+11*C;
	replace = true;
	x = (11+place)*T;
	y = 10*T;
	etat = 0;
	nouvelleCible();
}

Dernier petit bout de code en ce qui concerne les fantômes, il s'agit de le replacer au centre de la carte lorsqu'il s'est fait manger, cette méthode est publique car c'est le programme principal qui décide quand le fantôme doit être replacé. Pensez à bien vider le chemin en cours, la dernière cible connue et à modifier son état avant de tirer une nouvelle cible (une fois le fantôme replacé).

Passons à présent à la plus grosse partie, le pathfinding.

Pathfinder

Créez un nouveau fichier AS nommé “Pathfinder” et écrivez :

package {
 
  public class Pathfinder {
 
	private var i:int;
	private var j:int;
	private var C:int;
	private var M:int;
	private var P:int;
	private var d:int;
	private var b:Object
	private var cible:int;
	private var depart:int;
	private var longueur:int;
	private var map:Array;
	public var liste:Array;
	public var chemin:Array;
	private var dir:Array;
 
	public function Pathfinder(carte:Array, colonnes:int, mur:int, portes:int) {
		map = carte;
		C = colonnes;
		M = mur;
		P = portes;
		dir = [1,-1,C,-C]
	}
 
	public function chercheChemins(c:int, d:int):void {	
		liste = [];
		chemin = [];
		cible = c;
		depart = d;
		liste.push({id:depart,num:0});
		chemin.push({id:cible,num:-1});
		trouveVoisins(0, 1);
		trouveChemin(cible, -1);
	}
 
	private function trouveVoisins(debut:int, n:int):void{
		longueur = liste.length;
		for (i=debut; i<longueur; i++){
			for each(d in dir){
				if (voisin(liste[i].id+d, n)) return; // haut
			}
		}
		trouveVoisins(longueur, n+1);
	}
 
	private function voisin(index:int, n:int):Boolean{
		if (index == cible) return true;
		if (map[index] != M && map[index] != P) {
			for (j = liste.length-1; j>=0; j--){
				if (index == liste[j].id){
					if (n >= liste[j].num) return false;
					else liste.splice(j, 1);
				}
			}
			liste.push({id:index,num:n})
		}
		return false;
	}
 
	private function trouveChemin(index:int, n:int):void{
		for each(b in liste) {
			for each(d in dir){
				if (b.id == index+d && addChemin(b, n)) return;
			}
		}
	}
 
	private function addChemin(b:Object,n:int):Boolean{
		if(n == -1 || n-1 == b.num) {
			chemin.push(b);
			trouveChemin(b.id, b.num);
			return true;
		}
		return false;
	}
  }
}

Je ne souhaite pas vous faire le détail du processus, tout simplement parce qu'une fiche détaillée va suivre et qu'il existe déjà un tutoriel complet écrit par Thoutmosis ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/pathfinder_algorithme_astar

Voyons cependant la mécanique générale, à partir d'une case de départ on va propager à l'aide d'une fonction récursive autant de chemin que possible dans toutes les directions. On va compter le nombre de case nécessaire pour chaque chemin et chaque embranchement, jusqu'à ce qu'on tombe sur la case ciblée. Il peut y avoir plusieurs chemins possible pour atteindre cette case, aussi on va limiter la propagation au nombre minimum de case nécessaire pour atteindre la cible, tout chemin possédant plus de cases sera automatiquement stoppé.

On obtient donc un ensemble de cases numérotées, donc un certain nombre est le chemin le plus court entre les deux cases. Pour enregistrer ce chemin c'est très simple, à l'aide d'une seconde fonction récursive, on par de la case cible, on choisi une case adjacente comportant un numéro, et on remonte de case en case jusqu'à la case de départ en prenant soin de vérifier à chaque case que son numéro est bien immédiatement inférieur à la case précédente, ainsi on à notre chemin final.

Cette classe peut être utilisée dans n'importe quel jeu où vous avez besoin de trouver un chemin, il suffit de lui passer la case de départ, la case d'arrivée et la map, elle vous renverra la liste de toutes les cases testées et le chemin final sous forme d'une tableau, à vous ensuite de l'utiliser dans vos jeux.

Collisions

Il nous manque une dernière classe, celle qui va gérer les collisions, créez un nouveau fichier AS nommé “Collisions” et écrivez :

package {
 
	public class Collisions { 
 
		public function Collisions(){ };
 
		public static function Decor(dX:Number,dY:Number,x:Number,y:Number,map:Array,T:int,C:int,v:int):Array{
 
			var col:int = 0;
			var lig:int = 0;
			var hit = false;
 
			x += dX*v;
			if (dX<0) {
				col = x/T;	
				for (lig=y/T; lig<(y+T-1)/T; lig++) {
					if (map[col+lig*C]==1) {
						x = col*T+T;
						hit = true;
					} 
				}
			} else if (dX>0) {																					
				col = (x+T-1)/T;
				for (lig=y/T; lig<(y+T-1)/T ; lig++) {
					if (map[col+lig*C]==1) {
						x = col*T-T;
						hit = true;
					}
				}
			}	
 
			y += dY*v;
			if (dY<0) { 				
				lig = y/T ;
				for (col = x/T; col<(x+T-1)/T; col++) {
					if (map[col+lig*C]==1) {
						y = lig*T+T;
						hit = true;
					}
				}
			} else if (dY>0) { 	
				lig = (y+T-1)/T;
				for (col = x/T; col<(x+T-1)/T; col++) {
					if (map[col+lig*C]==1) {
						y = lig*T-T;
						hit = true;
					} 
				}
			}
			return [x, y, hit];
		}
 
		// Collision AABB entre deux objets (joueur/joueur ou joueur/objet)
		public static function hitObjet(A:Object, B:Object):Boolean {
			if((B.x >= A.x + A.width) || (B.x + B.height <= A.x) || (B.y >= A.y + A.height) || (B.y + B.height <= A.y))  return false; 	// pas de collision
			return true; 															// collision
		}
	}
}

Dans cette classe on va tester les collisions avec une grille grâce à la première méthode, et avec deux objets libres avec la seconde méthode.

Plutôt que vous faire de longues explications sur le fonctionnement, je vous renvoie à la fiche pratique à propos des collisions que vous trouverez ici : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/fiche_collisions

Regardez les chapitres : AABB vs Grille - et - AABB vs AABB

Conclusion

Voilà, nous avons fini cette première petite base en POO pour faire un Pacman, à partir de là tout l'exercice consiste à développer le comportement de vos fantômes car c'est surtout sur eux de repose tout le fun du jeu, pensez à aller chercher sur la toile les différents algorithmes utilisés, c'est assez passionnant, mais malheureusement trop long pour tenir dans un exercice.

Les sources

pacman_mb.fla version CS6
pacman_mb_cs5.fla version CS5