• Difficile

Mis à jour le 06/12/2013

Simple personnage

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous allons voir comment est défini un personnage, comment simplifier sa gestion, et voir comment l'insérer dans un monde simple.

Qu'est-ce qu'un personnage ?

Avant de se lancer dans du code, essayons de définir ce qu'est un personnage dans un petit jeu de plateforme, ou un jeu vu de dessus.

Il est important de définir quelques notions avant d'aller plus loin. Dans un jeu comme Mario Bros, Mario est dans le monde, et il bouge.

Ce mot bouge n'est pas précis. En réalité, le personnage bouge de deux manières indépendantes : l'animation, et le déplacement.

L'animation

Pour bien comprendre ce qu'on appellera l'animation, prenons n'importe quel GIF animé du net, sans couleur transparente derrière :

Image utilisateur

Un très joli petit Mario animé...

Que remarque-t-on ?

Nous remarquons que le Mario "bouge" à l'intérieur d'un carré vert (qui est sa boîte englobante), qui, elle, reste fixe.
J'ai désactivé la transparence du gif pour bien voir cette boîte verte.

Ce qui se passe à l'intérieur de cette boîte verte est l'animation.
Ce sont les pieds de Mario qui bougent, c'est un joli dessin qui s'anime...

Le déplacement

Dans un jeu, l'animation ne suffit pas, il faut qu'on puisse déplacer notre personnage. Concrètement, il faut faire bouger notre boîte verte, la faire avancer dans le monde, tout simplement.

Tous les personnages des jeux dont nous parlons sont animés, et se déplacent.
Mario en fait partie.

  • Parfois, l'animation est réduite à une seule image, donc il n'y a finalement pas d'animation. C'est le cas d'un nuage qui se déplace, d'un missile qui se déplace. Cela est de plus en plus rare, car on animera le missile (en le faisant tourner, en animant le réacteur...) pendant son déplacement pour un meilleur esthétisme. Qui a dit qu'un missile ne pouvait pas être esthétique ? :-°

  • Parfois, le sprite ne bouge pas, mais s'anime : c'est le cas de tous les gifs animés du net par exemple. C'est le cas d'un Ryu qui danse en garde avant le FIGHT !

Première approche de collision avec le décor

Nous allons maintenant parler collisions.

Notre but va être le suivant : Mario va se déplacer dans un monde avec des murs.

Reprenons notre petit Mario animé :

Image utilisateur

Comment savoir s'il touche un mur ?

Voici deux solutions.

Le pixel perfect

Le pixel perfect est un algorithme de collision perfectionniste qui va dire :

Nous oublions donc notre boite englobante verte pour cet algorithme :

Image utilisateur

Il va falloir déterminer, pour chaque pixel, s'il touche ou non un mur.
Sachant que notre Mario est animé, il se peut qu'à une frame d'animation, il ne touche pas le mur, et qu'à une autre, il le touche.
Que faisons nous alors ? On peut le faire reculer... Du coup, s'il continue d'être animé, et qu'on le fait déplacer vers le mur, on le verra trembler, car à chaque frame de l'animation, il sera déplacé. Disons le tout de suite, ça sera moche.

  • Ce sera moche !

  • Ce sera calculatoire, car il faudra déterminer s'il y a collision pour chaque pixel. Notre Mario est petit, mais s'il était plus grand, le nombre de calculs exploserait...

  • On aura des problèmes de collisions qui dépendront de la frame d'animation en cours.

Ce sont quelques problèmes que peut soulever le pixel perfect.

Il pourrait en poser encore bien d'autres : dans un jeu du genre Zelda, si le pixel perfect était appliqué, on pourrait coincer notre bouclier entre deux branches d'arbre du décor. Pour s'en sortir, ce ne serait pas simple.
Il y a d'autres problèmes qui pourraient arriver. Le pixel perfect est - selon moi - un nid à problèmes. Évidemment, cela n'engage que moi. Il peut être utile dans certains cas, mais sûrement pas dans notre cas à nous.

Nous allons donc oublier cet algorithme pour notre sujet, il n'est pas adapté.

Collision par boite englobante (AABB)

Une Axes Aligned Bounding Box (AABB) est le nom qu'on donne à la boite englobante verte de Mario :

Image utilisateur

On la définit par son origine (le point en haut à gauche), sa largeur et sa hauteur (d'ailleurs, SDL_Rect est typiquement fait pour ça).
Elle est alignée avec les axes du monde : pas de losanges, mais un beau rectangle "droit".

On partira du principe que si la boite englobante touche un mur, alors Mario touche. Et ceci indépendamment de la frame d'animation de ce dernier. Si la boite touche, ça touche, et si elle ne touche pas, alors Mario ne touche pas.

Observez bien cette boîte englobante, elle est suffisamment serrée autour de Mario pour que cette gestion des collisions soit suffisante.

L'algorithme de collision avec le décor que nous allons voir rapidement ici ne considèrera que la boite englobante.

Voici donc notre boîte verte :

Image utilisateur
Précaution

Pour utiliser cet algo, et donc faire abstraction des frames d'animation pour les collisions, il y a une précaution à prendre au niveau de l'animation.

L'animation est définie par un ensemble de petits dessins qu'on blit à la coordonnée (x,y) voulue, on en colle une à l'autre en fonction du temps passé. C'est ça qui fait l'animation.
Il est fondamental que ces petits dessins fassent tous la même taille (la même hauteur, la même largeur) : ainsi, la taille de la boîte englobante reste invariante d'une frame à l'autre.
C'est le cas de notre petit Mario. Vous pouvez constater sur le petit gif animé que si Mario bouge, la boîte verte, elle, reste fixe.

Sans cette contrainte, la boîte englobante se déformerait lors de l'animation, et, selon la frame, le rectangle, sans se déplacer, pourrait être dans le mur pour une frame, hors du mur pour une autre.
En garantissant les images de la même taille pour chaque frame, ce problème n'existe plus : soit le personnage est dans un mur, soit il ne l'est pas, et cela pour toutes ses animations.

Principe dans un monde

Notre boîte verte se déplace dans un monde, il faut simplement empêcher qu'elle rentre dans un mur.
Le principe est simple.

Nous définissons une fonction fondamentale, qu'on appellera CollisionDecor.
Cette fonction prend la boîte verte (sa position, sa largeur, sa hauteur), et nous dit simplement :

  • tu es dans un mur ;

  • tu n'es pas dans un mur.

Informatiquement parlant, elle renvoie 1 si on touche un mur, 0 sinon.

Tel un aveugle qui se ballade dans la rue, on veut juste savoir, à tout moment, si on touche un mur ou non. A partir de cette seule fonction, on va mettre en place nos collisions.

Algorithme de déplacement

Nous souhaitons déplacer notre boîte verte dans le décor.
Dans cette partie, nous allons voir comment faire, en faisant intervenir notre fonction CollisionDecor.
Voici le schéma suivant :

Image utilisateur

Au départ, nous avons notre boîte verte claire, qui est hors mur. En effet, il est interdit d'être dans un mur. La position initiale doit donc être hors mur.

Nous allons déplacer notre boîte verte selon un vecteur de déplacement (qui est rouge sur l'image ci dessus). Le but est que le modèle se déplace, mais qu'à sa position finale, il soit toujours hors mur.

Voici une première version de l'algorithme.

  • La boîte verte claire est a une position initiale hors mur, nous donnons le vecteur de déplacement souhaité.

  • Nous calculons l'éventuelle position finale (verte foncée).

  • Nous demandons à CollisionDecor si la boîte verte foncée est dans le mur ou non.

  • Si elle ne l'est pas, on valide le déplacement : notre boîte verte claire prend la place de la boîte verte foncée (cas A).

  • Sinon, nous ignorons le déplacement, on ne bouge pas notre boîte verte (cas B).

  • Dans tous les cas, nous sommes toujours hors mur à ce moment là.

Avec cet algorithme, le cas A et le cas B fonctionnent : on se déplace si on peut, on ne bouge pas si on ne peut pas.

Le cas C

Le cas C est plus complexe. Notre rectangle vert clair ne touche pas le mur, mais n'est pas collé contre non plus. Si on souhaite le faire bouger selon le vecteur rouge, la nouvelle position calculée rentrera dans le mur (cas C(a)), ce n'est pas bon, donc ce n'est pas validé. L'algorithme ci-dessus ne fera donc pas bouger du tout le rectangle vert clair -> nous nous retrouverons dans le cas B, sauf que nous ne sommes pas collés au mur.

L'idée est que si le mouvement nous amène dans le mur, de voir si on ne pourrait pas modifier le vecteur rouge de façon à faire un plus petit mouvement pour s'en approcher au plus près. Le mieux étant d'aller le toucher, se coller à lui au pixel près, mais sans qu'il y ait collision. Nous aurons alors le cas C(b). J'appellerai cette opération Affiner le mouvement.

Voici donc une deuxième version de l'algorithme.

  • La boîte verte claire est à une position initiale hors mur, nous donnons le vecteur de déplacement souhaité.

  • Nous calculons l'éventuelle position finale (verte foncée).

  • Nous demandons à CollisionDecor si la boîte verte foncée est dans le mur ou non.

  • Si elle ne l'est pas, on valide le déplacement : notre boîte verte claire prend la place de la boîte verte foncée (cas A).

  • Sinon, nous sommes dans le cas B ou le cas C.

    • Nous cherchons un vecteur affiné, qui permettrait d'aller se coller contre le mur, en faisant des essais, et en retestant avec CollisionDecor.

    • Si ce vecteur est nul (on est déjà collé sur le mur) alors on ne bouge pas (cas B).

    • Sinon on se déplace de ce vecteur, et on se retrouve collé au mur (sans rentrer dedans bien sûr !) et on valide le déplacement (cas C(b)).

  • Dans tous les cas, nous sommes toujours hors mur à ce moment là.

Nous proposerons plus loin des méthodes pour affiner.

Code exemple

Maintenant que nous avons vu le principe que nous allons mettre en place, voici un code qui va illustrer tout cela, dans un monde très très simple pour commencer !
Ce monde sera juste un rectangle marron...

Prenez le projet prog4, et compilez le, et lancez le.

Si on regarde le main, dans prog4.c :

#include "fevent.h"
#include "fsprite.h"

void RecupererVecteur(Input* in,int* vx,int* vy)
{
	int vitesse = 5;
	*vx = *vy = 0;
	if (in->key[SDLK_UP])
		*vy = -vitesse;
	if (in->key[SDLK_DOWN])
		*vy = vitesse;
	if (in->key[SDLK_LEFT])
		*vx = -vitesse;
	if (in->key[SDLK_RIGHT])
		*vx = vitesse;
}

void Evolue(Input* in,SDL_Rect* mur,Sprite* perso)
{
	int vx,vy;
	RecupererVecteur(in,&vx,&vy);
	DeplaceSprite(perso,mur,vx,vy);
}

int main(int argc,char** argv)
{
	SDL_Rect mur;
	Sprite* perso;
	SDL_Surface* screen;
	Input in;
	memset(&in,0,sizeof(in));
	SDL_Init(SDL_INIT_VIDEO);		// prepare SDL
	screen = SDL_SetVideoMode(800,600,32,SDL_HWSURFACE|SDL_DOUBLEBUF);
	mur.x = 450;
	mur.y = 100;
	mur.w = 100;
	mur.h = 200;
	perso = InitialiserSprite(101,150,50,100);
	while(!in.key[SDLK_ESCAPE])
	{
		UpdateEvents(&in);
		Evolue(&in,&mur,perso);
		SDL_FillRect(screen,NULL,0);  // nettoie l'ecran en noir
		SDL_FillRect(screen,&mur,0x800000);  // affiche le mur
		AfficherSprite(perso,screen);
		SDL_Flip(screen);
		SDL_Delay(5);
	}
	LibereSprite(perso);
	SDL_Quit();
	return 0;
}

On voit dans le main que je crée un SDL_Rect, que j'appelle mur. Ce sera mon décor. Je lui donne une position, et des dimensions.
Puis j'initialise un sprite, c'est à dire un objet mobile. Nous verrons plus loin par quoi est défini ici un sprite pour le moment.

dans la boucle, on "Evolue", puis on affiche le mur, et le sprite.

Evolue

La fonction Evolue, juste au dessus, fait 2 choses : d'abord, elle récupère un vecteur de déplacement via la fonction RecupererVecteur qui est au dessus, puis elle déplace le sprite selon ce vecteur.

RecupererVecteur

Cette fonction est très simple, elle lit les touches du clavier (les flèches) et met un vecteur à jour. 8 positions possibles (les 4 directions ainsi que les diagonales) ainsi que le vecteur nul possible.

Passons maintenant au fichier fsprite.h

#include <sdl/sdl.h>

#pragma comment (lib,"sdl.lib")      // ignorez ces lignes si vous ne linkez pas les libs de cette façon.
#pragma comment (lib,"sdlmain.lib")

typedef struct
{
	SDL_Rect position;
} Sprite;

Sprite* InitialiserSprite(Sint16 x,Sint16 y,Sint16 w,Sint16 h);
void LibereSprite(Sprite*);
int DeplaceSprite(Sprite* perso,SDL_Rect* mur,int vx,int vy);
void AfficherSprite(Sprite* perso,SDL_Surface* screen);

Vous voyez qu'actuellement, un sprite, ce n'est qu'un SDL_Rect.
On peut l'initialiser, le libérer quand on a fini, puis le déplacer, et l'afficher.

Passons a fsprite.c

#include "fsprite.h"

#define SGN(X) (((X)==0)?(0):(((X)<0)?(-1):(1)))
#define ABS(X) ((((X)<0)?(-(X)):(X)))

Sprite* InitialiserSprite(Sint16 x,Sint16 y,Sint16 w,Sint16 h)
{
	Sprite* sp = malloc(sizeof(Sprite));
	sp->position.x = x;
	sp->position.y = y;
	sp->position.w = w;
	sp->position.h = h;
	return sp;
}
void LibereSprite(Sprite* sp)
{
	free(sp);
}

int CollisionDecor(SDL_Rect* m,SDL_Rect* n)
{
	if((m->x >= n->x + n->w) 
		|| (m->x + m->w <= n->x) 
		|| (m->y >= n->y + n->h) 
		|| (m->y + m->h <= n->y) 
		) 
		return 0; 
	return 1; 
}

int EssaiDeplacement(Sprite* perso,SDL_Rect* mur,int vx,int vy)
{
	SDL_Rect test;
	test = perso->position;
	test.x+=vx;
	test.y+=vy;
	if (CollisionDecor(mur,&test)==0)
	{
		perso->position = test;
		return 1;
	}
	return 0;
}

void Affine(Sprite* perso,SDL_Rect* mur,int vx,int vy)
{
	int i;	
	for(i=0;i<ABS(vx);i++)
	{
		if (EssaiDeplacement(perso,mur,SGN(vx),0)==0)
			break;
	}
	for(i=0;i<ABS(vy);i++)
	{
		if (EssaiDeplacement(perso,mur,0,SGN(vy))==0)
			break;			
	}
}

int DeplaceSprite(Sprite* perso,SDL_Rect* mur,int vx,int vy)
{
	if (EssaiDeplacement(perso,mur,vx,vy)==1)
		return 1;
	/*Affine(mur,perso,vx,vy);*/
	return 2;
}

void AfficherSprite(Sprite* perso,SDL_Surface* screen)
{
	SDL_Rect copyperso;
	copyperso = perso->position;
	SDL_FillRect(screen,&copyperso,0x00FF00);  // affiche le perso
}

Regardons tout d'abord les fonctions les plus simples :
InitialiserSprite,LibereSprite ne devraient pas poser de soucis.

AfficherSprite contient une légère astuce : au lieu de passer directement le SDL_Rect du sprite à la fonction SDL_FillRect, je passe une copie.
Tout comme SDL_BlitSurface, si vous ne passez pas une copie, vous risquez d'avoir des problèmes si vous faites sortir votre sprite à gauche ou en haut de l'écran. Passer une copie permet de ne pas avoir ce problème.

DeplaceSprite

Nous voila à la fonction la plus complexe de ce programme.

Tout d'abord, la fonction lance EssaiDeplacement. Si cet essai est bon, on sort de la fonction. Sinon, on ne fait rien car la suite est commentée.

EssaiDeplacement

La fonction EssaiDeplacement va essayer de déplacer le sprite selon le vecteur donné. Je dis essayer, car elle prend le décor en paramètres (ici un simple mur), et si le déplacement nous amène dans un mur, elle ne déplace pas et renvoie 0, comme on a vu plus haut dans le cas B, ou le cas C(a).
Si elle arrive à nous déplacer (cas A), elle met à jour perso->position et retourne 1 pour dire qu'elle a réussi.

CollisionDecor

La fonction CollisionDecor est l'algorithme de collision AABB que je détaille ici.
Elle renvoie 1 si et seulement si les deux rectangles se chevauchent.

Allez vous coller contre le rectangle marron. Vous pouvez constater que si vous arrivez par la gauche, vous ne pouvez pas vous coller au pixel près.
Pire, lorsque vous êtes presque collé, si vous appuyez en même temps à droite et en haut, votre sprite ignore l'appui sur le haut, alors que vous pourriez monter.
Dans ce dernier cas, vous êtes dans le cas C(a) et il n'y a pas d'affinage.

Reprenons le code. Regardez la fonction DeplaceSprite.
Vous pouvez constater qu'une ligne est commentée :

/*Affine(mur,perso,vx,vy);*/

Décommentez les et relancez le programme.
Oh miracle, vous pouvez maintenant aller vous coller au pixel près, et glisser sur le mur.

La fonction Affine

La fonction affine va juste prendre le vecteur, et tenter de s'approcher pixel par pixel en X d'abord, puis en Y ensuite.
La macro ABS renvoie la valeur absolue de la valeur passée
la macro SGN renvoie 1 si la valeur est positive, -1 si elle est négative (0 si elle est nulle)

La fonction Affine va faire des essais pixel par pixel jusqu'à aller se coller contre.
C'est le cas C(b).

A la fin de cette première partie, nous avons fait les premiers pas vers la collision avec un décor. Ce décor-ci était très simple. Cependant, nous verrons que même si le décor est complexe, même s'il y a scrolling, et même si nous ne contrôlons pas directement le vecteur de déplacement lorsque c'est une fonction de gestion de physique qui le fait, le concept restera le même.

Nous allons progressivement parler de tout ça, en ajoutant, étape par étape, les nouveaux éléments.

Exemple de certificat de réussite
Exemple de certificat de réussite