| 2005 - Pérennisation |
Creation d'un moteur de jeu base sur les tiles
[40 mn de lecture - paru le 5/17/2005 5:39:48 PM - Public : Confirmé]
|
   
|
Auteur
2 Une ébauche de moteur
2.1 Les Tiles
Une fois la SDL correctement initialisée, nous allons pouvoir commencer l'écriture de notre moteur.
Ce dernier est basé sur des tiles, il nous faut donc une structure de
données capable de contenir l'image représentant le tile ainsi que
toutes les informations que nous jugerons utile d'y placer.
typedef struct __tile_t{
SDL_Surface *img;
int blocked;
}tile_t;
Pour le moment, cette structure ne contient que deux champs :
une SDL_Surface destinée à contenir l'image représentant le tile ainsi qu'un
flag indiquant si ce tile est bloqué ou non, c'est à dire si le joueur
peut le traverser : Une porte fermée, par exemple, n'est pas
traversable le flag sera donc à l'état vrai, c'est à dire 1. Pour
ouvrir la porte, il suffit de changer l'image puis de mettre le flag à
0.
Bien entendu nous ne stockons pas réellement l'image dans la structure !
Une carte comprend en général un grand nombre de tiles identiques, de
la pelouse par exemple : il serait dommage de charger 100 fois la meme
image en memoire alors qu'un pointeur nous permet de n'en conserver
qu'un unique exemplaire !
Remarques également que la structure est nommée( __tile_t )alors que
nous avons recours à un typedef : cela n'est pas utile pour le moment,
mais la structure est appelleé à evoluer, et soyez en sur, ceci
deviendra indispensable.
Nous ne définirons pas de fonction d'allocation particulière, allouer
un tile n'ayant en soit que peu d'intérêt : Nous manipulerons un
tableau de tiles.
2.2 La Carte( Tilemap )
2.2.1 Création
Une fois notre type de base défini, nous pouvons envisager d'afficher
un peu plus q'un unique tile sur notre écran : Un niveau entier, ou une
carte, si vous preferez, sera definie de la maniere suivante :
typedef struct{
tile_t **map;
int height;
int width;
int x_offset;
int y_offset;
}level_t;
Notre premier champ est bien entendu notre carte à proprement parler,
définie sous la forme d'un tableau à deux dimensions : Chaque tile sera
repéré par ses coordonnées x et y. Les deux champs suivants
contiendrons respectivement la hauteur et la largeur de la carte. Quant
aux deux derniers, ils indiquent le déplacement par rapport au premier
tile(0,0) en haut à gauche : c'est ainsi que nous deplacerons la carte
lorsque le joueur s'approchera danguereusement des bords de l'ecran.
Comme vous vous en doutez sûrement, cette structure ne sera pas
déclarée de manière statique, mais nous laisserons le travail
d'allocation mémoire de la structure comme du tableau de tiles à la
fonction suivante, qui prendra en paramètre deux entiers indiquant
respectivement la largeur et la hauteur en tiles de la carte.
level_t *create_tile_map(int width, int height )
{
level_t *rv;
int i, j;
rv = (level_t *)malloc(sizeof(level_t));
if( !rv )
return(NULL);
memset(rv,0,sizeof(level_t));
rv->map = (tile_t **)malloc(sizeof(tile_t *) * width);
if( !rv->map )
return(NULL);
for( i = 0; i < width; i++ ){
rv->map[i] = ( tile_t * )malloc( sizeof(tile_t) * height );
if( !rv->map[i] )
return(NULL);
memset( rv->map[i], 0, sizeof(tile_t) * height) ;
}
for( i = 0; i < width; i++ ){
for( j = 0; j < height; j++ ){
rv->map[i][j].blocked = 0;
rv->map[i][j].img = NULL;
}
}
rv->width = width;
rv->height = height;
return(rv);
}
Cette fonction ne contient aucun point particulier à décrire, il s'agit
principalement de l'allocation d'un tableau a deux dimensions.
Remarquez tout de même l'appel à memset() qui permet ici de mettre à 0 toute une
Portion de mémoire, en l'occurrence la mémoire occupée par la structure, ce qui a pour effet de régler tous ses champs à 0.
On notera que tous les tiles voient leur pointeur sur surface réglé à
NULL ainsi que leur champ blocked à 0 par le même procédé.
ATTENTION : Cette fonction ne crée pas vraiment de carte affichable,
elle ne fait que renvoyer une structure initialisée comportant un
tableau de tiles aux dimensions voulues. Tenter de l'afficher tel quel
provoquerait à coup sur une erreur !
Il faut d'abord faire pointer la surface de chaque tile vers une SDL_Surface précédemment allouée.
2.2.2 Chargement d'images
Voici une fonction permettant de charger une image, pour le moment au format BMP uniquement :
SDL_Surface *load_img_from_file( char *filename )
{
SDL_Surface *rv;
if( !filename )
return(NULL);
rv = SDL_LoadBMP(filename);
return(rv);
}
Certes, on pourrai largement se passer de cette fonction en appelant
directement SDL_LoadBMP, mais cela nous obligerait à modifier beaucoup
de code si nous décidions de supporter d'autres formats, ce qui sera
fait prochainement.
Enfin, il est vrai que la variable rv n'est pas obligatoire, il est
tout à fait possible de retourner directement le résultat de la
fonction, mais cela pourrai compromettre la lisibilité du code. Encore
une fois, rien ne vous empêche d'apporter les modifications que vous
jugerez utiles.
Maintenant que nous savons charger une image en mémoire, nous pouvons
sans difficulté créer une carte de 40x50 tiles représentant une vaste
et verdoyante prairie.
SDL_Surface *pict = load_img_from_file("grass.bmp");
level_t *level = create_tile_map( 40, 50 );
for( i = 0; i < level->width; i++ ){
for( j = 0; j < level->height; j++ ){
level->map[i][j].img = pict;
}
}
insérez ce code dans le main, ou créez une fonction d'initialisation si cela vous semble plus convenable.
Comme vous pouvez le constater, il n'existe aucune contrainte sur la
taille des tiles, cependant le moteur est destiné à gérer des tiles
carrés. Une taille de 64x64 pixels semble être tout à fait correcte en
regard de la résolution( 800x600 ) choisie.
2.2.3 Affichage
Une fois le pré chargé en mémoire, l'étape suivante est de le dessiner !
int draw_level( level_t *level )
{
int i,j;
SDL_Rect dest_rect;
if( !level )
return(-1);
SDL_FillRect( screen, NULL, SDL_MapRGB(screen->format,0,0,0) );
for( i = 0; i < level->width; i++ ){
for( j = 0; j < level->height; j++ ){
dest_rect.x = i * level->map[i][j].img->w - level->x_offset;
dest_rect.y = j * level->map[i][j].img->h - level->y_offset;
SDL_BlitSurface(level->map[i][j].img,NULL,screen, &dest_rect);
}
}
return(0);
}
Avant de dessiner quoi que ce soit, il faut effacer l'écran : Grâce à
SDL_FillRect, nous remplissons l'écran de couleur noire. Cette fonction
prend 3 paramètres :
- La surface de destination
- L'aire dans cette surface que nous voulons remplir ou NULL dans notre cas, afin de remplir la totalité de la surface.
- Le dernier paramètre correspond à la couleur.
Or, il n'est pas possible de spécifier de couleur directement.
En effet, chaque surface peutAvoir un format de pixel différent :
Il faut faire appel a une fonction, SDL_MapRGB qui va traduire une couleur spécifiée
au moyen des coefficients Rouge, Vert et Bleu dans le format de pixel de la surface
que l'on veut remplir. Pour ce faire, il suffit de passer à la fonction SDL_MapRGB
le format de pixel dans lequel il faut effectuer la translation ainsi que les valeurs RGB.
Chaque surface contient son format de pixel dans le champ... format !
C'est ainsi que pour remplir tout l'écran de couleur noire, nous utilisons la fonction suivante :
SDL_FillRect( screen, NULL, SDL_MapRGB(screen->format,0,0,0) );
Ensuite nous commençons le dessin de la carte proprement dit :
Grâce aux deux boucles for imbriqués, nous iterons sur tous les éléments (x, y)
de la carte.
Avant l'affichage, nous devons transformer les coordonnées en tiles vers des coordonnées en pixel :
Pour obtenir les coordonnées (x',y') en pixels du coin haut gauche de la tile, il suffit
,sachant que toutes les tiles ont la même taille, de multiplier ses coordonnées (x,y) ou
bien encore dans la fonction (i,j) par la largeur ou
la hauteur d'une tile selon si nous désirons la coordonnée x ou y puis de soustraire
l'offset correspondant : Inutile de vérifier si la tile est actuellement visible ou non !
SDL s'en charge pour nous : une fois l'offset soustrait, si la tile a deux coordonnee
négatives, elle ne sera pas dessinée.
dest_rect.x = i * level->map[i][j].img->w - level->x_offset;
dest_rect.y = j * level->map[i][j].img->h - level->y_offset;
Comme il apparaît dans le code, les coordonnées ne sont pas «
directement » spécifiées, il faut remplir une structure de type
SDL_Rect comprenant 4 champs :
- l'abscisse (x)
- l'ordonnée (y)
- la hauteur (h)
- la largeur (w)
Ici, seule l'ordonnée et l'abscisse sont nécessaires.
La fonction qui permet de copier la tile vers l'écran,
SDL_BlitSurface, prends 4 paramètres :
- L'adresse de la surface source
- L'adresse d'un SDL_Rect représentant la portion de surface source à copier
- L'adresse de la Surface de destination
- L'adresse d'un SDL_Rect dont seul les champs x et y sont utilisés
pour indiquer les coordonnées de la surface de destination vers
laquelle doit être réalisée la copie
Passer NULL en deuxième paramètre permet de copier la totalité de la surface.
A présent, nous sommes capables de créer une carte, et de l'afficher à l'écran. Il
ne nous manque plus qu'un joueur et bien entendu de pouvoir déplacer la
carte si notre héros venait à s'aventurer trop près des bords de
l'écran.
2.3 Le Joueur
2.3.1 Structure
Il sera représenté par la structure suivante :
typedef struct{
SDL_Surface *img;
int x,y;
int vx,vy;
}player_t;
Une image, ses coordonnées et enfin sa « vitesse » en x et en y.
2.3.2 Creation
Pour créer le joueur, utilisons la fonction suivante :
player_t *make_player( SDL_Surface *def, int x, int y )
{
player_t *rv;
if( !def )
return(NULL);
rv = (player_t *)malloc( sizeof(player_t) );
if( !rv )
return(NULL);
memset( rv, 0, sizeof(player_t) );
rv->img = def;
rv->x = x;
rv->y = y;
return(rv);
}
Cette fonction se contente de prendre en paramètre un pointeur vers
une surface contenant l'image représentant le joueur, sa position (x,y)
d'origine sur la carte
et renvoie un pointeur sur le joueur nouvellement alloué.
ATTENTION ! L'image représentant le joueur doit impérativement être de
la même taille que les tiles, en effet, la position (x,y) du joueur est
la position d'un tile, le tile représentant le joueur.
2.3.3 Affichage
2.3.3.1 Calcul de coordonnées
Avant de pouvoir afficher le joueur, nous devons être capables de
traduire sa position dans le repère des tiles vers celui de l'écran, en
pixel : il s'agit d'un simple changement de repere comme celui effectué
pour le dessin des tiles.
Ici, une fonction va nous être utile :
SDL_Rect player_coords_to_screen_area( level_t *level, player_t *player )
{
SDL_Rect rv;
if( !(level && player) )
return;
rv.x = player->x * player->img->w - level->x_offset;
rv.y = player->y * player->img->h - level->y_offset;
rv.h = player->img->h;
rv.w = player->img->w;
return(rv);
}
Avant toute chose, vous ne manquerez pas de faire la remarque suivante :
Pourquoi utiliser une fonction séparée, alors que le calcul de la position des tiles se fait dans le corps de la fonction d'affichage ?
Tout simplement a cause du coup de l'appel de fonction :
Lorsque l'on dessine la carte, ce calcul est fait un très grand nombre de fois :
Autant de fois qu'il y a de tiles dans la carte !
Là bas, le placer dans une fonction séparée aurait engendré un sur coup
en temps de calcul important, alors qu'ici cette fonction n'est appelée
qu'une fois par itération de la boucle principale, engendrant un coup
faible pour une meilleure lisibilité du code, ce qui devient
intéressant...
Il est vrai qu'il aurait été possible de faire de même en mettant cette
fonction « inline », ou encore sous forme de macro. Au risque de me
répéter, libre à vous !
2.3.3.2 Dessin
Maintenant, avant d'examiner la gestion du clavier, voyons la fonction qui prend en charge le dessin du joueur :
int draw_player( level_t *level, player_t *player )
{
SDL_Rect player_rect;
if( !(player && level ) )
return(-1);
player_rect = player_coords_to_screen_area( level, player );
SDL_BlitSurface(player->img, NULL, screen, &player_rect);
return(0);
}
Rien de franchement nouveau jusqu'ici, mais n'ayez crainte, cette fonction elle aussi
est appelée à évoluer.
2.3.4 Déplacements
Il existe des tiles inaccessibles au joueur : Pour prendre en compte ce fait, voici
une petite fonction inline qui vérifie si le joueur peut aller sur sa case de destination :
inline int is_blocked( int x, int y, level_t *level )
{
return(level->map[x][y].blocked);
}
Pour faire bouger le joueur, nous devons récupérer les événements du clavier :
La fonction move_player va s'en charger :
int move_player( level_t *level, player_t *player )
{
SDL_Event event;
SDL_Rect player_rect;
while( SDL_PollEvent( &event ) ){
switch( event.type ){
case SDL_KEYDOWN:
switch( event.key.keysym.sym ){
case SDLK_LEFT:
player->vx = -1;
break;
case SDLK_RIGHT:
player->vx = 1;
break;
case SDLK_UP:
player->vy = -1;
break;
case SDLK_DOWN:
player->vy = 1;
break;
case SDLK_ESCAPE:
return(0);
break;
default:
break;
}
break;
case SDL_KEYUP:
switch( event.key.keysym.sym ){
case SDLK_LEFT:
if( player->vx < 0 )
player->vx = 0;
break;
case SDLK_RIGHT:
if( player->vx > 0 )
player->vx = 0;
break;
case SDLK_UP:
if( player->vy < 0 )
player->vy = 0;
break;
case SDLK_DOWN:
if( player->vy > 0 )
player->vy = 0;
break;
default:
break;
}
break;
default:
break;
}
}
/* Update the player position */
player->x += player->vx;
if( player->x < 0 || player->x >= level->width )
player->x -= player->vx;
player->y += player->vy;
if( player->y < 0 || player->y >= level->height )
player->y -= player->vy;
if( is_blocked( player->x, player->y, level) ){
player->y -= player->vy;
player->x -= player->vx;
}
player_rect = player_coords_to_screen_area( level, player );
if( player_rect.y > 500 )
level->y_offset += 400;
if( player_rect.x > 700 )
level->x_offset += 400;
if( player_rect.y < 100 && level->y_offset > 0 )
level->y_offset -= 400;
if( player_rect.x < 100 && level->x_offset > 0 )
level->x_offset -= 400;
return(1);
}
Pour SDL, l'appui sur une touche, un mouvement de souris ou encore un click sont des
événements.
A chaque fois que l'utilisateur appuie sur une touche ou la relâche, un événement de
produit. S'il ne peut être traité immédiatement, SDL le met dans une file d'attente.
pour traiter l'appui sur les touches du clavier, nous devons donc récupérer chaque
événement dans la file puis verifier s'il s'agit bien d'un evenement clavier :
Pour ce faire nous declarons une variable event de type SDL_Event qui est en fait
une structure contenant plusieurs champs dont l'un nous intéresse particulièrement :
type c'est lui qui va nous indiquer si l'événement est bien un événement clavier.
L'adresse de cette structure sera passée à la fonction SDL_PollEvent qui enlève le prochain évènement de la file et remplit les champs la structure passée en paramètre.
Comme cette fonction retourne 1 tant qu'il reste des évènements à traiter et 0
dans le cas contraire, pour itérer sur le évènements il suffit de faire :
while( SDL_PollEvent( &event ) ){
switch( event.type ){
case SDL_KEYDOWN: //une touche est pressée
switch( event.key.keysym.sym ){
?
}
?.
case SD_KEYUP : //une touche est relachée
switch( event.key.keysym.sym ){
?
}
}
}
Une fois que l'appui ou le relâchement d'une touche est détecté, pour l'identifier,
il suffit de lire event.key.keysym.sym qui contient le code de la touche concernée
et de le comparer aux constantes définies par SDL, consultables soit dans la
documentation, soit dans le fichier /usr/include/SDL/SDL_keysym.h pour les
plus courageux.
Pourquoi traiter les événements SDL_KEYDOWN et SDL_KEYUP ?
Pour SDL, une fois que l'événement KEYDOWN s'est produit sur une touche,
SDLK_LEFT par exemple, il ne se produira plus avant que l'événement
SDL_KEYUP se produise pour la même touche( a moins d'avoir réglé le paramètre
SDL_EnableKeyRepeat, cas que nous ne traiterons pas ).
Ainsi pour se déplacer de 5 tiles, l'utilisateur devra presser et relâcher la touche
« Flèche de gauche » 5 fois !
Afin de remédier à cela, il suffit de considérer que le joueur se déplace à partir
du moment ou un événement KEYDOWN a été reçu sur la touche convenue
jusqu'au moment ou cette touche est relachée : d'où l'importance du vecteur
vitesse du joueur !
La direction voulue(vx ou vy ) est mise à 1 ou -1 selon le sens du déplacement(haut,
bas,droite,gauche) et n'est remise à 0 que lorsque la touche est relâchée.
Enfin nous mettons à jour la position de joueur, en vérifiant qu'il ne sorte
pas de la carte, ou qu'il se ne retrouve pas sur une tile bloquée, puis ayant
converti ses coordonnées en pixels, nous vérifions s'il est temps de déplacer
la carte.
Nous avons choisi pour les valeurs limites avant les bords de l'écran,
ainsi que le nombre de pixels de décalage de la carte de manière
totalement arbitraire, veillez à les modifier si vos tiles sont de taille différente par
rapport aux nôtres( 64x64 pixels).
Notez que la fonction retourne toujours 1 sauf si la touche ESCAPE est pressée,
auquel cas elle retourne 0, ce qui permet de mettre fin au jeu.
2.4 Boucle Principale
Nantis de ces fonctions, nous pouvons maintenant écrire la boucle principale
du jeu, destinée à être appelle par le main() :
void main_loop(level_t *level, player_t *player)
{
int playing = 1;
while( playing ){
playing = move_player(level, player);
draw_level(level,player);
draw_player(level, player);
SDL_Flip(screen);
}
}
Voilà ! Nous venons de jeter les bases du moteur, il est maintenant parfaitement
fonctionnel.
Cependant, le joueur n'est pas du tout anime, on ne peut pas le voir marcher,
Il semble être toujours dans la même direction qu'il aille en haut, en bas a gauche
ou à droite !
Il n'y a aucun mécanisme correct d'animation, et il faudrait considérablement,
dans l'état actuel des choses, alourdir la fonction d'affichage du niveau pour
faire clignoter une lumière !
|
|
|
 |