Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice pratique : le ROGUE LIKE - partie 2

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

Bonjour,

Voici la seconde partie de l'exercice à propos des Rogue-Like, vous devez impérativement avoir fait la première partie avant de vous attaquer à celle-ci.

Partie 1 : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/exercice_-_roguelike_partie_1

Dans la première partie je vous ai parlé de méthodes pour générer des environnements aléatoires, cette fois nous allons nous attaquer au remplissage, aux améliorations, et à un premier petit moteur de test pour y diriger un personnage.

Ce qui vas nous intéresser plus particulièrement c'est comment remplir ces environnements de manière cohérente.

On pourrait balancer au hasard tout un tas d'objets sur le sol, c'est d'ailleurs ce que je vais faire pour bon nombre d'entre-eux, mais pour les clés ?

Si on veut s'assurer que notre joueur pourra terminer sa partie, on ne peut pas se permettre de faire confiance au hasard pour répartir les clés, il risquerait de placer une clé l'intérieur de la salle qu'elle est sensée ouvrir, ou pire ne pas donner de clé accessible au joueur, ce qui le bloquerai dès le départ, bref…

Ce que l'on doit obtenir au final :

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

Utilisez les trois boutons (L C D) pour générer un Labyrinthe, une Caverne ou un Donjon.
Utilisez les flèches du clavier pour déplacer le joueur.
Utilisez la barre espace pour interagir avec l'environnement.
Cliquez sur le panneau d'affichage pour le faire disparaitre.

Pour utiliser les portes, portes dérobées, magasins ou sortie, tenez vous sur une tuile voisine et utilisez la barre espace.

Légende :

  • tuile grise : sol
  • tuile noire : mur
  • tuile rouge : clé
  • tuile rose : porte
  • tuile blanche : sol
  • tuile bleue : porte dérobée
  • tuile orange : magasin
  • tuile jaune : sortie
  • tuile gris foncé : cul de sac
  • tuile noire avec cercle gris : téléporteur
  • petite tuile verte : pnj
  • petite tuile jaune : coffre
  • petite tuile rouge : ennemi
  • petite tuile bleue : nid de monstres
  • petite tuile multicolore : objet de quête
  • tuile verte transparente : joueur

Les pré-requis

Partie 1 : http://forums.mediabox.fr/wiki/tutoriaux/flashplatform/jeux/exercice_-_roguelike_partie_1

Les mêmes pré-requis sont demandés que pour la partie 1, cette partie étant de fait également indispensable pour suivre ce qui va être dit ci-après.

Structure du programme

Dans la partie précédente, nous avons vu comment traiter indépendamment chaque environnement, mais l'intérêt pour monter un jeu de ce type est de pouvoir mélanger ces environnements afin que le joueur puisse passer de l'un à l'autre facilement. Il va donc nous falloir réunir les générateurs en un seul tout cohérent, permettre la génération de l'un ou l'autre des environnements et ajouter un moteur. Nous allons également devoir remplir ces environnements avec des objets et des clés (pour le donjon) et nous assurer que la répartition soit cohérente.

Notre programme va se composer de 6 parties principales et 1 partie annexe.

  • initialisation, le point de départ du programme
  • générateur de labyrinthe
  • générateur de cavernes
  • générateur de donjons
  • répartition des objets
  • fonctions génériques
  • joueur et moteur

La source fournie décompose le programme sur plusieurs calques afin de vous permettre de vous y retrouver facilement, par la suite je vous recommande de faire des classes, ce sera plus simple ;-)

Nous allons suivre ce schéma et étudier chaque partie indépendamment, bien sur comme nous avons déjà abordé les générateurs on va pouvoir aller très vite et je ne vais pas ré-expliquer les méthodes, juste vous signaler les changements à ce niveau.

Initialisation

Cette partie regroupe tout ce qui est utile pour initialiser le jeu, voyons le code :

const C:uint = 48;
const L:uint = 48;
const T:uint = 10;
const G:uint = C*L;
 
var grille:Array;
var grilleTests:Array;
var grilleObjets:Array;
var grilleInfosObjets:Array;
var grilleCollisions:Array;
var teleporteurs:Array;
var inventaire:Array;
var listeCles:Array;
var cavernes:int;
var vitesse:int;
var entree:int;
var action:int;
var gauche:int;
var droite:int;
var haut:int;
var bas:int;
var i:int;
var j:int;
 
var testSortie:Boolean;
 
var rendu:Sprite;
var joueur:Joueur;
 
init();
 
function init():void{
	entree = 		49;
	rendu = 		new Sprite();
	rendu.scaleX = 		2;
	rendu.scaleY = 		2;
	addChild(rendu);
	addChild(hud);
	addChild(panneau);
	genere("donjon");
}
 
function genere(choix:String):void{
 
	while(rendu.numChildren>0) rendu.removeChildAt(0);
 
	grille = 		[];
	grilleTests = 		[];
	grilleObjets = 		[];
	grilleInfosObjets = 	[];
	grilleCollisions = 	[];
	listeCles = 		[];
 
	for (i=0;i<G;i++) {
		grille.push(1);
		grilleTests.push(1);
		grilleObjets.push(0);
		grilleInfosObjets.push(0);
	}
 
	if(choix=="labyrinthe") {
		creeLabyrinthe(entree);
		grilleCollisions = grilleCollisions.concat(grille);
		repartirObjets("labyrinthe");
		renderDonjon();
		ajouteEntreeSortie("labyrinthe");
	}
	if(choix=="caverne") creeCaverne();
	if(choix=="donjon"){
		creeLabyrinthe(entree);
		creeDonjon();
	}
}

Nous avons là toutes les déclarations des constantes, variables et objets qui vont être utiles un peu partout dans le jeu, notez que je ne regroupe ici que les définitions dites “globales”, celles qui doivent pouvoir être utilisées depuis n'importe quel point du programme. Du coup c'est un peu long mais je pense suffisamment explicite pour ne pas demander d'explications supplémentaires. Notez simplement que nous allons travailler avec 5 grilles :

var grille:Array;
var grilleTests:Array;
var grilleObjets:Array;
var grilleInfosObjets:Array;
var grilleCollisions:Array;

“grille” est la map principale, elle va regrouper les numéros de tuiles utiles pour les décors et la structure générale des environnements.

“grilleTests” est utilisée au sein des générateurs, elle sert à effectuer des calculs sans impacter directement sur la grille principale.

“grilleObjets” va recenser tous les objets de l'environnement en cours, cela va des portes aux coffres, en passant par tout ce que l'on peut considérer comme “item” dans le jeu, ceci va me permettre de gérer les objets indépendamment de la structure du jeu, un peu comme une couche d'informations supplémentaire.

“grilleInfosObjets” stocke les informations associées aux objets, techniquement c'est là que la POO serait bien utile, normalement les informations des “items” devraient se trouver dans la classe de l'objet lui même, comme je travaille en procédural je préfère utiliser une grille qui va me lister tout ça. Pour vous donner un exemple d'utilité de cette grille, une porte est considérée comme un objet, elle est posée par dessus une tuile de décor (grille) et doit donc être listée dans la grilleObjets, mais elle peut aussi être fermée ou ouverte et ne s'ouvre qu'avec une clé, il faut donc lui associer un numéro de clé (grilleInfosObjets). Il en va de même pour par exemple un coffre qui est un conteneur ayant sa propre représentation graphique, se superpose à une tuile de décor et contient un certain nombre d'objets, etc…

“grilleCollisions” sert quand à elle au moteur pour gérer les collisions, il me semble important de différencier clairement ce qui est “décor”, “objets” et “structure”.

Ces grilles sont identiques par leur taille, ce qui fait qu'il n'est pas besoin de parcourir toutes les grilles à chaque fois que je souhaite obtenir une information, il me suffit de regarder où se trouve le joueur (ou la tuile que je veux tester dans l'environnement), son index dans la grille principale correspond au même index dans toutes les grilles. Le défaut de cette méthode est d'utiliser beaucoup de données (5 grilles), son avantage est qu'il est très facile de distinguer et d'accéder aux données, de les sauvegarder et de les réutiliser.

function init():void{
	entree = 		49;
	rendu = 		new Sprite();
	rendu.scaleX = 		2;
	rendu.scaleY = 		2;
	addChild(rendu);
	addChild(hud);
	addChild(panneau);
	genere("donjon");
}

Bien, ici j'initialise les données principales, remarquez que l'entrée est à présent un simple index dans la grille et non un point ayant deux positions X et Y. Notez également que j'ai multiplié par deux l'échelle du rendu graphique, afin qu'on puisse voir ce qu'il se passe. J'ai également ajouté un “hud” (une mini interface avec trois boutons) pour vous permettre de changer d'environnement à volonté en cours de partie, et un “panneau” qui sert surtout à renvoyer les informations du joueur (du genre : “j'ai pris la clé de la salle X”). Par défaut je commence par générer un “donjon” mais c'est un choix arbitraire, vous pouvez commencer par ce que vous voulez.

function genere(choix:String):void{
 
	while(rendu.numChildren>0) rendu.removeChildAt(0);
 
	grille = 		[];
	grilleTests = 		[];
	grilleObjets = 		[];
	grilleInfosObjets = 	[];
	grilleCollisions = 	[];
	listeCles = 		[];
 
	for (i=0;i<G;i++) {
		grille.push(1);
		grilleTests.push(1);
		grilleObjets.push(0);
		grilleInfosObjets.push(0);
	}
 
	if(choix=="labyrinthe") {
		creeLabyrinthe(entree);
		grilleCollisions = grilleCollisions.concat(grille);
		repartirObjets("labyrinthe");
		renderDonjon();
		ajouteEntreeSortie("labyrinthe");
	}
	if(choix=="caverne") creeCaverne();
	if(choix=="donjon"){
		creeLabyrinthe(entree);
		creeDonjon();
	}
}

Lorsque je souhaite générer un nouvel environnement, je passe par cette fonction. Toute la première partie concerne la remise à zéro des grilles et leur remplissage par défaut. Le seconde partie permet de générer un environnement au choix. Comme vous pouvez le constater, le labyrinthe dispose d'un code supplémentaire, ceci est dû au fait que j'ai besoin du labyrinthe pour générer un donjon, je ne peux donc pas y répartir les objets et lancer son rendu graphique directement depuis le générateur sinon cela ferait double emploi lorsque je veux générer un donjon.

Le labyrinthe

Voyons d'abord tout le code d'un coup.

function creeLabyrinthe(i:int):void{
 
	var X:int =	 	i%C;
	var Y:int = 		i/C;
	var pas:int = 		3;
	var retour:int = 	0;
	var directions:Array = 	[];
	var mouvements:Array = 	[i];
	var bouge:Point;
 
	while(mouvements.length){
		directions = [];
		retour = X+Y*C;
		if (X+pas<C && grille[retour+pas]) 	directions.push(new Point( 1,  0));
		if (X-pas>0 && grille[retour-pas]) 	directions.push(new Point(-1,  0));
		if (Y+pas<L && grille[retour+pas*C]) 	directions.push(new Point( 0,  1));
		if (Y-pas>0 && grille[retour-pas*C]) 	directions.push(new Point( 0, -1));
		if(directions.length){
			bouge = directions[int(Math.random()*directions.length)];
			for (i=0; i<pas; i++) {
				X += bouge.x;
				Y += bouge.y;
				grille[X+Y*C] = 0;
			}
			mouvements.push(X+Y*C);
		} else {
			retour = mouvements.pop();
			X = retour%C;
			Y = retour/C;
		}
	}
}

Le code à été simplifié, notamment grâce à Lilive qui s'y est attardé et a trouvé des raccourcis auxquels je n'avait pas pensé, mais ça reste sommairement la même chose et la même méthode de construction. Notez qu'à présent les variables utilisées sont locales, seule la grille principale est globale, ce qui me permettra par la suite de sortir facilement cette fonction du programme pour la glisser dans une classe. A la fin de la génération je me retrouve donc avec la grille principale modifiée, si je souhaite générer un labyrinthe le travail est terminé et grâce au petit bout de code que j'ai collé dans la fonction “genere” je peux tracer le labyrinthe. Sinon, si je souhaite créer un donjon, la grille principale est modifiée et il reste à y ajouter les pièces (on y reviendra plus tard).

Pas grand chose de plus à dire sur ce générateur, on passe aux cavernes.

La caverne

Dans la première partie de l'exercice je vous avait dit que la génération de caverne était incomplète, en effet un des gros défaut était qu'on pouvait se retrouver avec des salles isolées, or c'est un gros point noir lorsqu'on va essayer d'y répartir des objets, ces derniers devant être accessibles par le joueur. Imaginez que le programme place la sortie de la caverne dans une salle isolée… Il faut donc impérativement corriger ce point et nous avons deux choix. Soit on rempli de murs les salles isolées, on les rebouche, et dans ce cas on se retrouve avec parfois de toutes petites cavernes bourrées d'objets, soit on s'applique à relier d'une manière ou d'une autre les différentes salles pour les rendre accessibles. Sur ce dernier point deux solutions s'offrent à nous, tracer des couloirs, ou tout simplement poser des téléporteurs, c'est cette dernière solution que je vais retenir.

Voyons le nouveau code de ce générateur :

// créer la caverne
function creeCaverne():void{
 
	var r:int = 10;
	cavernes = 1;
	teleporteurs = [];
 
	for(i=0; i<G; i++){
		grille[i] = 0;
		if(Math.random() < 0.4) grille[i] = 1;
		if(bords(i)) grille[i]=1;
	}
 
	while(r--) generation();
	grilleCollisions = grilleCollisions.concat(grille);
	grilleTests = grilleTests.concat(grille);
	addEventListener(Event.ENTER_FRAME, testSortieCaverne);
	for (i=0; i<G; i++){
		if(!grilleTests[i]){
			remplirCaverne(i, 0, ++cavernes);
			return;
		}
	}
}
 
// génération de la map
function generation():void{
 
	var A:int; 
	var B:int; 
	var v:int;
 
	for(i=0; i<G; i++){
		if(!bords(i)) {
			A = 0;
			B = 0;
			for each(v in chercheVoisines(i,true))		if(grille[v]) A++;
			for each(v in chercheVoisinesLointaines(i)) 	if(grille[v]) B++;
			grilleTests[i] = 0;
			if(A>=5 || B<=2) grilleTests[i]=1;
		}
	}
 	for(i=0; i<G; i++) grille[i] = grilleTests[i];
} 
 
 
function remplirCaverne(i:int, A:int, B:int):void{
	var v:int;
	testSortie = false;
	if(grilleTests[i]==A) {
		grilleTests[i]=B;
		for each(v in chercheVoisines(i)) remplirCaverne(v, 0, B);
	}
	testSortie = true;
}
 
function testSortieCaverne(e:Event):void{
	if(testSortie==true) {
 
		// relance un test pour trouver les autres cavernes
		for (i=0; i<G; i++){
			if(!grilleTests[i]){
				remplirCaverne(i, 0, ++cavernes);
				return;
			}
		}
 
		removeEventListener(Event.ENTER_FRAME,testSortieCaverne);
		for(i=0; i<G; i++) grille[i] = grilleTests[i];// colore les cavernes
 
		var t:Array = [];
		var p:int = 0;
		var n:int = 0;
 
		// si il y a plus de une caverne
		if(cavernes>1) {
 
			// compte le nombre de tuile de chaque caverne 
			for (i=2; i<=cavernes; i++) t.push(compteElements(i));
 
			// compare la taille des cavernes
			for (i=0; i<t.length; i++){
				if(n<t[i]) n=t[i], p=i;	
			}
			// pose les téléporteurs dans les salles secondaires
			for (i=0; i<t.length; i++){
				if(i != p) {
					j = indexAleatoire(grille,i+2);
					grille[j] = 14;
					teleporteurs.push([j,0]);
				}
			}
 
			t = [];
			// pose les téléporteurs dans la salle principale
			for (i=0; i<teleporteurs.length; i++){
				j = indexAleatoire(grille,p+2);
				grille[j] = 14;
				teleporteurs[i][1] = j; //  téléporteur d'origine
				t.push([j,teleporteurs[i][0]]); // ajoute le nouveau téléporteur
			}
			teleporteurs = teleporteurs.concat(t);// merge la liste des téléporteurs
		}
 
		repartirObjets("caverne");
		renderDonjon();
		ajouteEntreeSortie("caverne");
	}
}
 
function compteElements(valeur:int):int{
	var v:int = 0;
	var j:int = 0;
	for(j; j<G; j++){
		if(grilleTests[j] == valeur) v++;
	}
	return v;
}

Oui, comparé au code que nous avions dans la première partie de l'exercice, il y a du monde…. on va voir ça pas à pas :

// créer la caverne
function creeCaverne():void{
 
	var r:int = 10;
	cavernes = 1;
	teleporteurs = [];
 
	for(i=0; i<G; i++){
		grille[i] = 0;
		if(Math.random() < 0.4) grille[i] = 1;
		if(bords(i)) grille[i]=1;
	}
 
	while(r--) generation();
	grilleCollisions = grilleCollisions.concat(grille);
	grilleTests = grilleTests.concat(grille);
	addEventListener(Event.ENTER_FRAME, testSortieCaverne);
	for (i=0; i<G; i++){
		if(!grilleTests[i]){
			remplirCaverne(i, 0, ++cavernes);
			return;
		}
	}
}

“r” est le nombre de générations que je souhaite pour créer la caverne (cf partie 1 de l'exercice), “cavernes” va me permettre de compter le nombre de cavernes présentes, par défaut il y en a au moins une, et “teleporteurs” est un tableau où je vais stocker les différents téléporteurs que je vais placer en fonction des salles.

La première boucle me permet de remplir la grille pour une caverne (donc aléatoirement sauf les bords qui sont des murs). Puis je lance mes 10 générations pour affiner la caverne (cf partie 1 de l'exercice). Une fois la caverne propre je rempli la grille des collisions avec les murs que j'ai trouvé, puis je rempli ma grille de tests qui va me servir pour les calculs, c'est la même que la grille principale puisque pour l'instant je n'ai que des murs et des sols.

J'ajoute ensuite un écouteur sur lequel nous reviendrons, et une boucle un peu trompeuse, lue rapidement on dirait qu'elle parcours toute la grille mais en fait non, elle ne fait que chercher le premier élément qui n'est pas un mur et lance un remplissage (sur lequel nous allons également revenir sous peu).

Ok on passe à la seconde partie, les génération :

// génération de la map
function generation():void{
 
	var A:int; 
	var B:int; 
	var v:int;
 
	for(i=0; i<G; i++){
		if(!bords(i)) {
			A = 0;
			B = 0;
			for each(v in chercheVoisines(i,true))		if(grille[v]) A++;
			for each(v in chercheVoisinesLointaines(i)) 	if(grille[v]) B++;
			grilleTests[i] = 0;
			if(A>=5 || B<=2) grilleTests[i]=1;
		}
	}
 	for(i=0; i<G; i++) grille[i] = grilleTests[i];
}

Je ne reviens pas sur la méthode de générations expliquée dans la partie 1 de l'exercice, mais je vais m'attarder quelques secondes sur les raccourcis utilisés. Comme vous pouvez le constater le code est bien plus court, c'est normal puisque j'ai remplacé certains blocs par des fonction dites “génériques”. Ces fonctions sont utilisées par différents générateurs et à de multiples endroits, pourtant elles font toujours la même chose, il est donc inutile de se retaper tout le code à chaque fois, une petite classe serait des plus utiles, mais en attendant on va se contenter de fonctions génériques que je vais aller placer à part (sur un calque par exemple). Je vais vous les décrire dans l'ordre où elles apparaissent dans l'exercice.

La première est “bords()”, elle sert tout simplement à vérifier qu'une tuile fait partie d'un bord de la map ou pas, voici son code :

// trouve les bords de la map
function bords(index:int):Boolean{	
	var X:int = index%C;
	var Y:int = index/C;
	if(X && Y && X!=C-1 && Y!=C-1) {
		return false;
	} else {
		return true;
	}
};

La seconde est “chercheVoisines”, elle permet de chercher les tuiles voisines à une tuile ciblée dans une grille, elle prend deux paramètres, l'index de la tuile ciblée et un booléen qui indique si la recherche se limite aux tuiles contiguës ou s'étend aux diagonales, voici son code :

// cherches les voisines d'une tuile
function chercheVoisines(i:int,tout:Boolean=false):Array{
	var t:Array = [];
	var X:int;
	var Y:int;
	for(X=-1; X<=1; X++){
		for(Y=-1; Y<=1; Y++){	
			if(tout) t.push(i+X+Y*C);
			else if(Math.abs(X+Y)==1) t.push(i+X+Y*C);
		}
	}
	return t;
}

Notez la petite astuce pour limiter la recherche aux tuiles contiguës : Math.abs(X+Y)==1

La troisième fonction générique est “chercheVoisinesLointaines”, si vous revenez sur l'explication de la génération d'une caverne vous y trouverez une recherche de voisine étendue à non plus 1 mais 2 pas de décalage, bien que cette fonction ne soit utile qu'ici je préfère en faire aussi une fonction générique dont voici le code :

// cherches les voisines lointaines d'une tuile
function chercheVoisinesLointaines(i:int):Array{
	var t:Array = [];
	var X:int = i%C;
	var Y:int = i/C;
	var x:int;
	var y:int;
	for(x=X-2; x<=X+2; x++){
		for(y=Y-2; y<=Y+2; y++){
			if(Math.abs(x-X)==2 && Math.abs(y-Y)==2) continue;	
			t.push(x+y*C);
		}
	}
	return t;
}

Le code étant déjà expliqué dans la partie 1 de l'exercice je ne revient pas dessus. Notez que les deux fonctions de recherche de voisines renvoient un tableau constitué du résultat de la recherche pour chaque voisine.

Ok, à ce stade nous avons créé une caverne et effectué 10 générations pour l'affiner, on se retrouve dans la situation présentée dans la première partie de l'exercice, à savoir une caverne composées d'une ou plusieurs salles, dont certaines ne sont pas accessibles car totalement fermées. A présent je dois m'efforcer de détecter chaque salle isolée, les identifier et y poser des téléporteurs reliés entre eux (entrée et sortie).

La méthode que j'utilise s'appelle un algorithme de remplissage par diffusion (cf : http://fr.wikipedia.org/wiki/Algorithme_de_remplissage_par_diffusion ), je vais tout simplement détecter le premier index de la grille comprenant un 0 (on a vu comment plus haut), de ce point je vais lancer une propagation qui rempli toute la zone libre et change la référence des tuiles ce qui me permet d'identifier la salle avec un référent unique. Une fois ce remplissage terminé, j'en relance un, toujours à la recherche d'un zéro (un sol), si j'en trouve un c'est que j'ai une salle isolée, je repart de ce point pour un nouveau remplissage, etc… jusqu'a ce que ma caverne ne dispose plus de zéro mais de tuiles identifiées, chaque identifiant correspondant à une salle unique.

A présent je peux poser des téléporteurs assez facilement, il me reste juste à repérer la plus grande, salle que je considérerai comme principale, à poser un téléporteur cible par identifiant (sauf celui de la salle principale), puis de poser autant de téléporteurs que de salles isolées dans ma salle principale et le tour est joué, toutes mes salles seront accessibles.

Voyons comment cela se met en place :

function remplirCaverne(i:int, A:int, B:int):void{
	var v:int;
	testSortie = false;
	if(grilleTests[i]==A) {
		grilleTests[i]=B;
		for each(v in chercheVoisines(i)) remplirCaverne(v, 0, B);
	}
	testSortie = true;
}

On commence par le plus simple, le remplissage, il s'agit d'une fonction très courte qui prend une tuile de départ, modifie sa référence, et recherche ses voisines. Pour chaque tuile voisine trouvée étant un sol (ou une référence au choix) on relance la même fonction de manière récursive. Notez que j'emploie ici la variable “testSortie”, en effet ce type de fonction récursive doit posséder une condition de sortie si on veut savoir lorsque le remplissage est terminé. Pour cela ma variable “sortieTest” est considérée comme “false” (on est pas sorti) en début de fonction et “true” (on est sorti) en fin de fonction. La fonction étant récursive ma variable “testSortie” ne prendra en fait la valeur “true” que lorsque plus aucune tuile voisine n'est détectée et donc que le remplissage est fini.

Que se passe t'il une fois le premier remplissage terminé ?

function testSortieCaverne(e:Event):void{
	if(testSortie==true) {
 
		// relance un test pour trouver les autres cavernes
		for (i=0; i<G; i++){
			if(!grilleTests[i]){
				remplirCaverne(i, 0, ++cavernes);
				return;
			}
		}
 
		removeEventListener(Event.ENTER_FRAME,testSortieCaverne);
		for(i=0; i<G; i++) grille[i] = grilleTests[i];// colore les cavernes
 
		var t:Array = [];
		var p:int = 0;
		var n:int = 0;
 
		// si il y a plus de une caverne
		if(cavernes>1) {
 
			// compte le nombre de tuile de chaque caverne 
			for (i=2; i<=cavernes; i++) t.push(compteElements(i));
 
			// compare la taille des cavernes
			for (i=0; i<t.length; i++){
				if(n<t[i]) n=t[i], p=i;	
			}
			// pose les téléporteurs dans les salles secondaires
			for (i=0; i<t.length; i++){
				if(i != p) {
					j = indexAleatoire(grille,i+2);
					grille[j] = 14;
					teleporteurs.push([j,0]);
				}
			}
 
			t = [];
			// pose les téléporteurs dans la salle principale
			for (i=0; i<teleporteurs.length; i++){
				j = indexAleatoire(grille,p+2);
				grille[j] = 14;
				teleporteurs[i][1] = j; //  téléporteur d'origine
				t.push([j,teleporteurs[i][0]]); // ajoute le nouveau téléporteur
			}
			teleporteurs = teleporteurs.concat(t);// merge la liste des téléporteurs
		}
 
		repartirObjets("caverne");
		renderDonjon();
		ajouteEntreeSortie("caverne");
	}
}

Ma première salle à été remplie, la première chose que je dois faire c'est vérifier que je n'ai pas une autre salle à remplir, je re-parcoures donc ma grille à la recherche d'un zéro, si j'en trouve un on repart dans un remplissage, sinon c'est que toutes les salles ont été trouvées et remplies et on peut passer à la suite.

Je commence par retirer l'écouteur (on en a plus besoin), puis je met à jour ma grille principale avec les nouvelles données de la grilleTest, c'est à dire avec les références des salles que j'ai trouvé par remplissage.

Mon objectif à présent est de poser les téléporteurs, pour cela je regarde si il y a plus d'une salle, si c'est le cas je cherche la salle principale en comptant le nombre de tuile de chaque salle :

// compte le nombre de tuile de chaque caverne 
for (i=2; i<=cavernes; i++) t.push(compteElements(i));

J'utilise pour cela une petite fonction indépendante que j'ai placé tout à la fin du générateur et dont voici le code :

function compteElements(valeur:int):int{
	var v:int = 0;
	var j:int = 0;
	for(j; j<G; j++){
		if(grilleTests[j] == valeur) v++;
	}
	return v;
}

Elle ne fait que compter les éléments identiques dans la grille, comme j'ai une référence unique par salle c'est assez facile de compter les tuiles, attention cependant, les 0 (sols) n'existent plus pour la caverne (sauf dans la grille des collisions) et les 1 (murs) ne doivent pas non plus être comptés, c'est pour ça que la boucle précédente commence à 2 et non à 0.

A présent je vais comparer la taille de toutes les salles :

// compare la taille des cavernes
for (i=0; i<t.length; i++){
	if(n<t[i]) n=t[i], p=i;	
}

J'identifie ici la salle principale, celle qui compte le plus grand nombre de tuiles et que je repère par la variable “p”.

Je veux à présent poser mes téléporteurs dans toutes les salles secondaires :

// pose les téléporteurs dans les salles secondaires
for (i=0; i<t.length; i++){
	if(i != p) {
		j = indexAleatoire(grille,i+2);
		grille[j] = 14;
		teleporteurs.push([j,0]);
	}
}

Pour chaque salle qui n'est pas la principale je vais chercher un index aléatoire, j'utilise pour cela la fonction générique suivante :

// retourne un index aléatoire avec condition
function indexAleatoire(G:Array,A:int):int{
	var index:int = Math.random()*G.length;
	while (G[index]!=A) index = Math.random()*G.length;
	return index;
}

“G” est la grille dans laquelle je veux chercher, et “A” est a référence du type de tuile que je cherche.

Une fois que j'ai un index libre dans une salle, j'y pose un téléporteur et j'enregistre, dans le tableau de stockage des téléporteurs, sa référence (le point de départ) et pour le moment une destination nulle (le point de d'arrivée).

Chaque salle secondaire est à présent équipée d'un téléporteur, je dois donc placer les récepteurs correspondants dans ma salle principale :

t = [];
// pose les téléporteurs dans la salle principale
for (i=0; i<teleporteurs.length; i++){
	j = indexAleatoire(grille,p+2);
	grille[j] = 14;
	teleporteurs[i][1] = j; //  téléporteur d'origine
	t.push([j,teleporteurs[i][0]]); // ajoute le nouveau téléporteur
}

Pour chaque téléporteur stocké, je crée un nouveau téléporteur dans la salle principale, je modifie la destination du téléporteur de départ pour faire correspondre l'arrivée (celui que je suis en train de placer) et j'ajoute le nouveau téléporteur dans un tableau temporaire.

Lorsque tous les téléporteurs son placés je met à jour la liste des téléporteurs, elle me servira pour téléporter le joueur par la suite.

// merge la liste des téléporteurs
teleporteurs = teleporteurs.concat(t);

C'est terminé pour cette partie, il reste bien sur à poser les objets, lancer le rendu et poser la sortie de la caverne, des choses générique sur lesquelles nous reviendrons plus tard et qui se lancent ainsi :

repartirObjets("caverne");
renderDonjon();
ajouteEntreeSortie("caverne");

Bien, voilà deux environnements de propres, reste le troisième, le donjon.

Le donjon

Contrairement au labyrinthe et à la caverne, le donjon est un environnement doté d'une structure complexe, composée de pièces, de couloirs et surtout de portes. Si nous avons vus comment placer les portes dans la première partie de l'exercice, nous ne savons pas encore comment répartir les clés qui les ouvrent. On ne peut pas les lancer aléatoirement car on risquerait de se retrouver dans une configuration où le joueur ne peut atteindre une clé pour ouvrir la porte qui lui bloque le passage et donc la partie est injouable. Il va donc falloir trouver une méthode qui réparti les clés de manière cohérente, en s'assurant que la clé pour ouvrir une porte est toujours accessible selon le chemin que va prendre le joueur, c'est là tout le défi qui nous intéresse sur cette partie.

Voyons le code :

function creeDonjon():void{
 
	var t:Array;
	var piece:Array =  [];
	var salles:Array = [];
	var libres:int;
	var diagos:int;
	var index:int;
	var angle:Boolean;
 
	var X:int;
	var Y:int;
	var v:int;
 
	// initialise les salles
	for(i=0; i<36; i++)	{
		salles.push([]);
		for (j=0; j<64; j++) salles[i].push(1);
	}
 
	// crée les pièces
	for each (piece in salles){
		index = salles.indexOf(piece);
		X =  Math.random()*8;
		Y =  Math.random()*8;
		for (i=0; i<X; i++) {
			for (j=0; j<Y; j++) {
				piece[i+j*8] = index+30;
			}
		}
		for (i=0; i<piece.length; i++) {
			j = i%8+(index%6)*8+(int(i/8)+int(index/6)*8)*C;
			if(!bords(j)) {
				grilleTests[j] = piece[i];
				if(piece[i]>1) grille[j] = 2; 
			}
		}
	}
 
	// ajuste les pieces dans la grille
	for (i=0; i<G; i++){
		if(grille[i]!=1){
 
			t = [];	
 
			for each(v in chercheVoisines(i,true)) {
				if(grille[v]!=1) t.push(0) else t.push(1); 
			}
 
			libres = 			4-(t[3]+t[5]+t[1]+t[7]);
			diagos = 			t[0]+t[2]+t[6]+t[8];
			angle = 			t[0]+t[1]+t[3]==0 || t[1]+t[2]+t[5]==0 || t[3]+t[6]+t[7]==0 || t[5]+t[7]+t[8]==0;
 
			if (libres==0) 			grille[i] = 1;
			if (libres==1) 			grille[i] = 6;
			if (libres==2) {
				if (t[3]+t[5]==2) 	grille[i] = 0;
				if (t[1]+t[7]==2) 	grille[i] = 0;
				if (diagos > 1) 	grille[i] = 0;
				if (angle) 		grille[i] = 2;
			}	
			if (libres==3) {
				if (diagos > 1) 	grille[i] = 0;
				if (angle) 		grille[i] = 2;
			}	
			if (libres==4) {
				if (diagos == 4) 	grille[i] = 0;
				else 			grille[i] = 2;
			}
		}
	}
 
	// corrige les pieces dans la grille test et pose les portes
	for (i=0; i<G; i++){
		for each(v in chercheVoisines(i)) {
			if(grille[v]==2) {
				if(grilleTests[i]>29) grilleTests[v]=grilleTests[i];
				if(grille[i]==0) grille[i]=5;
			}
		}
	}
 
	// corrige la grille définitive
	for (i=0; i<G; i++){
		grilleCollisions[i] = 0;
		if(grille[i]==1) grilleCollisions[i] = 1;
		if(grille[i]==5) grilleCollisions[i] = 1;
		if(grille[i]==2) grille[i]=grilleTests[i];		
	}
 
	// enregistre le numéro des portes
	for (i=0; i<G; i++){
		if(grille[i]==5){
			for each(v in chercheVoisines(i)) {
				if(grille[v]>29) grilleObjets[i] = grille[v]-28;
			}
		}
	}
 
	// répartition des clés
	for(i=0; i<G; i++) grilleTests[i] = 0;
	addEventListener(Event.ENTER_FRAME, finCreaDonjon);
	grilleTests[entree] = 1;
	repartirCles(entree);
}
 
// répartir les clés
function repartirCles (i:int):void{
	testSortie = false;
	var libres:Array = [];
	var portes:Array = [];
	var v:int;
 
	// cherche les voisines libres et enregistre les portes
	for each(v in chercheVoisines(i)) {
		if (grille[v]!=1 && grilleTests[v]==0)  libres.push(v);
		if (grilleObjets[v] && portes.indexOf(grilleObjets[v])==-1) portes.push(grilleObjets[v]);
	}
 
	// ouvre les portes et jette la clé
	while(portes.length){
		for (j=0; j<portes.length; j++){
			if(listeCles.indexOf(portes[j])==-1){
				i = Math.random()*G;
				while(grilleTests[i]==0 || grilleObjets[i]==4 || grille[i]==5){		
					i = Math.random()*G;
					for each(v in chercheVoisines(i)) {
						if(grilleObjets[v]==4) i = Math.random()*G;
					}
				}
				grilleObjets[i] = 4; 
				grilleInfosObjets[i] = portes[j];
				listeCles.push(portes[j]);
			}
			portes.splice(j,1);
		}
	}
 
	// lance un nouveau test à chaque emplacement libre
	while(libres.length){
		i = libres.shift();
		if(grilleTests[i]==0) {
			grilleTests[i] = 1;
			repartirCles(i);
		}
	}
	testSortie = true;
}
 
// la recherche est terminée
function finCreaDonjon(e:Event):void{
	if(testSortie==true) {
		removeEventListener(Event.ENTER_FRAME,finCreaDonjon);
		repartirObjets("donjon");
		renderDonjon();
		ajouteEntreeSortie("donjon");
	}
}

Nous avons déjà vu comment générer le donjon et poser les portes, ouf, une bonne partie du programme qu'il n'est pas nécessaire d'expliquer de nouveau, intéressons nous à la répartition des clés qui se lance ainsi à la fin de la première fonction :

// répartition des clés
for(i=0; i<G; i++) grilleTests[i] = 0;
addEventListener(Event.ENTER_FRAME, finCreaDonjon);
grilleTests[entree] = 1;
repartirCles(entree);

L'écouteur devrait vous mettre la puce à l'oreille (mauvais jeu de mot….), on va utiliser exactement le même principe que pour la caverne, c'est à dire un algorithme de remplissage. Cette fois on part de l'entrée et on propage, quand une tuile trouve une porte elle vérifie si la clé a déjà été jetée (si c'est le cas elle ne tient pas compte de la porte), dans le cas contraire, elle enregistre le numéro de la porte (donc la référence de la clé), jettes la clé aléatoirement sur le chemin parcouru par le remplissage et continue. De cette manière on s'assure que les clés sont bien accessibles et donc que la répartition est cohérente.

Voyons comment cela se met en place :

// répartir les clés
function repartirCles (i:int):void{
	testSortie = false;
	var libres:Array = [];
	var portes:Array = [];
	var v:int;
 
	// cherche les voisines libres et enregistre les portes
	for each(v in chercheVoisines(i)) {
		if (grille[v]!=1 && grilleTests[v]==0)  libres.push(v);
		if (grilleObjets[v] && portes.indexOf(grilleObjets[v])==-1) portes.push(grilleObjets[v]);
	}
 
	// ouvre les portes et jette la clé
	while(portes.length){
		for (j=0; j<portes.length; j++){
			if(listeCles.indexOf(portes[j])==-1){
				i = Math.random()*G;
				while(grilleTests[i]==0 || grilleObjets[i]==4 || grille[i]==5){		
					i = Math.random()*G;
					for each(v in chercheVoisines(i)) {
						if(grilleObjets[v]==4) i = Math.random()*G;
					}
				}
				grilleObjets[i] = 4; 
				grilleInfosObjets[i] = portes[j];
				listeCles.push(portes[j]);
			}
			portes.splice(j,1);
		}
	}
 
	// lance un nouveau test à chaque emplacement libre
	while(libres.length){
		i = libres.shift();
		if(grilleTests[i]==0) {
			grilleTests[i] = 1;
			repartirCles(i);
		}
	}
	testSortie = true;
}

Pour les cavernes c'était simple, on se contentait de remplir les salles, là nous avons besoin d'un peu plus de manipulations. On part de l'entrée, on cherche les tuiles voisines libres et on les stocke dans un tableau qui va représenter le chemin parcouru, si on tombe sur une porte on l'enregistre aussi, ainsi on sera en mesure de savoir si on a déjà ouvert un type de porte. Tant qu'il y a des portes d'enregistrées, pour chacune on vérifie si la clé est déjà présente dans la liste des clés, si c'est le cas on ne la compte pas, sinon on jette la clé correspondant à la porte sur le chemin. Afin d'assurer une répartition plus homogène, je vérifie au moment où je pose ma clé qu'il n'y a pas déjà une clé dans son voisinage, auquel cas je choisi une autre tuile pour poser la clé. Quand une clé est posée, je l'enregistre dans la grille des objets, j'enregistre sa référence dans la grille des infos des objets et j'enregistre aussi la clé dans la liste des clés. Enfin, pour chaque voisine libre que j'ai trouvé, je l'identifie comme étant découverte et je relance la propagation à partir de ces tuiles.

Le remplissage du donjon est terminé, les clés sont réparties, que faire à présent ?

// la recherche est terminée
function finCreaDonjon(e:Event):void{
	if(testSortie==true) {
		removeEventListener(Event.ENTER_FRAME,finCreaDonjon);
		repartirObjets("donjon");
		renderDonjon();
		ajouteEntreeSortie("donjon");
	}
}

On retire l'écouteur, on lance la répartition des objets, le rendu graphique et on ajoute la sortie.

Cette fois c'est terminé pour le générateur du donjon, ouf !

Répartition des objets

Nos trois environnements sont générés, fort heureusement la répartition des objets est quelque chose de tout à fait générique (en dehors des clés), nous allons donc pouvoir l'aborder indépendamment du reste.

Tout d'abord le code :

function repartirObjets(c:String):void{
	var t:Array;
	if(c=="labyrinthe") 	t = [10,15,15,15,10,5];
	if(c=="caverne") 	t = [8,10,4,15,3,5];
	if(c=="donjon") 	t = [20,30,30,30,10,10];
	placeStructure(7,5);					// magasins (5 en tout)
	placeStructure(8,2+Math.random()*t[0]);			// portes dérobées
	placeObjet(10,2+Math.random()*t[1]);			// coffres
	placeObjet(11,2+Math.random()*t[2]);			// pnjs
	placeObjet(12,2+Math.random()*t[3]);			// ennemis
	placeObjet(13,2+Math.random()*t[4]);			// nids
	placeObjet(14,2+Math.random()*t[5]); 			// quêtes	
}
 
function placeStructure(n:int, b:int):void{
	var v:int;
	while(b){
		j = 4;
		i = Math.random()*G;
		if(grille[i]==1 && !bords(i)){
			for each(v in chercheVoisines(i)) {
				if(grille[v]==1 || grille[v]==5) j--;
			}
			if(j) {
				grille[i] = n;
				b--;
			} 
		}
	}
}
 
function placeObjet(n:int, b:int):void{
	while(b){
		i = Math.random()*G;
		if(grille[i]!=1 && grille[i]!=7 && grille[i]!=14 && grilleObjets[i]==0){
			grilleObjets[i] = n; 
			b--;
		}
	}
}

Nous avons là une fonction principale qui lance la répartition, cette dernière est différente (non pas en termes de configuration mais de nombre) en fonction de chaque environnement, un labyrinthe contient moins d'objets qu'un donjon par exemple, pour cela trois références et un tableau suffisent.

On va distinguer deux groupes d'objets, les structures et les items. Les structures sont des objets faisant partie des murs, comme les portes dérobées et les magasins, les items sont quand à eux placés sur des tuiles vides, des sols, et peuvent représenter à la fois des objets inanimés, comme des coffres, et des objets animés comme des PNJ ou des ennemis (il est plus simple de se représenter un ennemi comme un objet lambda pour le moment, on fera évoluer le moteur plus tard).

Selon le type d'environnement je vais donc générer un certain nombre de structures et d'objets à l'aide de deux fonctions simples :

function placeStructure(n:int, b:int):void{
	var v:int;
	while(b){
		j = 4;
		i = Math.random()*G;
		if(grille[i]==1 && !bords(i)){
			for each(v in chercheVoisines(i)) {
				if(grille[v]==1 || grille[v]==5) j--;
			}
			if(j) {
				grille[i] = n;
				b--;
			} 
		}
	}
}

Pour les structures, je m'assure que je ne suis pas sur un bord, que la tuile où je veux la poser est bien un mur, n'est pas une porte ni une autre structure, et qu'au moins une voisine contiguë est libre.

function placeObjet(n:int, b:int):void{
	while(b){
		i = Math.random()*G;
		if(grille[i]!=1 && grille[i]!=7 && grille[i]!=14 && grilleObjets[i]==0){
			grilleObjets[i] = n; 
			b--;
		}
	}
}

Pour les objets je m'assure que la tuile où je veux le poser n'est pas un mur, une porte, un téléporteurs ou un autre objet.

C'est terminé pour la répartition des objets et structures, reste encore à poser une sortie dans les environnement afin de permettre au joueur de passer d'une salle à une autre, ça se fait avec une petite fonction générique dont voici le code :

function ajouteEntreeSortie(type:String):void{
 
	var poser:Boolean;
	var v:int;
 
	if(type=="labyrinthe") 		ajouteJoueur(entree);// labyrinthe	
	if(type=="donjon")		ajouteJoueur(entree);// donjon
	if(type=="caverne"){
		i = Math.random()*G;
		while(grilleCollisions[i]!=0 || grille[i]==8){
			i = Math.random()*G;
		}
		ajouteJoueur(i);
	}
 
	// poser la sortie
	i = Math.random()*G;
	poser = false;
	while(grille[i]!=1 || poser==false){
		poser = false;
		i = Math.random()*G;
		for each(v in chercheVoisines(i)) {
			if (grille[v]!=1 && grille[v]!=7 && grille[v]!=8 && grille[v]!=undefined) poser = true;
		}
	}
	renduTuile(i%C,int(i/C),16,"");
	grilleObjets[i] = 1000;// enregistre la sortie dans la map
}

Là c'est un peu confus, j'en suis désolé, tout d'abord pour poser la sortie je m'assure qu'elle fait partie d'un mur et non d'un bord, j'évite de la poser de manière contiguë à une porte ou tout un tas de choses qui pourraient me bloquer, je l'ajoute au rendu graphique et je l'identifie par un numéro bien spécifique dans la grille.

Mais dans cette fonction (c'est là que c'est un peu mal foutu), je vais également poser le joueur, si pour le donjon et le labyrinthe on à pas le choix, il se place à l'entrée, pour les cavernes ce n'est pas la même chose, il faut s'assurer que la tuile choisie est bien un simple sol, une petit boucle est donc nécessaire.

Nous y sommes, terminé pour les générateurs et la répartition cohérente des objets, structures, sorties, etc….

Et si nous ajoutions à présent un joueur et un petit moteur pour tester nos environnements ?

Joueur et moteur

Nous sommes dans un contexte “tile based”, donc un moteur qui repose essentiellement sur des grilles et des tuiles. Ce type de moteur à déjà largement été expliqué sur le Wiki, que ce soit en POO ou en procédural. Tout ce que nous allons faire c'est adapter le principe générique à nos grilles, voyons donc le code :

// ajouter un joueur
function ajouteJoueur(i:int):void{
	vitesse = 	5;
	listeCles =  	[];
	inventaire = 	[];
	joueur = new Joueur();
	joueur.x = (i%C)*T;
	joueur.y = int(i/C)*T;
	rendu.addChild(joueur);
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuier);
	stage.addEventListener(KeyboardEvent.KEY_UP, relacher);
	stage.addEventListener(Event.ENTER_FRAME, deplacer);
}
 
// appuyer sur une touche
function appuier (e:KeyboardEvent):void{
	if (e.keyCode == 37) 	gauche = 1;
	if (e.keyCode == 39) 	droite = 1;
	if (e.keyCode == 38)	haut = 1;
	if (e.keyCode == 40)	bas = 1;
	if (e.keyCode == 32)	action = 1;
}
 
// Relâcher une touche
function relacher (e:KeyboardEvent):void{
	if (e.keyCode == 37) 	gauche = 0;
	if (e.keyCode == 39) 	droite = 0;
	if (e.keyCode == 38)	haut = 0;
	if (e.keyCode == 40)	bas = 0;
	if (e.keyCode == 32)	action = 0;
}
 
// déplacer le joueur
function deplacer(e:Event):void{
 
	var bouge:Array = collisions(droite-gauche,bas-haut,joueur.x,joueur.y);
	var objet:int;
	var infos:int;
	var v:int;
	var t:Array = ["labyrinthe","caverne","donjon"];
 
	if(!bouge[2]){
		joueur.x = bouge[0];
		joueur.y = bouge[1];
	}	
 
	i = int((joueur.x+joueur.width/2)/T)+int((joueur.y+joueur.height/2)/T)*C;
	objet = grilleObjets[i];
	infos = grilleInfosObjets[i];
 
	// ramasse les coffres et objets de quête automatiquement
	if(objet==14 || objet==10){
		inventaire.push(objet);
		if(objet==10)affichePanneau("Coffre au trésor !");
		if(objet==14)affichePanneau("Objet de quête !");
		grilleObjets[i] = 0;
	}
 
	// ramasse les clés automatiquement
	if(objet==4){
		if(listeCles.indexOf(infos)==-1){
			listeCles.push(infos);
			affichePanneau("Clé de la salle : \n"+infos);
		}
	}
 
	if(action) {
 
		// ouvre les portes, magasins et portes dérobées
		for each(v in chercheVoisines(i)) {
			if (grille[v]==7) affichePanneau("Le magasin est fermé.");
			if (grille[v]==8) {
				affichePanneau("Passage secret ouvert !");
				grilleCollisions[v] = 0;
				grilleObjets[v] = 0;
			}
			if (grille[v]==5) {
				if(listeCles.indexOf(grilleObjets[v])>=0){
					affichePanneau("Salle "+grilleObjets[v]+" ouverte !");
					grilleCollisions[v] = 0;
					grilleObjets[v] = 0;
				} else if(grilleCollisions[v]){
					affichePanneau("Fermé !");
				}
			}
			// passe la sortie
			if(grilleObjets[v]==1000){
				affichePanneau("Sortie !");
				genere(t[int(Math.random()*3)]);
			}
		}
 
		// passe un téléporteur
		if(grille[i]==14){
			for(j=0; j<teleporteurs.length; j++){
				if(teleporteurs[j][0]==i) {
					affichePanneau("Téléportation !");
					joueur.x = int(teleporteurs[j][1]%C)*T;
					joueur.y = int(teleporteurs[j][1]/C)*T;
					action = 0;
				}
			}
		}
 
		if(objet==13)	affichePanneau("Un nid.");// interagit avec nid
		if(objet==11) 	affichePanneau("Un PNJ.");// interagit avec pnj ** attention pourront être en mouvement **
		if(objet==12)	affichePanneau("Un ennemi.");// interagit avec ennemi ** attention pourront être en mouvement **
	}
}

Nous allons passer les points qui sont déjà ultra connus (voir pré requis) :

  • ajouter un joueur
  • appuyer sur une touche
  • relâcher une touche

Et nous intéresser aux déplacement du joueur.

Dès la première ligne on gère les collisions :

bouge:Array = collisions(droite-gauche,bas-haut,joueur.x,joueur.y);

Pour ça j'utilise une nouvelle fonction générique qui fonctionnera autant pour les joueurs que les PNJ ou les ennemis, bref pour tout ce qui se déplace dans la grille, voici son code :

// collisions avec le décor
function collisions(dX,dY,x,y):Array{
 
	var col:int = 0;
	var lig:int = 0;
	var hit = false;
 
	x += dX*vitesse;
	if (dX<0) {
		col = x/T;	
		for (lig=y/T; lig<(y+T-1)/T; lig++) {
			if (grilleCollisions[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 (grilleCollisions[col+lig*C]==1) {
				x = col*T-T;
				hit = true;
			}
		}
	}	
 
	y += dY*vitesse;
	if (dY<0) { 				
		lig = y/T ;
		for (col = x/T; col<(x+T-1)/T; col++) {
			if (grilleCollisions[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 (grilleCollisions[col+lig*C]==1) {
				y = lig*T-T;
				hit = true;
			} 
		}
	}
	return [x, y, hit];
}

Comme nous sommes dans un “exercice” et pas un “tutoriel”, ça va être à vous de travailler à ce niveau, pas d'explications si ce n'est que la grille qui sert aux collisions est “grilleCollisions” ;-)

Je plaisante, mais les explications seront brèves, on va regarder dans quelle direction le joueur se déplace sur chaque axe, puis à l'aide de sa vitesse et de sa direction, on teste toutes les tuiles occupées par le joueur avec la grille des collisions. Si on tombe sur un mur, il y a collision, on retourne donc un tableau de valeurs utiles (position x, position y, indicateur de collision).

Revenons à nous moutons, à la suite des collisions (dont je stocke le résultat dans un tableau) je déplace mon joueur. Puis je vais aller chercher la position du centre du joueur, ce sera plus pratique pour gérer le ramassage des objets.

i = int((joueur.x+joueur.width/2)/T)+int((joueur.y+joueur.height/2)/T)*C; // centre du joueur

Je regarde si le centre de mon joueur correspond à une référence d'objet ou d'info dans une des grilles.

objet = grilleObjets[i];
infos = grilleInfosObjets[i];

Certains objets sont ramassés automatiquement.

// ramasse les coffres et objets de quête automatiquement
if(objet==14 || objet==10){
	inventaire.push(objet);
	if(objet==10)affichePanneau("Coffre au trésor !");
	if(objet==14)affichePanneau("Objet de quête !");
	grilleObjets[i] = 0;
}
 
// ramasse les clés automatiquement
if(objet==4){
	if(listeCles.indexOf(infos)==-1){
		listeCles.push(infos);
		affichePanneau("Clé de la salle : \n"+infos);
	}
}

Quand un objet est ramassé, il est ajouté à l'inventaire du joueur et le joueur signale ce qu'il vient de prendre, par l'intermédiaire du panneau d'affichage. L'objet ayant été ramassé on le supprime de la grilleObjets dans laquelle il était référencé. Pour les clés c'est le même principe.

Certains objets ou éléments de structure vont demander une intervention du joueur, comme par exemple ouvrir la porte d'un magasin ou une porte dérobée, parler à un PNJ, attaquer un ennemi, …

if(action) {
 
	// ouvre les portes, magasins et portes dérobées
	for each(v in chercheVoisines(i)) {
		if (grille[v]==7) affichePanneau("Le magasin est fermé.");
		if (grille[v]==8) {
			affichePanneau("Passage secret ouvert !");
			grilleCollisions[v] = 0;
			grilleObjets[v] = 0;
		}
		if (grille[v]==5) {
			if(listeCles.indexOf(grilleObjets[v])>=0){
				affichePanneau("Salle "+grilleObjets[v]+" ouverte !");
				grilleCollisions[v] = 0;
				grilleObjets[v] = 0;
			} else if(grilleCollisions[v]){
				affichePanneau("Fermé !");
			}
		}
		// passe la sortie
		if(grilleObjets[v]==1000){
			affichePanneau("Sortie !");
			genere(t[int(Math.random()*3)]);
		}
	}
 
	// passe un téléporteur
	if(grille[i]==14){
		for(j=0; j<teleporteurs.length; j++){
			if(teleporteurs[j][0]==i) {
				affichePanneau("Téléportation !");
				joueur.x = int(teleporteurs[j][1]%C)*T;
				joueur.y = int(teleporteurs[j][1]/C)*T;
				action = 0;
			}
		}
	}
 
	if(objet==13)	affichePanneau("Un nid.");// interagit avec nid
	if(objet==11) 	affichePanneau("Un PNJ.");// interagit avec pnj 
	if(objet==12)	affichePanneau("Un ennemi.");// interagit avec ennemi 
}

Comme pour la fonction de collisions, je vous laisses dépiauter, c'est toujours le même principe ;-)

Et c'est terminé…. pour le moment !

Conclusion

Pour le moment nous n'avons que des grilles et des tuiles de couleur, on peut considérer ça comme un moteur basique, mais les jeux utilisent des graphismes colorés pour offrir une expérience intéressante aux joueurs. En outre il est possible, et même préférable, de proposer différentes vues, citons par exemple le top-down, l'isométrique, ou le subjectif, trois notions totalement détachées des calculs d'environnement. A partir de cette base il vous sera donc possible de générer les vues que vous souhaitez, cela passera sans doute par la création d'une nouvelle grille de décor adaptée à la vue que vous souhaitez mettre en place. Tout est possible que ce soit du scrolling ou de la disposition d'effets comme les lumières, les zones non éclairées, les débris, et tout un tas de petites choses qui permettent un rendu graphique intéressant. Notez que le moteur du joueur devra être amélioré et repensé, ce n'est pour le moment qu'un brouillon, au même titre que tout le reste du programme d'ailleurs, mon but est juste de vous montrer des solutions possible, à vous de les concrétiser dans des programmes viables ;-)

Les sources

moteur_objets_mb.fla Moteur et Objets version CS6
moteur_objets_mb_cs5.fla Moteur et Objets version CS5