Attaquons maintenant le gros du sujet : la fonction
jouer !
Cette fonction est la plus importante du programme, aussi soyez attentifs car c'est vraiment là qu'il faut comprendre. Vous verrez après que créer l'éditeur de niveaux n'est pas si compliqué que ça en a l'air.
Les paramètres envoyés à la fonction
La fonction
jouera besoin d'un paramètre : la surface
ecran. En effet, la fenêtre a été ouverte dans le
main, et pour que la fonction
jouer puisse y dessiner, il faut qu'elle récupère le pointeur sur
ecran !
Si vous regardez le
main à nouveau, vous voyez qu'on appelle
jouer en lui envoyant
ecran :
Code : C
Le prototype de la fonction, que vous pouvez mettre dans
jeu.h, est donc le suivant :
Code : C | void jouer(SDL_Surface* ecran);
|
La fonction ne renvoie aucune valeur (d'où le void), mais on pourrait en renvoyer une, si on voulait. On pourrait par exemple renvoyer un booléen pour dire si oui ou non on a gagné.
Les déclarations de variables
Cette fonction va avoir besoin de nombreuses variables.
Je n'ai pas pensé à toutes les variables dont j'ai eu besoin du premier coup. Il y en a donc certaines que j'ai ajoutées par la suite.
Variables de types définis par la SDL
Voici pour commencer toutes les variables de types définis par la SDL dont j'ai besoin :
Code : C | SDL_Surface *mario[4] = {NULL}; // 4 surfaces pour 4 directions de mario
SDL_Surface *mur = NULL, *caisse = NULL, *caisseOK = NULL, *objectif = NULL, *marioActuel = NULL;
SDL_Rect position, positionJoueur;
SDL_Event event;
|
J'ai créé un tableau de
SDL_Surface appelé
mario. C'est un tableau de quatre cases qui stockera Mario dans chacune des directions (un vers le bas, un autre vers la gauche, vers le haut et vers la droite).
Il y a ensuite plusieurs surfaces correspondant à chacun des sprites que je vous ai fait télécharger plus haut :
mur,
caisse,
caisseOK (une caisse sur un objectif) et
objectif.
À quoi sert marioActuel ?
C'est un pointeur vers une surface. Il pointe sur la surface correspondant au Mario orienté dans la direction actuelle. C'est donc
marioActuel que l'on blittera à l'écran. Si vous regardez tout en bas de la fonction
jouer, vous verrez justement :
Code : C | SDL_BlitSurface(marioActuel, NULL, ecran, &position);
|
On ne blitte donc pas un élément du tableau
mario, mais le pointeur
marioActuel.
Ainsi, en blittant
marioActuel, on blitte soit le Mario vers le bas, soit celui vers le haut, etc. Le pointeur
marioActuel pointe vers une des cases du tableau
mario.
Quoi d'autre à part ça ?
Une variable
position de type
SDL_Rect dont on se servira pour définir la position des éléments à blitter (on s'en servira pour tous les sprites, inutile de créer un
SDL_Rect pour chaque surface !).
positionJoueur est en revanche un peu différente : elle indique à quelle case sur la carte se trouve actuellement le joueur. Enfin, la variable
event traitera les événements.
Variables plus « classiques »
J'ai aussi besoin de me créer des variables un peu plus classiques de type
int (entier).
Code : C | int continuer = 1, objectifsRestants = 0, i = 0, j = 0;
int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};
|
continuer et
objectifsRestants sont des booléens.
i et
j sont des petites variables qui vont me permettre de parcourir le tableau
carte.
C'est là que les choses deviennent vraiment intéressantes. J'ai en effet créé un tableau à deux dimensions. Je ne vous ai pas parlé de ce type de tableaux auparavant, mais c'est justement le moment idéal pour vous apprendre ce que c'est. Ce n'est pas bien compliqué, vous allez voir.
Regardez la définition de plus près :
Code : C | int carte[NB_BLOCS_LARGEUR][NB_BLOCS_HAUTEUR] = {0};
|
En fait, il s'agit d'un tableau d'
int (entiers) qui a la particularité d'avoir deux paires de crochets
[ ].
Si vous vous souvenez bien de
constantes.h,
NB_BLOCS_LARGEUR et
NB_BLOCS_HAUTEUR sont des constantes qui valent toutes les deux 12.
Ce tableau sera donc à la compilation créé comme ceci :
Code : C
Mais qu'est-ce que ça veut dire ?
Ça veut dire que pour chaque « case » de
carte, il y a 12 sous-cases.
Il y aura donc les variables suivantes :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 | carte[0][0]
carte[0][1]
carte[0][2]
carte[0][3]
carte[0][4]
carte[0][5]
carte[0][6]
carte[0][7]
carte[0][8]
carte[0][9]
carte[0][10]
carte[0][11]
carte[1][0]
carte[1][1]
carte[1][2]
carte[1][3]
carte[1][4]
carte[1][5]
carte[1][6]
carte[1][7]
carte[1][8]
carte[1][9]
carte[1][10]
...
carte[11][2]
carte[11][3]
carte[11][4]
carte[11][5]
carte[11][6]
carte[11][7]
carte[11][8]
carte[11][9]
carte[11][10]
carte[11][11]
|
C'est donc un tableau de 12 * 12 = 144 cases !
Chacune des ces cases représente une case de la carte.
La fig. suivante vous donne une idée de la façon dont la carte est représentée.
Ainsi, la case en haut à gauche est stockée dans
carte[0][0].
La case en haut à droite est stockée dans
carte[0][11].
La case en bas à droite (la toute dernière) est stockée dans
carte[11][11].
Selon la valeur de la case (qui est un nombre entier), on sait si la case contient un mur, une caisse, un objectif, etc.). C'est justement là que va servir notre énumération de tout à l'heure !
Code : C | enum {VIDE, MUR, CAISSE, OBJECTIF, MARIO, CAISSE_OK};
|
Si la case vaut
VIDE (0) on sait que cette partie de l'écran devra rester blanche. Si elle vaut
MUR (1), on sait qu'il faudra blitter une image de mur, etc.
Initialisations
Chargement des surfaces
Maintenant qu'on a passé en revue toutes les variables de la fonction
jouer, on peut commencer à faire quelques initialisations :
Code : C | // Chargement des sprites (décors, personnage...)
mur = IMG_Load("mur.jpg");
caisse = IMG_Load("caisse.jpg");
caisseOK = IMG_Load("caisse_ok.jpg");
objectif = IMG_Load("objectif.png");
mario[BAS] = IMG_Load("mario_bas.gif");
mario[GAUCHE] = IMG_Load("mario_gauche.gif");
mario[HAUT] = IMG_Load("mario_haut.gif");
mario[DROITE] = IMG_Load("mario_droite.gif");
|
Rien de sorcier là-dedans : on charge tout grâce à
IMG_Load.
S'il y a une petite particularité, c'est le chargement de
mario. On charge en effet Mario dans chacune des directions dans le tableau
mario en utilisant les constantes
HAUT,
BAS,
GAUCHE,
DROITE. Le fait d'utiliser les constantes rend ici — comme vous le voyez — le code plus clair. On aurait très bien pu écrire
mario[0], mais c'est quand même plus lisible d'avoir
mario[HAUT] par exemple !
Orientation initiale du Mario (marioActuel)
On initialise ensuite
marioActuel pour qu'il ait une direction au départ :
Code : C | marioActuel = mario[BAS]; // Mario sera dirigé vers le bas au départ
|
J'ai trouvé plus logique de commencer la partie avec un Mario qui regarde vers le bas (c'est-à-dire vers nous). Si vous voulez, vous pouvez changer cette ligne et mettre :
Code : C | marioActuel = mario[DROITE];
|
Vous verrez que Mario sera alors orienté vers la droite au début du jeu.
Chargement de la carte
Maintenant, il va falloir remplir notre tableau à deux dimensions
carte. Pour l'instant, ce tableau ne contient que des 0.
Il faut lire le niveau qui est stocké dans le fichier
niveaux.lvl.
Code : C | // Chargement du niveau
if (!chargerNiveau(carte))
exit(EXIT_FAILURE); // On arrête le jeu si on n'a pas pu charger le niveau
|
J'ai choisi de faire gérer le chargement (et l'enregistrement) de niveaux par des fonctions situées dans
fichiers.c.
Ici, on appelle donc la fonction
chargerNiveau. On l'étudiera plus en détails plus loin (elle n'est pas très compliquée, de toute manière). Tout ce qui nous intéresse ici c'est de savoir que notre niveau a été chargé dans le tableau
carte.
Si le niveau n'a pas pu être chargé (parce que niveaux.lvl n'existe pas), la fonction renverra « faux ». Sinon, elle renverra « vrai ».
On teste donc le résultat du chargement dans une condition. Si le résultat est négatif (d'où le point d'exclamation qui sert à exprimer la négation), on arrête tout : on appelle
exit.
Sinon, c'est que tout va bien et on peut continuer.
Nous possédons maintenant un tableau
carte qui décrit le contenu de chaque case :
MUR,
VIDE,
CAISSE…
Recherche de la position de départ de Mario
Il faut maintenant initialiser la variable
positionJoueur.
Cette variable, de type
SDL_Rect, est un peu particulière. On ne s'en sert pas pour stocker des coordonnées en pixels. On s'en sert pour stocker des coordonnées en « cases » sur la carte. Ainsi, si on a :
positionJoueur.x == 11
positionJoueur.y == 11
… c'est que le joueur se trouve dans la toute dernière case en bas à droite de la carte.
Reportez-vous au schéma de la carte de la fig. suivante pour bien voir à quoi ça correspond si vous avez (déjà) oublié.
On doit parcourir notre tableau
carte à deux dimensions à l'aide d'une double boucle. On utilise la petite variable
i pour parcourir le tableau verticalement et la variable
j pour le parcourir horizontalement :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13 | // Recherche de la position de Mario au départ
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
{
if (carte[i][j] == MARIO) // Si Mario se trouve à cette position
{
positionJoueur.x = i;
positionJoueur.y = j;
carte[i][j] = VIDE;
}
}
}
|
À chaque case, on teste si elle contient
MARIO (c'est-à-dire le départ du joueur sur la carte). Si c'est le cas, on stocke les coordonnées actuelles (situées dans
i et
j) dans la variable
positionJoueur.
On efface aussi la case en la mettant à
VIDE pour qu'elle soit considérée comme une case vide par la suite.
Activation de la répétition des touches
Dernière chose, très simple : on active la répétition des touches pour qu'on puisse se déplacer sur la carte en laissant une touche enfoncée.
Code : C | // Activation de la répétition des touches
SDL_EnableKeyRepeat(100, 100);
|
La boucle principale
Pfiou ! Nos initialisations sont faites, on peut maintenant s'occuper de la boucle principale.
C'est une boucle classique qui fonctionne sur le même schéma que celles qu'on a vues jusqu'ici. Elle est juste un peu plus grosse et un peu plus complète (faut c'qui faut comme on dit !).
Regardons de plus près le
switch qui teste l'événement :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | switch(event.type)
{
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN:
switch(event.key.keysym.sym)
{
case SDLK_ESCAPE:
continuer = 0;
break;
case SDLK_UP:
marioActuel = mario[HAUT];
deplacerJoueur(carte, &positionJoueur, HAUT);
break;
case SDLK_DOWN:
marioActuel = mario[BAS];
deplacerJoueur(carte, &positionJoueur, BAS);
break;
case SDLK_RIGHT:
marioActuel = mario[DROITE];
deplacerJoueur(carte, &positionJoueur, DROITE);
break;
case SDLK_LEFT:
marioActuel = mario[GAUCHE];
deplacerJoueur(carte, &positionJoueur, GAUCHE);
break;
}
break;
}
|
Si on appuie sur la touche
Echap, le jeu s'arrêtera et on retournera au menu principal.
Comme vous le voyez, il n'y a pas 36 événements différents à gérer : on teste juste si le joueur appuie sur les touches « haut », « bas », « gauche » ou « droite » de son clavier.
Selon la touche enfoncée, on change la direction de Mario. C'est là qu'intervient
marioActuel ! Si on appuie vers le haut, alors :
Code : C | marioActuel = mario[HAUT];
|
Si on appuie vers le bas, alors :
Code : C | marioActuel = mario[BAS];
|
marioActuel pointe donc sur la surface représentant Mario dans la position actuelle. C'est ainsi qu'en blittant
marioActuel tout à l'heure, on sera certain de blitter Mario dans la bonne direction.
Maintenant, chose très importante : on appelle une fonction
deplacerJoueur. Cette fonction va déplacer le joueur sur la carte s'il a le droit de le faire.
- Par exemple, on ne peut pas faire monter Mario d'un cran vers le haut s'il se trouve déjà tout en haut de la carte.
- On ne peut pas non plus le faire monter s'il y a un mur au-dessus de lui.
- On ne peut pas le faire monter s'il y a deux caisses au-dessus de lui.
- Par contre, on peut le faire monter s'il y a juste une caisse au-dessus de lui.
- Mais attention, on ne peut pas le faire monter s'il y a une caisse au-dessus de lui et que la caisse se trouve au bord de la carte !
Oh la la, c'est quoi ce bazar ?
C'est ce qu'on appelle
la gestion des collisions. Si ça peut vous rassurer, ici c'est une gestion des collisions extrêmement simple, vu que le joueur se déplace par « cases » et dans seulement quatre directions possibles à la fois. Dans un jeu 2D où on peut se déplacer dans toutes les directions pixel par pixel, la gestion des collisions est bien plus complexe.
Mais il y a pire : la 3D. La gestion des collisions dans un jeu 3D est vraiment la bête noire des programmeurs. Heureusement, il existe des bibliothèques de gestion des collisions en 3D qui font le gros du travail à notre place.
Revenons à la fonction
deplacerJoueur et concentrons-nous. On lui envoie trois paramètres :
- la carte : pour qu'elle puisse la lire mais aussi la modifier, si on déplace une caisse par exemple ;
- la position du joueur : là aussi, la fonction devra lire et éventuellement modifier la position du joueur ;
- la direction dans laquelle on demande à aller : on utilise là encore les constantes HAUT, BAS, GAUCHE, DROITE pour plus de lisibilité.
Nous étudierons la fonction
deplacerJoueur plus loin. J'aurais très bien pu mettre tous les tests dans le
switch, mais celui-ci serait devenu énorme et illisible. C'est là que découper son programme en fonctions prend tout son intérêt.
Blittons, blittons, la queue du cochon
Notre
switch est terminé : à ce stade du programme, la carte et le joueur ont probablement changé. Quoi qu'il en soit, c'est l'heure du blit !
On commence par effacer l'écran en lui donnant une couleur de fond blanche :
Code : C | // Effacement de l'écran
SDL_FillRect(ecran, NULL, SDL_MapRGB(ecran->format, 255, 255, 255));
|
Et maintenant, on parcourt tout notre tableau à deux dimensions
carte pour savoir quel élément blitter à quel endroit sur l'écran.
On effectue une double boucle comme on l'a vu plus tôt pour parcourir toutes les 144 cases du tableau :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | // Placement des objets à l'écran
objectifsRestants = 0;
for (i = 0 ; i < NB_BLOCS_LARGEUR ; i++)
{
for (j = 0 ; j < NB_BLOCS_HAUTEUR ; j++)
{
position.x = i * TAILLE_BLOC;
position.y = j * TAILLE_BLOC;
switch(carte[i][j])
{
case MUR:
SDL_BlitSurface(mur, NULL, ecran, &position);
break;
case CAISSE:
SDL_BlitSurface(caisse, NULL, ecran, &position);
break;
case CAISSE_OK:
SDL_BlitSurface(caisseOK, NULL, ecran, &position);
break;
case OBJECTIF:
SDL_BlitSurface(objectif, NULL, ecran, &position);
objectifsRestants = 1;
break;
}
}
}
|
Pour chacune des cases, on prépare la variable
position (de type
SDL_Rect) pour placer l'élément actuel à la bonne position sur l'écran.
Le calcul est très simple :
Code : C | position.x = i * TAILLE_BLOC;
position.y = j * TAILLE_BLOC;
|
Il suffit de multiplier
i par
TAILLE_BLOC pour avoir
position.x.
Ainsi, si on se trouve à la troisième case, c'est que
i vaut 2 (n'oubliez pas que
i commence à 0 !). On fait donc le calcul 2 * 34 = 68. On blittera donc l'image 68 pixels vers la droite sur
ecran.
On fait la même chose pour les ordonnées
y.
Ensuite, on fait un
switch sur la case de la carte qu'on est en train d'analyser.
Là encore, avoir défini des constantes est vraiment pratique et rend les choses plus lisibles.
On teste donc si la case vaut
MUR, dans ce cas on blitte un mur. De même pour les caisses et les objectifs.
Test de victoire
Vous remarquerez qu'avant la double boucle, on initialise le booléen
objectifsRestants à 0.
Ce booléen sera mis à 1 dès qu'on aura détecté un objectif sur la carte. S'il ne reste plus d'objectifs, c'est que toutes les caisses sont sur des objectifs (il n'y a plus que des
CAISSE_OK).
Il suffit de tester si le booléen vaut « faux », c'est-à-dire s'il ne reste plus d'objectifs.
Dans ce cas, on met la variable
continuer à 0 pour arrêter la partie :
Code : C | // Si on n'a trouvé aucun objectif sur la carte, c'est qu'on a gagné
if (!objectifsRestants)
continuer = 0;
|
Le joueur
Il nous reste à blitter le joueur :
Code : C | // On place le joueur à la bonne position
position.x = positionJoueur.x * TAILLE_BLOC;
position.y = positionJoueur.y * TAILLE_BLOC;
SDL_BlitSurface(marioActuel, NULL, ecran, &position);
|
On calcule sa position (en pixels cette fois) en faisant une simple multiplication entre
positionJoueur et
TAILLE_BLOC. On blitte ensuite le joueur à la position indiquée.
Flip !
On a tout fait, il ne nous reste plus qu'à afficher l'écran au joueur :
Code : C
Fin de la fonction : déchargements
Après la boucle principale, on doit faire quelques
FreeSurface pour libérer la mémoire des sprites qu'on a chargés.
On désactive aussi la répétition des touches en envoyant les valeurs 0 à la fonction
SDL_EnableKeyRepeat :
Code : C | // Désactivation de la répétition des touches (remise à 0)
SDL_EnableKeyRepeat(0, 0);
// Libération des surfaces chargées
SDL_FreeSurface(mur);
SDL_FreeSurface(caisse);
SDL_FreeSurface(caisseOK);
SDL_FreeSurface(objectif);
for (i = 0 ; i < 4 ; i++)
SDL_FreeSurface(mario[i]);
|
La fonction deplacerJoueur
La fonction
deplacerJoueur se trouve elle aussi dans
jeu.c.
C'est une fonction… assez délicate à écrire. C'est peut-être la principale difficulté que l'on rencontre lorsqu'on code un jeu de Sokoban.
Rappel : la fonction
deplacerJoueur vérifie si on a le droit de déplacer le joueur dans la direction demandée. Elle met à jour la position du joueur (
positionJoueur) et aussi la carte si une caisse a été déplacée.
Voici le prototype de la fonction :
Code : C | void deplacerJoueur(int carte[][NB_BLOCS_HAUTEUR], SDL_Rect *pos, int direction);
|
Ce prototype est un peu particulier. Vous voyez que j'envoie le tableau
carte et que je précise la taille de la deuxième dimension (
NB_BLOCS_HAUTEUR).
Pourquoi cela ?
La réponse est un peu compliquée pour que je la développe au milieu de ce cours. Pour faire simple, le C ne devine pas qu'il s'agit d'un tableau à deux dimensions et il faut au moins donner la taille de la seconde dimension pour que ça fonctionne.
Donc, lorsque vous envoyez un tableau à deux dimensions à une fonction, vous devez indiquer la taille de la seconde dimension dans le prototype. C'est comme ça, c'est obligatoire.
Autre chose : vous noterez que
positionJoueur s'appelle en fait
pos dans cette fonction. J'ai choisi de raccourcir le nom parce que c'est plus court à écrire, et vu qu'on va avoir besoin de l'écrire de nombreuses fois, autant ne pas se fatiguer.
Commençons par tester la direction dans laquelle on veut aller via un grand
switch :
Code : C | switch(direction)
{
case HAUT:
/* etc. */
|
Et c'est parti pour des tests de folie !
Il faut maintenant écrire tous les tests de tous les cas possibles, en essayant de ne pas en oublier un seul.
Voici comment je procède : je teste toutes les possibilités de collision cas par cas, et dès que je détecte une collision (qui fait que le joueur ne peut pas bouger), je fais un
break; pour sortir du
switch, et donc empêcher le déplacement.
Voici par exemple toutes les possibilités de collision qui existent pour un joueur qui veut se déplacer vers le haut :
- le joueur est déjà tout en haut de la carte ;
- il y a un mur au-dessus du joueur ;
- il y a deux caisses au-dessus du joueur (et il ne peut pas déplacer deux caisses à la fois, rappelez-vous) ;
- il y a une caisse puis le bord de la carte.
Si tous ces tests sont ok, alors je me permets de déplacer le joueur.
Je vais vous montrer les tests pour un déplacement vers le haut. Pour les autres sens, il suffira d'adapter un petit peu le code.
Code : C | if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
break;
|
On commence par vérifier si le joueur est déjà tout en haut de l'écran. En effet, si on essayait d'appeler
carte[5][-1] par exemple, ce serait le plantage du programme assuré !
On commence donc par vérifier qu'on ne va pas « déborder » de l'écran.
Ensuite :
Code : C | if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
break;
|
Là encore c'est simple. On vérifie s'il n'y a pas un mur au-dessus du joueur. Si tel est le cas, on arrête (
break).
Ensuite (attention les yeux) :
Code : C | // Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y - 1] == CAISSE_OK) &&
(pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR ||
carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
break;
|
Ce gros test peut se traduire comme ceci : « SI au-dessus du joueur il y a une
caisse (ou une
caisse_ok, c'est-à-dire une caisse bien placée)
ET SI au-dessus de cette caisse il y a soit le vide (on déborde du niveau car on est tout en haut), soit une autre caisse, soit une caisse_ok :
ALORS on ne peut pas se déplacer :
break. »
Si on arrive à passer ce test, on a le droit de déplacer le joueur. Ouf !
On appelle d'abord une fonction qui va déplacer une caisse si nécessaire :
Code : C | // Si on arrive là, c'est qu'on peut déplacer le joueur !
// On vérifie d'abord s'il y a une caisse à déplacer
deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y - 2]);
|
Le déplacement de caisse : deplacerCaisse
J'ai choisi de gérer le déplacement de caisse dans une autre fonction car c'est le même code pour les quatre directions. On doit juste s'être assuré avant qu'on a le droit de se déplacer (ce qu'on vient de faire).
On envoie à la fonction deux paramètres : le contenu de la case dans laquelle on veut aller et le contenu de la case d'après.
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | void deplacerCaisse(int *premiereCase, int *secondeCase)
{
if (*premiereCase == CAISSE || *premiereCase == CAISSE_OK)
{
if (*secondeCase == OBJECTIF)
*secondeCase = CAISSE_OK;
else
*secondeCase = CAISSE;
if (*premiereCase == CAISSE_OK)
*premiereCase = OBJECTIF;
else
*premiereCase = VIDE;
}
}
|
Cette fonction met à jour la carte car elle prend en paramètres des pointeurs sur les cases concernées.
Je vous laisse la lire, c'est assez simple à comprendre. Il ne faut pas oublier que si on déplace une
CAISSE_OK, il faut remplacer la case où elle se trouvait par un
OBJECTIF. Sinon, si c'est une simple
CAISSE, alors on remplace la case en question par du
VIDE.
Déplacer le joueur
On retourne dans la fonction
deplacerJoueur.
Cette fois c'est la bonne, on peut déplacer le joueur.
Comment fait-on ? C'est très très simple :
Code : C | pos->y--; // On peut enfin faire monter le joueur (oufff !)
|
Il suffit de diminuer l'ordonnée y (car le joueur veut monter).
Résumé
En guise de résumé, voici tous les tests pour le cas HAUT :
Code : C 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | switch(direction)
{
case HAUT:
if (pos->y - 1 < 0) // Si le joueur dépasse l'écran, on arrête
break;
if (carte[pos->x][pos->y - 1] == MUR) // S'il y a un mur, on arrête
break;
// Si on veut pousser une caisse, il faut vérifier qu'il n'y a pas de mur derrière (ou une autre caisse, ou la limite du monde)
if ((carte[pos->x][pos->y - 1] == CAISSE || carte[pos->x][pos->y - 1] == CAISSE_OK) &&
(pos->y - 2 < 0 || carte[pos->x][pos->y - 2] == MUR ||
carte[pos->x][pos->y - 2] == CAISSE || carte[pos->x][pos->y - 2] == CAISSE_OK))
break;
// Si on arrive là, c'est qu'on peut déplacer le joueur !
// On vérifie d'abord s'il y a une caisse à déplacer
deplacerCaisse(&carte[pos->x][pos->y - 1], &carte[pos->x][pos->y - 2]);
pos->y--; // On peut enfin faire monter le joueur (oufff !)
break;
|
Je vous laisse le soin de faire du copier-coller pour les autres cas (attention, il faudra adapter le code, ce n'est pas exactement pareil à chaque fois !).
Et voilà, on vient de finir de coder le jeu !
Enfin presque : il nous reste à voir la fonction de chargement (et de sauvegarde) de niveaux.
On verra ensuite comment créer l'éditeur. Rassurez-vous, ça ira bien plus vite !