Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Etape 2 : Grilles et collision

Par Monsieur Spi, le 23 avril 2011

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu. Utilisez les flèches de votre clavier pour déplacer le personnage.

Nous avons vus dans le chapitre précédent comment déplacer et animer un personnage.
Je me propose de vous expliquer comment ajouter un pseudo décor et les faire interagir.
Notez que nous allons repartir de la source de l'exercice précédent pour celui-ci.

Tout d’abord sachez que nous ne travaillons pas dans le bon ordre.
D’habitude on commence par étudier le moteur avant de s’attaquer aux animations.
Mais cette première étape a sans doute été plus simple à aborder pour la plupart des débutants.
En général la première chose qu'on s'amuse à faire quand on débute c'est un personnage qui bouge.
Nous allons donc tenter de nous greffer dessus malgré tout.
La prochaine fois commencez directement le moteur avant de faire joujou avec les animations ;-)

Cet exercice constitue donc, en fait, la véritable première étape de notre aventure.

Comme souvent, il y a plusieurs manières de faire les choses et je vais choisir d’utiliser des tuiles.
Le principe des tuiles est expliqué un peu partout sur le Web, et c’est sans doute la méthode la plus répandue pour de nombreux jeux, vous ne manquerez donc pas de références si vous souhaitez aller plus loin. De plus c’est une solution qui marche à peu près partout quel que soit le langage utilisé car elle repose essentiellement sur des formules mathématiques simples. Et c’est plutôt sympa d’apprendre des choses qui passent presque partout. Enfin, vu la profusion de tutoriels qui expliquent l’utilisation des tuiles, je vais me concentrer sur l'essentiel.

Notez qu'en fait lorsqu'on parle de “technique des tuiles” on devrait plutôt parler de “technique de la grille”, car comme vous allez le voir au fil de cet exercice c'est surtout la grille qui est importante pour le moteur.

1 - Grille de tuiles

Quand on parle de tuiles (Tiles en anglais) on imagine immédiatement le toit d’une maison ou, avec un petit effort d’imagination, une mosaïque. Une mosaïque est un ensemble de petites pièces qui représente un dessin, chaque pièce représente une petite partie du dessin à afficher.

plateforme_00.jpg

Chaque tuile est rigoureusement de la même taille et de la même forme que toutes les autres.
Chaque tuile peut (ou pas) représenter une couleur ou un dessin différent.
L’ensemble forme un dessin complet ou la structure d’un dessin.

Maintenant si on vous demandait de construire cette mosaïque comment vous y prendriez-vous ?
Il vous faudrait un plan pour éviter de perdre du temps, une sorte de grille avec des repères.
Et pour chaque case de votre grille il faut une référence pour savoir quelle tuile y placer.
Enfin au dos de chaque pièce à poser il faut la même référence pour associer pièce et case.

Ok mais ça sert à quoi quand on crée un jeu ?

plateforme_01.jpg

Les tuiles ont deux objectifs, l’un technique qui permet de simplifier les calculs, et l’autre graphique qui permet d’alléger le poids du jeu et la taille des images utilisées pour les décors, parfois très grands.

Mathématiquement parlant, il est bien plus simple de travailler avec des grilles qu’avec des formes libres. Une grille est composée de cases toutes identiques dont la taille et la position est connue, il est donc possible de normaliser les calculs en fonction des cases, nous y reviendrons.

D’autre part, si en ActionScript il existe une fonction pré-mâchée appelée « hitTest » qui permet de gérer plus ou moins efficacement les collisions libres entre deux objets, sachez que ce n’est pas le cas de tous les langages de programmation. Il est donc fort utile de savoir faire sans, d’autant que la fonction « hitTest », lorsqu’elle est mal employée, est un gouffre à ressources. Les tuiles vont donc simplifier les calculs nécessaires à la détection de collision.

plateforme_02.jpg

L’autre avantage des tuiles c’est de réduire le poids des décors, et du même coup du jeu.
En effet si on décompose le décor en une grille, certaines tuiles sont rigoureusement identiques.
Dans le décor de Mario ci-dessus on s’aperçoit qu’en fait très peu de tuiles différentes sont utilisées.

  • 1 = plateforme_03.jpg = ciel
  • 2 = plateforme_04.jpg = sol
  • 3 = plateforme_05.jpg = plateformes

Dans la grille, à chaque chiffre correspond un type de tuile.
Ainsi à l’aide de peu de tuiles on peut créer des décors gigantesques qui ne pèsent presque rien.
Le poids total de l’image complète du décor correspond au poids des modèles de tuiles utilisés.

Les tuiles sont très utiles pour débuter dans la création de jeux, elles sont aussi indispensables que les tableaux, mais sachez qu’il existe, bien sûr, d’autres méthodes qui permettent de s’en passer, plus complexes et inadaptées pour ce tutorial débutant.

2 – Préparer son travail

Toute construction suit des règles précises si on ne veut pas se retrouver avec des objets mal proportionnés et des détections imprécises. Nous devons donc définir nos bases de travail avec exactitude afin que tout les calculs soient très simples à effectuer et sans marge d’erreur.

Utiliser une norme

Nous venons de voir que nous allons travailler avec une grille.
Cette grille a pour le moment une taille fixe (largeur et hauteur).
Chaque case de cette grille a une taille fixe (largeur et hauteur).

Quand on fait un jeu on doit s’efforcer à tout instant de trouver des raccourcis pour simplifier notre code sans pour autant alourdir son fonctionnement et souvent les calculs peuvent être simplifiés à l’aide d’une « norme ».

Simplifions la définition de Wikipedia pour les non matheux :

En géométrie, la norme permet de mesurer la longueur commune à toutes les représentations d'un vecteur, mais définit aussi une distance invariante entre deux vecteurs.

plateforme_06.jpg

Pas de panique ce n’est pas si compliqué à comprendre, si on ne veut pas entrer dans les détails.
Par exemple, si je travaille avec des cases ayant une taille de 32 pixels, l’unité de mesure est 32.
Toutes les positions des cases de ma grille seront un multiple de 32, c'est-à-dire :

  • 1 = 32
  • 2 = 64
  • 3 = 96

1 case est donc égale à 1 unité de mesure.
Nous pouvons du coup travailler avec des valeurs plus simples pour nos calculs.
Et lorsqu’on travaille de cette manière on utilise forcément des nombres entiers.
Si par exemple je souhaite savoir à quelle case correspond la position 259 pixels sur l’axe X :

259/32 = 8,09375

Or l’entier de 8.09375 est 8, notez que l’entier est toujours un arrondi à la valeur inférieure.
La valeur 259 pixels sur X correspond donc à une case située dans la huitième colonne de ma grille.
Et grâce à notre plan nous sommes capables de savoir quel type de tuile occupe cette case.

Puisque nous venons de définir une unité de mesure, nous allons la stocker dans une variable.
Ainsi pour changer la taille il suffit de changer l’unité de mesure, la plupart des calculs resteront justes.

Dans le calque « variables » ajouter la ligne suivante :

var T:int = 32;

A partir de maintenant (T), notre unité de mesure pour les cases, est de 32 pixels (hauteur et largeur).
S’il est plus facile de travailler avec des tuiles carrées, rien ne vous empêche de tenter l’aventure avec des tuiles rectangulaires, hexagonales, ou plus tard (sans doute) avec des vecteurs, des triangles, des cercles et tout un tas d’autres formes tortueuses.

Adapter la zone de jeu

Nous venons de déterminer une unité de mesure : 32.
Ce qui signifie que chaque case de notre grille est de taille 32*32 pixels.

Or dans l’exercice précédent nous avions défini la taille de notre projet à :

  • Largeur = 600 px
  • Hauteur = 400 px

Ces valeurs ne sont pas un multiple de notre unité de mesure : 600/32 = 18,75 et 400/32=12,5
Donc impossible de faire entrer correctement toutes les cases de la grille dans la scène.
Pour corriger ceci rien de plus simple, il suffit de trouver des valeurs multiples de l’unité de mesure.

  • 32*20 = 640
  • 32*15 = 480

Notre scène aura donc d’une taille de 640*480.
Elle comportera 20 colonnes et 15 lignes.

Modifiez la taille de la scène dans l’exercice précédent.

Utiliser des tableaux

Nous avons la taille de la scène et de la grille, ainsi que l’unité de mesure.
Nous allons à présent nous occuper de notre « plan de montage ».
Nous devons créer un plan (map en anglais) au sein du programme, il a la forme d’une grille.
Il doit indiquer quelles tuiles placer à quel endroit à l’aide de repères, souvent des chiffres.

On peut construire une grille de plusieurs manières, mais la plus « représentative » est un tableau à deux dimensions.
Si vous n’êtes pas à l’aise avec les tableaux je vous recommande d’aller faire un tour sur le tuto de Nataly
C’est important car si vous ne savez pas manipuler les tableaux vous ne pourrez guère aller beaucoup plus loin.

Voici une liste, ou tableau à une dimension :

var monTableau:Array = [1,0,0,0,0,0,0,1,0,1,0,1,0,1]

Une liste est un tableau simple contenant une suite de références.
Cette définition est valable en ActionScript mais pas forcément dans tous les autres langages de programmation, où une liste et un tableau peuvent parfois être deux choses un peu différentes.

Voici un tableau à deux dimensions :

var monTableau:Array = [[1,0,0,0,0,0,0,1,0,2,0,1,0,1], [1,0,2,3,2,0,0,3,3,3,0,1,0,1], [1,0,0,2,6,0,0,1,0,1,4,1,0,1], [1,1,1,0,0,0,0,1,0,1,0,1,0,1]]

Un tableau à deux dimensions est une liste de tableaux à une dimension.
Pour plus de clarté il peut s’écrire sous cette forme :

var monTableau:Array = [
    [1,0,0,0,0,0,0,1,0,2,0,1,0,1],
    [1,0,2,3,2,0,0,3,3,3,0,1,0,1],
    [1,0,0,2,6,0,0,1,0,1,4,1,0,1],
    [1,1,1,0,0,0,0,1,0,1,0,1,0,1]
     			 ]

Le tableau principal « monTableau » représente la grille entière, le plan.
Chaque index du tableau principal contient un nouveau tableau qui représente une ligne de la grille.
Chaque ligne est composée d’un certain nombre de colonnes, une par index.
Chaque case ainsi formée contient un chiffre de référence qui va servir à poser une tuile.

Comme vous pouvez le constater, écrit de cette manière c’est beaucoup plus lisible car vous voyez réellement notre grille au sein du programme. Mais sachez que si un tableau à deux dimensions est plus facile d’approche, ce n’est pas pour autant optimisé. Il est bien plus lourd pour un programme de devoir parcourir des tableaux imbriqués qu’une simple liste, mais dans le cadre d’un tutorial débutant c’est une solution passe partout.

Voyons ce que nous avons à rajouter à notre programme

Ouvrez votre projet et ajoutez un calque nommé « Tableaux » et écrivez :

var map:Array = [
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
			 [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
		 ]

Nous venons de créer un tableau à deux dimensions appelé « map ».
Il représente le plan du décor que nous voulons afficher.
Il fait 15 lignes de 20 colonnes, soit un total de 300 cases.
Chaque case contient la référence d’un modèle de tuile à afficher :

  • 0 = vide
  • 1 = bloc solide

Nous n’allons utiliser que deux types de tuiles pour le moment, vide et solide.
Ce qui nous intéresse avant tout c’est la manière dont le perso et le plan interagissent.
Nous étudions donc des types de tuiles et non les dessins qu’elles représentent pour le moment.
Nous nous pencherons sur les dessins des tuiles un peu plus tard.

3 – Eléments et affichage

Notre plan de décor est prêt, reste à présent à afficher la grille dans notre jeu.
Dans le premier exercice nous avons appris à créer et à afficher l’objet « perso ».
Globalement c’est la même procédure et cela va nous simplifier les choses.

Le conteneur

Nous allons commencer par créer un objet « grille » qui va contenir notre décor.
Cet objet est affiché sous le personnage, il nous faudra donc l’ajouter avant le personnage.
(voir : ordre de la liste d’affichage)

Editez le calque « variables » et ajoutez les lignes suivantes :

// grille d'affichage
var grille:MovieClip = new MovieClip();
addChild(grille)

J’ai choisi d’utiliser un MovieClip pour créer le conteneur, ce n’est pas forcément une très bonne idée car un Clip est bien plus gourmand en ressources qu’un Sprite. Mais cela n’a que peu d’importance à notre niveau car nos décors sont fixes, nous n’avons pas encore de scrolling, on ne va donc pas s’embêter avec ces optimisations pour le moment, n’en déplaise aux puristes.

Les tuiles

Nous avons une map et un conteneur, attaquons nous aux tuiles.

plateforme_07.jpg

Toujours selon la même méthode on va créer un clip de référence qui va contenir toutes nos tuiles.

Créez un nouveau clip exporté pour AS et ayant le nom « Tuiles ».
Le repère du clip doit se trouver à la position 0.0 du clip, soit sur son angle haut gauche.
C’est important pour la suite.

En frame 1 créez un carré blanc de 32 pixels de large et de haut.

De la même manière que l’on se sert des frames pour afficher des animations différentes du perso en fonction de ses mouvements, on va utiliser les frames du clip « Tuiles » pour afficher la tuile correspondant au numéro de référence que nous allons trouver dans la map.

Afficher la grille

On a tout ce qu’il nous faut à présent.
Un plan (map), une unité de mesure (T), un conteneur pour la grille (grille), des tuiles (Tuiles).

plateforme_08.jpg

Créez un nouveau calque nommé « Création du level » et écrivez le code suivant :

function creeDecor():void{
	for (var i:int=0; i<15; i++){
		for (var j:int=0; j<20; j++){
			if(map[i][j]!=0){
				var t:Tuiles = new Tuiles()
				t.x= j*T;
				t.y= i*T;
				t.gotoAndStop(map[i][j])
				grille.addChild(t)
			}
		}
	}
}

Cette fonction sert à afficher la grille à l’écran.
Elle parcourt la map et affiche chaque tuile au bon endroit et à la bonne frame.
Notre jeu ne va pas être constitué d’un seul et unique level, cette fonction va donc resservir plus tard.

Dans le détail tout commence par deux boucles, imbriquées.

for (var i:int=0; i<15; i++){
	for (var j:int=0; j<20; j++){
	//…
	}
}

C’est la méthode la plus classique pour parcourir un tableau à plusieurs dimensions.
La première boucle représente les lignes « i ».
La seconde représente les colonnes « j ».
Donc pour chaque ligne de la map, nous parcourons toutes les colonnes une par une.
Et pour chaque case trouvée :

if(map[i][j]!=0){}

On regarde si la valeur est différente de 0 (vide).
Nous ne nous occupons que des tuiles solides, inutile d’afficher ce qui est vide.

var t:Tuiles = new Tuiles()
t.x= j*T;
t.y= i*T;

Si la tuile existe on crée donc une occurrence « t » (pour « tuile ») du modèle.
On place cette occurrence à sa position dans la scène grâce à notre unité de mesure.

  • Numéro de la colonne multiplié par l’unité de mesure
  • Numéro de la ligne multiplié par l’unité de mesure
t.gotoAndStop(map[i][j])

Pour l’instant nous n’avons qu’un type de tuile à afficher mais par la suite on en aura bien plus.
Autant se débarrasser tout de suite de ça, le chiffre de référence nous donne la frame à afficher.

grille.addChild(t)

Il ne reste plus qu’à ajouter notre tuile à la liste d’affichage de la grille pour qu’elle soit créée.

Il ne faut pas oublier de placer l’appel à cette fonction quelque part.

Editez le calque « variables » et à la fin ajoutez le code :

// crée le décor
creeDecor()

Le perso n’est pas encore capable de détecter la grille mais elle est au moins affichée.
A ce stade vous pouvez tester le programme pour voir si la grille se construit correctement.

Dimensions du perso

Notre but est d’empêcher le perso de passer à travers le décor.
Le décor est donc une surface infranchissable pour l’objet « perso ».
Mais notre objet « perso » a été fait sans penser une seconde à son utilisation avec des tuiles.
La première chose à faire est donc de redéfinir les dimensions exactes de notre objet « perso ».

Il faut trouver une taille qui va simplifier les calculs, l’idéal c’est qu’il corresponde à l’unité de mesure.
Si on regarde l’animation du perso cela ne pose pas de problème en largeur.
Le perso, même lorsqu’il court ou saute, entre facilement dans ces 32 pixels de large.
Nous allons donc dire que le perso fait 32 pixels de large, ou T, la taille d’une case.

En hauteur c’est un peu différent, notre perso ne tient pas dans 32 pixels de haut.
On va donc devoir utiliser deux cases, soit 64 pixels pour la hauteur même si le perso ne remplit pas tout l’espace de la case du haut, nous verrons plus tard comment affiner un peu la hauteur réelle.

La taille de l’objet perso est donc pour le moment de 32*64 pixels ou 1*2 cases.

plateforme_09.jpg

Pour nous permettre de visualiser correctement ce que nous allons faire vous allez éditer l’objet « Perso » qui se trouve dans la bibliothèque.

Ajoutez un calque sous les frames de l’animation du perso.
Créez un bloc blanc de 32*64 pixels.
Placez le bloc à -16*-32 pixels.
Rappel, le repère du perso est au centre du perso.

Ok nous avons à présent un repère visuel un peu plus fiable que l’animation.
Il représente la taille réelle du bloc contenant l’animation du perso.
C’est lui qui va nous permettre de déterminer les collisions.

4 – Déplacements et collisions

Nous allons entamer la partie la plus complexe de tout le programme, les collisions.
Nous allons bannir l’utilisation de « hitTest » au profit de quelques formules simples.
Ne râlez pas les non matheux, avec un petit effort de réflexion vous allez vous affranchir de bien des cauchemars de tâtonnement et d’imprécision auxquels vous vous seriez confrontés autrement.

Avant tout une collision c’est quoi ?
Une collision c’est lorsque deux objets occupent tout ou partie d’un même espace.
On dit de deux objets qu’ils sont en collision lorsque les deux sont en contact.
Donc, qu’au moins un pixel d’un objet A se trouve à l’intérieur de la surface occupée par un objet B.

Isoler les déplacements

Nous voici dans le vif du sujet.
Jusqu’à présent nos déplacements étaient gérés dans la fonction « moteur » par ces lignes :

// vertical
if (perso.y>=300) {
	perso.y = 300;
} else {
	gravite++;
}
 
// déplacements           
if (!vise) perso.x += vitesse*sens;// horizontal
perso.y += gravite;// vertical

Je vous propose de séparer clairement les déplacements et le reste du moteur.
En effet les déplacements du personnage sont une partie du moteur mais pas tout le moteur.
Ce dernier se charge aussi de gérer les mouvements et un tas d’autres choses.

Dans la fonction « moteur » remplacez ces lignes par la suivante :

deplacement(sens)

Tout va se passer à présent dans cette nouvelle fonction « deplacement » que nous allons créer.
Notez au passage que nous lui passons le paramètre « sens » qui donne la direction du déplacement.

On prend une profonde inspiration et on se prépare à attaquer les déplacements et collisions.

Créez un nouveau calque « Déplacements » et créez une fonction nommée « deplacement ».

Cette fonction accepte un paramètre « dx » équivalent à « sens », c’est un nombre entier.
Notez le choix des noms de mes variables, pour “dx” par exemple, “d” pour direction et “x” pour l'axe.
Le choix du nom des variables est très important, il doit permettre de savoir rapidement de quoi on parle.
Par habitude j'évite les nom du genre “directionSurX”, je leur préféré un simple “dx”, plus simple à utiliser au sein d'un calcul ou d'une longue fonction.

Latéral et collisions

Sachez qu’il est préférable de traiter les déplacements un axe après l’autre.
C'est-à-dire d’abord les déplacements horizontaux puis les déplacements verticaux.
Pour l’instant nous ne nous occupons que des déplacements horizontaux.

Cette opération nécessite trois étapes :

plateforme_10.jpg

D’abord on déplace le perso, puis on vérifie si il y a collision auquel cas on le replace devant la tuile.

Ecrivez la fonction suivante :

function deplacement(dx:int):void{
 
	perso.x += vitesse*dx; 
	var Y:Number = perso.y
	var X:Number = perso.x
	var C:int = (X+16*dx)/T;	
	var L:int = (Y-16)/T;
 
	// latéral							
	for (L; L<Y/T+1; L++) {
		if (map[L][C]==1) {
			X = perso.x = C*T+16-T*dx;
		}
	}
}

Etudions ce code pas à pas.

function deplacement(dx:int):void{}

La fonction accepte le paramètre « dx » qui équivaut à la direction du perso sur X.

plateforme_11.jpg

perso.x += vitesse*dx;

On déplace le perso de la vitesse multipliée par la direction.
Dans notre cas la vitesse est de 4 et la direction de 1,-1 ou 0.

var Y:Number = perso.y
var X:Number = perso.x
var C:int = (X+16*dx)/T;
var L:int = (Y-16)/T;

On défini 4 variables.
Les deux premières n’ont qu’un seul but, simplifier l’écriture des formules.

  • Y représente la position du perso sur l’axe Y.
  • X représente la position du perso sur l’axe X.

Les deux lignes suivantes représentent la ligne et la colonne que nous souhaitons tester.
L pour ligne et C pour colonne, nous donne donc la case à tester dans le tableau.
Ce que nous souhaitons tester c’est les bords du rectangle qui représente le perso.

var C:int = (X+16*dx)/T;

On cherche à savoir quelle est la colonne à tester après le déplacement sur X.
C = (position du perso sur X+16*sens du déplacement), et on divise le tout par T (norme).

Attention, souvenez-vous que, contrairement aux tuiles, le repère de notre perso est au centre.
On doit donc apporter une correction pour trouver le bord exact du rectangle à tester.

plateforme_12.jpg

dx est la direction du perso, elle est égale à 1,0 ou -1
16 c’est la distance qui sépare le centre du perso d’un bord.

var L:int = (Y-16)/T;

L représente la ou les lignes à tester, nous y reviendrons.

// latéral							
for (L; L<Y/T+1; L++) {}

Ok pas d’affolement ce n’est qu’une simple boucle.
Ici on doit tester plusieurs lignes car notre perso peut occuper plusieurs cases en hauteur.
On va donc faire une boucle entre la position la plus haute et la plus basse.
Soit trois lignes à tester au maximum.

plateforme_13.jpg

La case du haut est trouvée de la manière suivante :

L=(Y-16)/T

Ce n’est pas logique ?
C’est normal.

Si vous avez tout bien suivi la case haute devrait être obtenue de cette manière : L=Y/T-1
Oui mais, rappelez-vous que notre perso ne remplit pas toute la case du haut en hauteur.
C’est ici que nous allons pouvoir affiner un peu la détection de collision.

plateforme_14.jpg

Modifiez le clip « Perso ».

Tracez une ligne rouge qui sépare exactement en deux la case supérieure du repère du perso.
Coup de bol (?), la ligne passe exactement sur la casquette du perso.
Cette ligne se trouve exactement à -16 pixels de haut du centre du perso.
Là du coup ça devient tout à fait logique non ?

Ce qui donne :

plateforme_15.jpg

La case basse est obtenue à l’aide de :

L<Y/T+1

Ici on cherche à trouver la case la plus basse occupée par le perso : Y/T
La valeur à ne pas dépasser pour la boucle est donc Y/T +1 case.

Quelques explications sont cependant nécessaires (merci Lilive de l'avoir vu).

Il y a deux cas de figure :

Le premier se produit lorsque la position du personnage est un multiple de la norme.
Dans ce cas le personnage est “calé” sur la grille et nous n'avons que deux cases à tester.

plateforme_special_01.jpg

La haute (Y-16)/T et la basse (Y/T).
Dans ce cas la valeur maximale de la boucle devrait être :

L<int(Y/T+1)

Soit une case en dessous de la dernière ligne testée : Y/T+1

Mais si le personnage est légèrement plus bas sur l'axe Y alors nous n'avons plus deux mais trois cases à tester.

plateforme_special_02.jpg

La haute (Y-16)/T, la basse (Y/T) et celle qui se trouve tout en bas (Y/T+1).
Et dans ce cas la condition de la boucle devrait être :

L<=int(Y/T+1)

Soit une case en dessous de la dernière ligne testée : Y/T+2

Pour éviter de multiplier les tests, car ce sont deux tests bien différents, l'astuce est de ne pas utiliser un entier pour calculer L, mais la position réelle du personnage sur Y.

L<Y/T+1

plateforme_16.jpg

if (map[L][C]==1) {
...
}

Si la case testée est un bloc solide.
Il y a collision car le bord testé se trouve à l’intérieur d’une tuile solide.
Il nous faut ramener le bord du perso contre le bord de la tuile pour l’empêcher d’aller plus loin.

plateforme_17.jpg

X = perso.x = C*T+16-T*dx;

C*T nous donne la position de la colonne concernée par la collision.
16 est le décalage entre le bord et le centre du perso.
T*dx nous donne la case à ajouter ou retirer en fonction du déplacement.
On en profite pour modifier la valeur de X puisque le personnage a bougé.

Dit autrement, si le perso se dirige à droite et que le bord droit entre en collision avec un mur.
On replace le perso sur la case qu’il occupe actuellement -16 pixels (pour le centre)
Si le perso se dirige vers la gauche et que le bord gauche entre en collision avec un mur.
On replace le perso sur la case qu’occupe le mur +1 case + 16 pixels (pour le centre)

Tomber et collisions

Ce n’est pas bien compliqué si vous avez compris le chapitre précédent.
La grosse différence c’est que le déplacement sur l’axe Y est déterminé par la gravité.
Nous avons déjà vus comment gérer la gravité, je vous fais donc grâce des détails à ce niveau.
Notez également qu'à ce stade le personnage s'est déjà déplacé sur X et a été replacé au bon endroit.
Il est important de savoir où se trouve votre personnage au moment où vous effectuez un test.
De même la valeur de L a été modifiée par les tests de déplacement sur X.

Rappel : notre condition de déplacement sur X pour trouver la colonne ne devait pas dépasser L<Y/T+1, elle s'arrêtait lorsque L avait atteint cette valeur. L désigne donc à présent soit la case située immédiatement sous le joueur soit la case qui se trouve encore en dessous, c'est important comme vous le verrez par la suite.

plateforme_18.jpg

Modifiez la fonction « deplacement » en y ajoutant le code suivant :

Y = perso.y += gravite; 
 
// tombe
for (C=(X-16)/T; C<(X+16)/T; C++) {	
	if (map[L][C]==1 && Y>=(L-1)*T) {	
		perso.y = (L-1)*T;
		gravite = -int(haut)*11;
		return;				
	}
}
 
if (gravite++>T) gravite=T;

Voyons ce code dans le détail.

plateforme_19.jpg

Y = perso.y += gravite;

La position du perso sur l’axe Y s’incrémente de la gravité.
Ne pas oublier de modifier également la valeur de Y pour nos calculs.

for (C=(X-16)/T; C<(X+16)/T; C++) {}

C’est une boucle, identique à celle que nous avons vue pour les déplacements sur l’axe X.
Cette fois on va chercher à tester toutes les colonnes où se trouve le perso, deux au maximum.

plateforme_20.jpg

if (map[L][C]==1 && Y>=(L-1)*T) {}

La première partie de cette condition vous la connaissez, c’est la même que sur l’axe X.
On vérifie que la valeur comprise dans la case est bien un bloc solide.
La seconde partie en revanche peut vous paraître plus obscure.

perso.y>=(L-1)*T

Cette formule sert essentiellement à laisser le perso finir son mouvement vers le bas.
Sans elle votre personnage va se coller au bas de la tuile alors qu’il commencé à peine à y entrer.
On s’assure donc que le perso est bien au delà de la limite de la tuile avant de le replacer.

Rappelez-vous que L peut prendre deux valeurs, celle de la tuile située sous le perso et celle de la tuile située encore en dessous. Si on effectue pas ce test, lorsque le personnage descend vers la tuile et qu'il teste la collision deux tuiles en dessous de son centre et que cette tuile est un solide, la collision est détectée alors que le personnage n'a pas encore les pieds posés sur le sol. Résultat il se colle au sol sans terminer son déplacement proprement.

plateforme_21.jpg

perso.y = (L-1)*T
gravite = -int(haut)*11
return

Si la collision à lieu, on replace le perso à la bonne position.
C'est-à-dire juste au dessus de la case où il y a eu collision.

Puis on modifie la gravité pour éviter que le perso continue à tomber indéfiniment.
Si la touche « haut » n’est pas enfoncée la gravité vaut 0.
Si elle enfoncée la gravité vaut -11 pixels, le perso saute.

Enfin on va stopper l'exécution du reste de la fonction à l’aide de l’instruction « return ».
Cette instruction est très importante dans la construction de notre code.

« return » est une instruction qui arrête l'exécution de la fonction en cours et renvoie à la fonction appelante. Elle peut ou non retourner une valeur, dans le cas qui nous occupe elle ne retourne rien et ne “sert” qu'à arrêter l'exécution de la fonction deplacement. Ce qui implique que si le perso touche le sol, l'exécution de la fonction déplacement est aussitôt interrompue, y compris la boucle en cours.
C’est une manière très simple de stopper l'exécution d’une fonction lorsqu’on en a besoin.

Mais pourquoi négliger la lecture du reste du code si on touche le sol ?
C’est une astuce pour éviter de multiplier les tests et les conditions.
Tant que notre fonction « deplacement » ne rencontre pas un « return » elle lit la suite.
Sinon elle s’arrête pour reprendre au début au prochain pas du programme.

Or vous remarquerez la dernière ligne de la fonction :

if (gravite++>T) gravite=T;

Déjà elle est écrite de manière étrange mais nous allons voir ça plus tard.
Globalement elle permet d’incrémenter la gravité.
Le « return » est donc indispensable, car la gravité ne s’incrémente que si on ne touche pas le sol.

Petit aparté sur l’écriture d’un code.

Voyons d’un peu plus près cette ligne qui ne ressemble à rien de ce que nous avons déjà vue.

Classiquement on l’écrirait :

if (gravite>T) {
 	gravite=T
} else {
 	gravite++
}

Mais je souhaite en profiter pour vous montrer qu’il existe de nombreuses manières de rédiger un code, certaines permettent de réduire considérablement sa longueur avec un peu d’habitude. L’astuce c’est de faire comme dans votre langue maternelle, essayer de construire des phrases.

Si ( la gravité est supérieure à l’unité de mesure ) {
	La gravité est égale à l’unité de mesure
} sinon {
	La gravité s’incrémente
}

Au passage vous remarquerez qu'il s'agit là d'un algorithme. C'est plus ou moins ainsi qu'on conçoit un programme, d'abord dans sa langue maternelle pour qu'il soit logique et bien construit, puis on le traduit en instructions.

Ce qu'on peut remarquer avec cet “algorithme” c'est qu'il n'est pas très pratique à lire.
On pourrait certainement trouver une formulation plus jolie et plus courte.

Tout d’abord les accolades, souvent on peut s’en passer car elles servent à encadrer des blocs.

Si (la gravité est supérieure à l’unité de mesure) la gravité vaut l’unité de mesure sinon elle s’incrémente

Cette écriture est donc tout à fait correcte et fonctionne :

if (gravite>T) gravite=T else gravite++

Une seule à lire ligne au lieu de 5, c’est mieux, mais ça pourrait être encore plus lisible.
Il faut simplement savoir que l’opérateur « ++ », à droite d'une variable, modifie la variable considérée en lui ajoutant 1, ce qui n'est pas pareil que d'écrire par exemple “gravite+1” car dans ce cas la variable n'est pas modifiée, on calcule juste un résultat. L'intérêt ici est donc de modifier une variable et de prendre en compte le résultat de cette modification avant la condition.
Cela fait toute la différence car on peut alors simplifier cette ligne par :

if (gravite++>T) gravite=T;
Si (la gravité incrémentée de un est supérieure à l’unité de mesure) la gravité vaut l’unité de mesure.

Cette fois on a une phrase construite et courte, ce qui la rend logiquement plus lisible.
La variable est directement incrémentée dans la condition, que cette dernière soit vraie ou fausse.
Ce qui, avec un peu d’habitude, est beaucoup plus clair.

Cependant l’ablation des accolades et la mise en forme sur une ligne va à l’encontre des habitudes de la plupart des développeurs qui se sont faits aux conventions en usage. Cela demande donc un petit effort de concentration au début pour aller au-delà de ses habitudes.

Bien sur vous choisissez l’écriture qui vous convient le mieux, ce petit aparté n’est là que pour vous montrer qu’en réfléchissant un peu vous pouvez grandement alléger l’écriture de vos codes.

En revanche, en règle générale on commence par créer les algorithmes dans sa propre langue.
On les triture jusqu'à ce qu'ils nous semblent à la fois justes et très clairs.
Enfin on traduit le tout dans le langage de programmation.

Cette fois encore nous travaillons un peu à l'envers ;-) Retenez cependant qu'un code très long et peu lisible est souvent le signe d'une faiblesse dans sa préparation. Si vous développez un peu à la volée et que vos codes commencent à vous sembler bien compliqués, penchez vous un peu sur votre algorithme, c'est à dire la suite d'instructions logiques qui vous mène à la réponse.

Revenons à nos moutons, cette dernière ligne a donc deux buts, incrémenter la gravité et limiter sa valeur à la taille d’une tuile. C’est très important, car si la vitesse de déplacement du perso est plus grande que la taille d’une tuile (votre unité de mesure) vous avez de fortes chances de passer au travers sans la voir.

Sauter et collisions

Si vous avez compris la descente il n’y a pas de raison pour que la montée soit plus complexe.

plateforme_22.jpg

Modifiez la fonction « deplacement ».

Insérez le code suivant au dessus de la ligne qui incrémente la gravité.

// touche plafond
if (gravite<0) {					
	L = (Y-16)/T;					
	for (C=(X-16)/T; C<(X+16)/T; C++) {	
		if (map[L][C]>0) {		
			perso.y = (L+2)*T-16
			gravite=1;
		}
	}
}

Nous avons vu comment sauter dans l’exercice précédent.
Que se passe-t-il quand le perso touche un plafond, donc un bloc solide lorsqu’il est en train de sauter

plateforme_23.jpg

if (gravite<0) {}

Si la gravité est inférieure à 0, le personnage est en train de monter, donc en train de sauter.

L = (Y-16)/T;

On cherche la ligne à tester, dans notre cas rappelez-vous que le haut du perso ne coïncide pas avec le haut du bloc, il se trouve 16 pixels plus bas, c'est-à-dire à la moitié du bloc.

for (C=(X-16)/T; C<(X+16)/T; C++) {}

C’est un classique, on cherche les colonnes à l’aide d’une boucle qui vérifie les bords du perso.

plateforme_24.jpg

if (map[L][C]>0) {}

Cette fois on ne va pas spécifier que la valeur de la tuile est de 1 mais qu’elle est supérieure à 0.
Nous allons utiliser différentes tuiles dans le jeu et le perso se déplace pour l'instant sur les tuiles vides.

plateforme_25.jpg

perso.y = (L+2)*T-16
gravite=1;

On replace le perso, le haut du perso correspond au bas du bloc solide.
On indique que la gravité est de 1 donc que le perso retombe.
Là on ne laisse pas le choix au joueur de pouvoir ressauter : s’il touche le plafond, il retombe.
Cette fois pas de « return », la gravité réagit normalement.

Conclusion de l'étape

C’est déjà la fin de cette étape, comme vous pouvez le constater tout ceci tient en très peu de lignes et permet déjà de faire un paquet de choses.

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

Comme le code est assez simple j’en ai profité pour introduire tout de suite quelques notions bien utiles pour la création d’un programme. Des raccourcis, des solutions pour alléger votre code, pour le rendre plus lisible, plus logique, et pour qu’il ressemble moins à un « assemblage de gros pavés ».

C’est très important, vous devez alléger vos codes au maximum sans détériorer leur efficacité, et bien souvent ça passe par quelques formules bien trouvées. J’ai passé quelques heures à me creuser la cervelle pour que mon code tienne sur le moins de lignes possibles tout en restant cohérent. On pourrait certainement alléger encore en structurant autrement, mais j’ai mes propres limites et il faut que vous puissiez suivre même si vous êtes débutants. Il est d'ailleurs préférable de ne pas non plus trop raccourcir les choses, un code plus court (en nombre de lignes) n'est pas forcément le synonyme d'un code optimisé, prenez garde à ce que l'optimisation prime sur la longueur du code.

Globalement je vous recommande d'éviter les longues conditions et les accumulations de tests en tous genres en vous appuyant sur la logique du programme. Dit clairement, l’astuce c’est « ne pas dire en 12 mots ce que l’on peut dire en 3 »

Vous remarquerez également qu’aucune fonction ne dépasse la cinquantaine de lignes.
Il ne faut pas hésiter à subdiviser votre programme en petites fonctions indépendantes.
C’est beaucoup plus clair et cela permet de localiser rapidement et efficacement les bugs.

Enfin pour en revenir au jeu en lui-même, on ne peut pas dire que la construction que j’ai choisie soit la plus performante, disons que c’est la méthode classique, mais il existe bien des solutions pour accélérer l'exécution du code, même si pour le moment ce n'est pas très important à notre niveau.

On pourrait par exemple utiliser des « bitwise », des opérations binaires, pour accélérer les calculs, mais c’est déjà un cran au dessus et je crois que ce n’est pas encore le moment d’en parler, plus tard sans doute.

Pour les collisions une autre méthode serait d’exploiter un peu plus ce que permet l’AS en utilisant la classe « rectangle » et l’instruction « intersect ».

Beaucoup de chemins mènent à Rome, mais Rome ne s’est pas faite en un jour et nous avons le temps de revenir sur les optimisations possibles et les diverses voies que chacun choisit de suivre. Gardez simplement à l'esprit qu'un jeu complet demande beaucoup de ressources pour fonctionner correctement et qu'il vous faut porter une grande attention à ce que vos programme soient les moins gourmands possibles.

Vous avez à présent une base pour commencer à faire des petits jeux.
Si vous avez tout bien assimilé nous allons pouvoir passer à la vitesse supérieure.

Les sources