Tile Mapping : Défilement (scrolling)

Fvirtman

Jusqu'à maintenant, on affichait une pauvre image fixe. Nous allons voir ici comment avoir un scrolling, c'est-à-dire un défilement d'écran.

Quand dans Mario, vous courez, vous voyez le paysage qui défile derrière vous. C'est ce qu'on appelle le scrolling.

Commentez

- Titre : Tile Mapping : Défilement (scrolling)

- Auteur : Fvirtman

- Parution : 21 novembre 2013

- Public visé :

- Licence : Le contenu de cet article est rédigé par Fvirtman et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Navigation

- II. Introduction

- III. L'idée de l'image géante

- IV. Le fenêtrage

- IV-A. Deux repères

- IV-B. Première version

- IV-C. Deuxième version

- IV-D. Troisième version

- V. Code exemple

- Navigation

Navigation

Tutoriel précédent : propriétés des tuiles

Sommaire

Tutoriel suivant : éditeur de niveaux

II - Introduction

Jusqu'à maintenant, on affichait une pauvre image fixe. Nous allons voir ici comment avoir un scrolling, c'est-à-dire un défilement d'écran.

Quand dans Mario, vous courez, vous voyez le paysage qui défile derrière vous. C'est ce qu'on appelle le scrolling.

III - L'idée de l'image géante

Dans ce paragraphe, nous allons parler de l'idée de l'image géante. C'est une idée à laquelle on pense quand on veut faire du scrolling…

Imaginons un monde de Mario qui fait 300 tuiles de large (le monde entier, du départ jusqu'au drapeau). Chaque tuile fait 24 pixels de large.

Si nous voulons dessiner tout le monde d'un coup, nous avons donc besoin d'une image de 300 * 24 = 7200 pixels de large. Une image géante donc !

L'idée est d'afficher l'image géante où il faut, pour que seule la partie « intéressante » apparaisse. Et de l'afficher légèrement plus loin à l'image d'après pour qu'on ait l'impression qu'on a bougé.

Mais cette image géante, en mémoire, prendra plusieurs dizaines de Mo. Et encore, si le monde est grand comme une carte de Zelda, ce sera en centaines de Mo que ça se comptera…

Sur un PC puissant, cette technique pourra marcher, même si elle risque de saturer la mémoire graphique (VRAM), mais c'est épouvantablement lourd.

Alors pourquoi les consoles comme la NES, très peu puissantes, arrivaient à gérer des scrollings alors qu'elles n'avaient que quelques Ko de mémoire ?

Tout simplement parce que stocker une image géante n'est pas une bonne idée…

IV - Le fenêtrage

Bien que trop lourde en mémoire, nous n'allons pas oublier notre image géante.

Nous allons pour l'instant juste imaginer qu'elle existe, mais ne pas la stocker en mémoire.

Voici une belle image :

Elle venait d'un exemple précédent que j'ai mis à jour. Dans l'exemple de la fin de ce chapitre, nous en aurons une autre plus jolie.

Qu'est-ce que le rectangle rouge en bas à gauche ?

C'est un rectangle que j'ai rajouté pour l'exemple. Ce rectangle, je vais l'appeler fenêtre.

Cette fenêtre, c'est ce que vous verrez sur votre écran. Cette fenêtre se décalera et vous verrez donc autre chose. Si cette fenêtre glisse vers la droite, alors on aura l'impression d'avancer dans le monde.

Vous voyez le concept ? Seule la partie incluse dans la fenêtre sera affichée sur votre écran.

J'appellerai ce rectangle la fenêtre du scrolling et il suffira de déplacer cette fenêtre pour faire défiler le niveau.

Je vous propose un petit travail manuel pour bien vous en rendre compte. Prenez une feuille A4, et découpez, en plein milieu, un rectangle de la taille du rectangle rouge. Posez la feuille sur votre écran sur l'image géante ci-dessus. Puis déplacez-la. Vous voyez le monde défiler dans le trou que vous avez fait.

IV-A - Deux repères

Notre monde entier est l'image géante, que nous n'afficherons jamais entièrement, mais qui existe.
Dans ce monde, les coordonnées varient de 0 à… beaucoup. 10 000 peut-être, bien plus encore, si notre monde est grand.
Nous appellerons ça le repère absolu, ou repère global.

Par contre, la partie que nous voyons à l'écran, elle, a toujours la même largeur et hauteur (notre écran) avec ses coordonnés qui vont de 0 à 800 par exemple, jamais plus.
Nous appellerons ça le repère local.

Pour passer de l'un à l'autre, c'est très simple : nous allons définir le point S, de coordonnées xscroll/yscroll pour la fenêtre. C'est le point du coin supérieur gauche de la fenêtre dans le repère global.

Si on a un point dans le repère local, et qu'on veut sa coordonnée dans le repère global, on fait une addition.
Pour passer de global à local, on fait une soustraction.

Le simple fait de modifier le point S permettra le scrolling.

Voici comment nous allons modifier notre structure Map :

1. typedef struct

2. {

3. SDL_Rect R;

4. char plein;

5. } TileProp;

7. typedef struct

8. {

9. int LARGEUR_TILE,HAUTEUR_TILE;

10. int nbtiles;

11. TileProp* props;

12. SDL_Surface* tileset;

13. tileindex** schema;

14. int nbtiles_largeur_monde,nbtiles_hauteur_monde;

15. int xscroll,yscroll;

16. int largeur_fenetre,hauteur_fenetre;

17. } Map;

Vous pouvez constater, par rapport aux structures de l'exemple d'avant, que seuls quatre paramètres ont été ajoutés : le reste n'a pas bougé.

int xscroll,yscroll;
int largeur_fenetre,hauteur_fenetre;

Alors ces paramètres sont très simples :

largeur_fenetre et hauteur_fenetre sont la largeur et la hauteur de ma fenêtre de scrolling (la largeur et hauteur du rectangle rouge), et xscroll et yscroll sont la position de son point supérieur gauche, le point S.

Exactement comme un SDL_rect !

Mais pourquoi ne pas utiliser un SDL_Rect ?

Parce qu'un SDL_Rect utilise un x,y en tant que signed short, c'est-à-dire qu'il est limité à 32 767 pixels.

Or, si notre monde est très très grand, l'image géante imaginée sera possiblement plus grande que cela. Nous utiliserons donc un int qui pourra nous permettre d'aller beaucoup plus loin.

largeur_fenetre et hauteur_fenetre resteront invariants : tout au long du jeu, la taille de la fenêtre d'affichage (ce que vous voyez) restera constante.

Par contre, xscroll et yscroll, eux, changeront.

Et quand ils changeront, la fenêtre rouge sur l'image géante se déplacera. Concrètement, il y aura scrolling...

L'idée est donc maintenant de modifier la fonction d'affichage pour qu'elle affiche la partie du monde correspondant à la fenêtre rouge.

IV-B - Première version

int AfficherMap(Map* m,SDL_Surface* screen)
{
int i,j;
SDL_Rect Rect_dest;
int numero_tile;
for(i=0;i<m->nbtiles_largeur_monde;i++)
{
for(j=0;j<m->nbtiles_hauteur_monde;j++)
{
Rect_dest.x = i*m->LARGEUR_TILE - m->xscroll;
Rect_dest.y = j*m->HAUTEUR_TILE - m->yscroll;
numero_tile = m->schema[i][j];
SDL_BlitSurface(m->tileset,&(m->props[numero_tile].R),screen,&Rect_dest);
}
}
return 0;
}

Vous pouvez constater que la seule différence avec la fonction AfficherMap d'avant, c'est que Rdest.x et Rdest.y sont ôtés de m->xscroll et m->yscroll.

Concrètement, avec cette fonction, je vais afficher TOUTE la grande carte (car mes for vont de 0 à nbtiles_largeur_monde et de 0 à nbtiles_hauteur_monde, donc couvrent tout), mais en « décalant » de xscroll et yscroll.
Concrètement, si mon rectangle rouge est à l'endroit ci-dessus, je vais quand même tout afficher (90 % seront hors de l'écran, mais tant pis) y compris le « FRED » qu'on voit en haut : il sera affiché hors écran (donc ignoré) mais on le calculera quand même...

Cette simple soustraction permet le scrolling. En effet, si xscroll évolue positivement, alors, pour chaque tuile, Rect_dest.x évoluera négativement : ce qui est normal, car quand le rectangle rouge avance, on a l'impression que les tuiles reculent ! Eh oui, c'est magique !
Si vous regardez Mario, quand vous courez dans un monde, les tuiles, elles, vont en arrière…

L'exemple qui finira cette partie vous illustrera cela.

IV-C - Deuxième version

L'inconvénient de la première version est qu'elle va essayer d'afficher toutes les tuiles du niveau. Quand elles seront dehors, elles ne seront pas affichées, mais la machine essayera de les afficher quand même.
Du coup, plus le monde sera grand, plus les for seront longs, et plus la machine tentera d'afficher, et plus ça va ralentir…
C'est dommage.

Je propose donc une optimisation.

Voici l'idée : seules les tuiles présentes dans le cadre rouge devront être affichées. Les autres seront dehors : inutile de les afficher.
Nous allons donc, au lieu de faire varier notre for entre 0 et m->nbtiles_largeur_monde, le faire varier entre un xmin et un xmax. Pareil pour y.
Ainsi, nous restreignons notre boucle à la seule zone d'affichage.

Il faut donc calculer ces xmin, xmax, ymin et ymax

Quel est le xmin ? C'est la coordonnée de gauche de la fenêtre que divise la taille d'une tuile tout simplement.
Et quel est le max ? La coordonnée de droite de la fenêtre que divise la taille d'une tuile…

Cela nous donne immédiatement notre deuxième version de la fonction AfficherMap :

int AfficherMap(Map* m,SDL_Surface* screen)
{
int i,j;
SDL_Rect Rect_dest;
int numero_tile;
int minx,maxx,miny,maxy;
minx = m->xscroll / m->LARGEUR_TILE-1;
miny = m->yscroll / m->HAUTEUR_TILE-1;
maxx = (m->xscroll + m->largeur_fenetre)/m->LARGEUR_TILE;
maxy = (m->yscroll + m->hauteur_fenetre)/m->HAUTEUR_TILE;
for(i=minx;i<=maxx;i++)
{
for(j=miny;j<=maxy;j++)
{
Rect_dest.x = i*m->LARGEUR_TILE - m->xscroll;
Rect_dest.y = j*m->HAUTEUR_TILE - m->yscroll;
numero_tile = m->schema[i][j];
SDL_BlitSurface(m->tileset,&(m->props[numero_tile].R),screen,&Rect_dest);
}
}
return 0;
}

je calcule mon xmin, xmax, ymin, ymax, puis je ne fais varier mes for que dans ces zones-là.

Pour xmin et ymin, je mets -1 car si le fenêtrage est entre deux tuiles, il faut que la tuile d'avant soit affichée, pour voir le morceau de droite (ou du bas) arriver par la gauche (ou par le haut).

La taille de la carte finale ne ralentira plus rien : en effet, le niveau peut être grand ou petit, il n'y aura pas plus de calculs.

IV-D - Troisième version

Avec la deuxième version, la fonction plantera si la fenêtre de scrolling sort de l'espace de l'image géante, car les i,j déborderont du tableau m->schema.

Donc soit on fait attention à limiter le scrolling, soit on protège la fonction avec un if, soit les deux.

Dans le cas de cette version, si on sort du schéma, on dit qu'on a des tuiles de type 0, à l'infini…

int AfficherMap(Map* m,SDL_Surface* screen)
{
int i,j;
SDL_Rect Rect_dest;
int numero_tile;
int minx,maxx,miny,maxy;
minx = m->xscroll / m->LARGEUR_TILE-1;
miny = m->yscroll / m->HAUTEUR_TILE-1;
maxx = (m->xscroll + m->largeur_fenetre)/m->LARGEUR_TILE;
maxy = (m->yscroll + m->hauteur_fenetre)/m->HAUTEUR_TILE;
for(i=minx;i<=maxx;i++)
{
for(j=miny;j<=maxy;j++)
{
Rect_dest.x = i*m->LARGEUR_TILE - m->xscroll;
Rect_dest.y = j*m->HAUTEUR_TILE - m->yscroll;
if (i<0 || i>=m->nbtiles_largeur_monde || j<0 || j>=m->nbtiles_hauteur_monde)
numero_tile = 0;
else
numero_tile = m->schema[i][j];
SDL_BlitSurface(m->tileset,&(m->props[numero_tile].R),screen,&Rect_dest);
}
}
return 0;
}

V - Code exemple

Voici maintenant le code.

Ouvrez le projet « prog3 », compilez-le et lancez-le.

Appuyez sur les flèches pour faire défiler le paysage !

Expliquons un peu le code.

fevent.h

#include <sdl/sdl.h>

typedef struct
{
char key[SDLK_LAST];
int mousex,mousey;
int mousexrel,mouseyrel;
char mousebuttons[8];
char quit;
} Input;

void UpdateEvents(Input* in);
void InitEvents(Input* in);

C'est ma façon de gérer les événements, je mets à jour ma structure Input. fevent.c va avec.

Concentrons-nous maintenant sur le main, dans prog3.c :

#include "fmap.h"
#include "fevent.h"

#define LARGEUR_FENETRE 500
#define HAUTEUR_FENETRE 500
#define MOVESPEED 1

void MoveMap(Map* m,Input* in)
{
if (in->key[SDLK_LEFT])
m->xscroll-=MOVESPEED;
if (in->key[SDLK_RIGHT])
m->xscroll+=MOVESPEED;
if (in->key[SDLK_UP])
m->yscroll-=MOVESPEED;
if (in->key[SDLK_DOWN])
m->yscroll+=MOVESPEED;
// limitation
if (m->xscroll<0)
m->xscroll=0;
if (m->yscroll<0)
m->yscroll=0;
if (m->xscroll>m->nbtiles_largeur_monde*m->LARGEUR_TILE-m->largeur_fenetre-1)
m->xscroll=m->nbtiles_largeur_monde*m->LARGEUR_TILE-m->largeur_fenetre-1;
if (m->yscroll>m->nbtiles_hauteur_monde*m->HAUTEUR_TILE-m->hauteur_fenetre-1)
m->yscroll=m->nbtiles_hauteur_monde*m->HAUTEUR_TILE-m->hauteur_fenetre-1;
}

int main(int argc,char** argv)
{
SDL_Surface* screen;
Map* m;
Input I;
InitEvents(&I);
SDL_Init(SDL_INIT_VIDEO);        // prepare SDL
screen = SDL_SetVideoMode(LARGEUR_FENETRE, HAUTEUR_FENETRE, 32,SDL_HWSURFACE|SDL_DOUBLEBUF);
m = ChargerMap("level2.txt",LARGEUR_FENETRE,HAUTEUR_FENETRE);
while(!I.key[SDLK_ESCAPE] && !I.quit)
{
UpdateEvents(&I);
MoveMap(m,&I);
AfficherMap(m,screen);
SDL_Flip(screen);
SDL_Delay(1);
}
LibererMap(m);
SDL_Quit();
return 0;
}

Dans le main, j'initialise ChargerMap en précisant en paramètres supplémentaires la taille de la fenêtre que je désire.
Si vous voulez une fenêtre plus grande, changez simplement les #define en haut de ce fichier.

Dans le while du main, j'appelle une fonction MoveMap qui est au-dessus. C'est elle qui va me permettre de commander mon scrolling.
Puis j'affiche la carte, j'alterne les tampons et je fais un délai pour réguler la vitesse.

La fonction MoveMap est très simple :
je regarde les touches de direction, et en fonction d'elles, je mets simplement à jour les variables xscroll et yscroll. La fonction AfficherMap en tiendra compte.

Notez la partie « limitation », qui empêche la fenêtre de sortir du repère global. Si vous l'enlevez, alors vous pourrez sortir sans souci.
Et dans la mesure où la fonction AfficherMap considère que tout ce qui est dehors est la tuile 0, alors si vous sortez, vous verrez des « tuile 0 » à perte de vue. Essayez donc !

Ici, ma tuile 0 est un bloc qui se voit bien. Mais on pourrait mettre du ciel, ou donner un autre numéro de tuile par défaut…

En ce qui concerne le fichier fmap.c, tout ce qui diffère avec la version précédente, c'est la fonction AfficherMap expliquée juste au-dessus.
Il y a juste, à la fin de la fonction ChargerMap, le stockage des variables passées, et une initialisation de xscroll et yscroll à 0.

Ce qu'il faut bien retenir dans le scrolling, c'est que nous « imaginons » une grande image, faite du monde entier, qui peut être très grand ; et nous n'affichons que la partie désirée.
Nous ne stockons pas l'image géante complète en mémoire, mais nous stockons uniquement de quoi en calculer rapidement une partie (celle qui sera visible).

Pour faire défiler l'écran, il suffit de changer la valeur des variables xscroll et yscroll. L'affichage montre ce qu'il faut en conséquence.

Ainsi, un scrolling horizontal et vertical revient uniquement à mettre à jour deux variables…

Navigation

Tutoriel précédent : propriétés des tuiles

Sommaire

Tutoriel suivant : éditeur de niveaux