Navigation

Tutoriel précédent : éditeur de niveaux   Sommaire   Tutoriel suivant : insertion dans un monde de tuiles

II. Introduction

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

III. 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 plate-forme, 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.

III-A. L'animation

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

Mario animé

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…

III-B. 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.

Il est important de faire la différence entre déplacement et animation !

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 ? Image non disponible
  • 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 !

IV. 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.

Il ne faut pas que Mario passe à travers les murs…

Reprenons notre petit Mario animé :

Image non disponible

Comment savoir s'il touche un mur ?

Voici deux solutions.

IV-A. Le pixel perfect

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

Si un seul pixel de Mario touche un mur, alors il y a collision.

Nous oublions donc notre boite englobante verte pour cet algorithme :

Mario animé

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 étape de l'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 image 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 l'étape de l'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é.

IV-B. Collision par boîte englobante (AABB)

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

Mario animé

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 boîte englobante touche un mur, alors Mario touche. Et ceci indépendamment de l'étape de l'animation de ce dernier. Si la boîte 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 boîte englobante.

On oublie donc l'animation de Mario, on ne parle plus que de la boîte !

Voici donc notre boîte verte :

Boîte verte

IV-B-1. Précaution

Pour utiliser cet algorithme et donc faire abstraction des étapes de l'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 affiche à 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 image à 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 l'image, le rectangle, sans se déplacer, pourrait être dans le mur pour une image, hors du mur pour une autre.

En garantissant des boîtes de la même taille pour chaque image, 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.

IV-B-2. Principe dans un monde

Notre boîte verte se déplace dans un monde, il faut simplement empêcher qu'elle ne 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.

Cette fonction clé va faire le pont entre le personnage et le décor.

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

V. 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 utilisant notre fonction CollisionDecor.

Voici le schéma suivant :

Schéma explicatif des collisions

Au départ, nous avons notre boîte vert clair, 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 vert clair est à une position initiale hors mur, nous donnons le vecteur de déplacement souhaité ;
  • nous calculons l'éventuelle position finale (vert foncé) ;
  • nous demandons à CollisionDecor si la boîte vert foncé est dans le mur ou non ;
  • si elle ne l'est pas, on valide le déplacement : notre boîte vert clair prend la place de la boîte vert foncé (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.

V-A. 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.

Notre rectangle ne touchera donc jamais le mur, il restera toujours à quelques pixels de celui-ci. C'est gênant.

L'idée est, si le mouvement nous amène dans le mur, de voir comment 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 serons alors dans le cas C(b). J'appellerai cette opération Affiner le mouvement.

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

  • la boîte vert clair est à une position initiale hors mur, nous donnons le vecteur de déplacement souhaité ;
  • nous calculons l'éventuelle position finale (vert foncé) ;
  • nous demandons à CollisionDecor si la boîte vert foncé est dans le mur ou non ;
  • si elle ne l'est pas, on valide le déplacement : notre boîte vert clair prend la place de la boîte vert foncé (cas A) ;
  • sinon, nous sommes dans le cas B ou le cas C :dans tous les cas, nous sommes toujours hors mur à ce moment-là.
    • nous cherchons un vecteur affiné, qui permettrait d'aller se coller contre le mur, en faisant des essais, et en re-testant 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.

VI. 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 simple pour commencer !

Ce monde sera juste un rectangle marron…

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

Utilisez les flèches pour déplacer le rectangle vert, Echap pour quitter.

Vous constatez rapidement que, quand on s'approche du rectangle marron, il y a quelques soucis. Lisez bien cette partie jusqu'au bout pour résoudre cela.

Si on regarde la fonction main, dans prog4.c :

 
Sélectionnez
#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 la fonction 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 plus précisément un sprite.

Dans la boucle, on « Evolue », puis on affiche le mur, et le sprite.

VI-A. Evolue

La fonction Evolue, juste au-dessus, fait deux 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.

VI-B. 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 :

 
Sélectionnez
#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 :

 
Sélectionnez
#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.

VI-C. DeplaceSprite

Nous voici à 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.

VI-D. EssaiDeplacement

La fonction EssaiDeplacement va tenter de déplacer le sprite selon le vecteur donné. Je dis tenter, 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 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.

VI-E. 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.

Arrêtons de détailler le code ici pour le moment, et relançons le programme

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 :

 
Sélectionnez
/*Affine(mur,perso,vx,vy);*/

Enlevez les commentaires et relancez le programme.

Oh miracle, vous pouvez maintenant aller vous coller au pixel près, et glisser sur le mur.

VI-F. 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).

À 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.

Navigation

Tutoriel précédent : éditeur de niveaux   Sommaire   Tutoriel suivant : insertion dans un monde de tuiles