• Difficile

Mis à jour le 06/12/2013

Scrolling

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

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.

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 tiles de large (le monde entier, du départ jusqu'au drapeau). Chaque tile 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 de blitter l'image géante où il faut, pour que seule la partie "intéressante" apparaisse. Et de la blitter légèrement plus loin à la frame 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...

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 :

Image utilisateur

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 affichée 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.

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.
$P_{global} = P_{local} + S$$P_{local} = P_{global} - S$

Voici comment nous allons modifier notre structure Map :

typedef struct
{
	SDL_Rect R;
	char plein;
} TileProp;

typedef struct
{
	int LARGEUR_TILE,HAUTEUR_TILE;
	int nbtiles;
	TileProp* props;
	SDL_Surface* tileset;
	tileindex** schema;
	int nbtiles_largeur_monde,nbtiles_hauteur_monde;
	int xscroll,yscroll;
	int largeur_fenetre,hauteur_fenetre;
} Map;

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

int xscroll,yscroll;
int largeur_fenetre,hauteur_fenetre;

Alors ces paramètre sont très simples :
largeur_fenetreet, hauteur_fenetre sont la largeur et 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é à 32767 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 il changeront la fenêtre rouge sur l'image géante se déplacera. Concrètement, il y aura scrolling...

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 otés de m->xscroll et m->yscroll

Concrètement, avec cette fonction, je vais afficher TOUTE la grande map (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% sera 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 tile, Rect_dest.x évoluera négativement : ce qui est normal, car quand le rectangle rouge avance, on a l'impression que les tiles reculent ! Et oui, c'est magique !
Si vous regardez Mario, quand vous courrez dans un monde, les tiles, eux, vont en arrière...

L'exemple qui finira cette partie vous illustrera cela.

Deuxième version

L'inconvénient de la première version est qu'elle va essayer de blitter tous les tiles du niveau. Quand ils seront dehors, ils ne seront pas affichés, mais la machine essayera des les blitter quand même.
Du coup, plus le monde sera grand, plus les for seront longs, et plus la machine tentera de blitter, et plus ça va ralentir...
C'est dommage.

Je propose donc une optimisation.

Voici l'idée : seuls les tiles présents dans le cadre rouge devront être affichés. 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'un tile tout simplement.
Et quel est le max ? La coordonnée de droite de la fenêtre que divise la taille d'un tile ...

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 tiles, il faut que le tile d'avant soit affiché, pour voir le morceau de droite (ou du bas) arriver par la gauche (ou par le haut).

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 que on a des tiles 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;
}

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 events, je mets à jour ma structure Input. J'explique tout cela dans un autre tutoriel. 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 map, je flip et je fais un Delay 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 soucis.
Et dans la mesure ou la fonction AfficherMap considère que tout ce qui est dehors est le tile 0, alors si vous sortez, vous verrez des "tile 0" à perte de vue. Essayez donc !

Ici, mon tile 0 est un bloc qui se voit bien. Mais on pourrait mettre du ciel, ou donner un autre numéro de tile 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 affiche ce qu'il faut en conséquence.

Ainsi, un scrolling horizontal et vertical revient uniquement à mettre à jour 2 variables...

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