Aller au contenu

🐍 Projet en C : jeu Snake

Jeu du Snake
Le jeu Snake

1. Installer la bibliothÚque de développement

Pour réaliser le jeu, nous allons nous reposer sur une bibliothÚque de développement appelée SDL (Simple DirectMedia). Nous utiliserons la version 2. Un wiki est disponible en ligne pour lire la documentation.

Cette bibliothĂšque fournit des fonctions permettant assez facilement de crĂ©er une fenĂȘtre de d'y dessiner. Elle est capable d'utiliser les capacitĂ©s d'accĂ©lĂ©ration de la carte graphique Ă©ventuellement prĂ©sente dans l'ordinateur. Elle permet Ă©galement d'afficher des images, de jouer des sons, de gĂ©rer les entrĂ©es clavier, etc. Tout ce qu'il faut pour programmer un petit jeu vidĂ©o.

Sous Linux, l'installation de la bibliothĂšque est trĂšs simple puisqu'elle est normalement disponible dans les paquets de votre distribution. Par exemple, pour les distributions de la famille Debian (dont Ubuntu) la commande d'installation est :

sudo apt install libsdl2-dev libsdl2-2.0-0

On peut également installer certains paquets optionnels comme :

sudo apt install libsdl2-image-dev libsdl2-image-2.0-0
pour permettre d'afficher des images.

Pour vérifier si l'installation a fonctionné, on peut par exemple exécuter la commande :

sdl2-config --version

2. CrĂ©er une fenĂȘtre graphique

Pour nous familiariser avec SDL, nous allons commencer par crĂ©er une fenĂȘtre graphique, attendre 5 secondes, et quitter :

Fichier main.c minimal

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char * argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *fenetre = SDL_CreateWindow("Snake974", 50, 50, 640, 480, 0);

    SDL_Delay(5000); // Attendre 5 sec
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return EXIT_SUCCESS;
}

On a inclu les fichiers d'en-tĂȘte standard et celui correspondant Ă  la bibliothĂšque SDL. Nous notons que toutes les fonctions et noms concernant la bibliothĂšque SDL sont prĂ©fixĂ©s par SDL_.

Le programme précédent commence par initialiser SDL avec la fonction SDL_Init. Cette fonction a pour paramÚtre un entier qui représente les options d'initialisation. Pour construire cet entier, on se sert de constantes prédéfinies dans la bibliothÚque appelées drapeaux comme SDL_INIT_VIDEO pour signifier qu'on veut initialiser les graphismes. Il est possible de combiner plusieurs drapeaux avec l'opérateur | qui est le OU bit à bit. Par exemple, si on veut initialiser le son et la vidéo on écrit :

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);

On appelle ensuite la focntion SDL_CreateWindow permettant de crĂ©er une fenĂȘtre. Ses paramĂštres sont le titre de la fenĂȘtre, sa position (50, 50), ses dimensions 640x480. Le dernier paramĂštre est un entier pour les options que l'on fixe encore une fois Ă  l'aide de drapeaux. J'ai mis ici 0 pour dire que je voulais les options par dĂ©faut (aucun drapeau).

La suite du programme est claire : on demande une attente de 5000ms puis on fait le ménage en détruisant dans le bon ordre les objets construits précédemment. En effet, on se doute bien que la fonction SDL_CreateWindow alloue de la mémoire sur le tas et qu'il est nécessaire de la libérer avec une fonction de destruction, c'est le rÎle de SDL_DestroyWindow.

On peut compiler le jeu avec la commande :

Compilation avec SDL2
gcc main.c -o snake $(sdl2-config --cflags --libs)
On utilise ici l'outil sdl2-config qui construit automatiquement les paramÚtres à ajouter la ligne de compilation usuelle pour que le programme soit lié à la bibliothÚque SDL.

FenĂȘtre du jeu (vide pour l'instant)
FenĂȘre du jeu (vide pour l'instant)

3. La gestion des erreurs dans SDL

Les appels aux fonctions de la bibliothÚque SDL peuvent provoquer des erreurs. Dans ce cas, les fonctions retournent généralement un code d'erreur (valeur non nulle) ou un pointeur NULL selon les cas.

Il faut donc en théorie à chaque appel de fonction de SDL, vérifier sa valeur de retour. Pour nous faciliter la vie, la bibliothÚque SDL fournit la fonction SDL_GetError() qui retourne une chaßne de caractÚres décrivant l'erreur rencontrée. Voilà comment on peut adapter le code précédent :

Illustration du traitement d'erreurs

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char * argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO) != 0) {
        fprintf(stderr, "Erreur lors de l'initialisation : %s \n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    SDL_Window *fenetre = SDL_CreateWindow("Snake974", 50, 50, 640, 480, 0);
    if (fenetre == NULL) {
        fprintf(stderr, "Erreur lors de la crĂ©ation de la fenĂȘtre : %s \n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    SDL_Delay(5000); // Attendre 5 sec
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Avertissement

Dans toute la suite, pour me concentrer uniquement sur l'essentiel, je n'inclurai aucune autre vérification d'erreur SDL, mais cela doit faire partie du travail...

3. Construire et afficher la grille du jeu

Commençons maintenant à définir à quoi ressemblera le jeu. Notre jeu sera se déroulera dans une arene qui est une grille rectangulaire de blocs carrés. On notera ARENA_W la largeur de l'arene en nombre de blocs, ARENA_H sa hauteur et BLOCK_SIZE la longueur du cÎté d'un bloc en pixels.

L'arĂšne pourra contenir les types de bloc suivants :

  • VIDE : bloc vide
  • WALL : qui reprĂ©sente un mur
  • COCO : qui reprĂ©sente une noix de coco
  • SNAKE : qui reprĂ©sente une partie du corps du serpent

Cela fait de nombreuses constantes Ă  dĂ©finir, nous utilisons pour cela un fichier d'en-tĂȘte main.h pour dĂ©clarer les valeurs publiques (que tout le code peut du projet peut utiliser).

Fichier main.h

#ifndef MAIN_H
#define MAIN_H

// Position et dimensions de la fenetre
#define WIN_W 640
#define WIN_H 480
#define WIN_X 50
#define WIN_Y 50

// Dimensions de l'arene 
#define ARENA_W 20
#define ARENA_H 10
#define B_SIZE 20

// Types de blocs
#define VIDE 0
#define WALL 1
#define COCO 2
#define SNAKE 4

/* L'arene est définie comme une matrice
   de blocs */
typedef int Arena[ARENA_W][ARENA_H];

#endif

Il suffira ensuite d'inclure ce fichier d'en-tĂȘte dans n'importe quel fichier .c pour pouvoir utiliser ces dĂ©finitions :

#include "main.h"

Attention

Notez bien la présence de guillements "" et non de chevrons <> quand on inclus un fichier situé dans le répertoire courant.

Notez qu'on en a profitĂ© pour ajouter certains paramĂštres comme les dimensions de la fenĂȘtre. Il est toujours prĂ©fĂ©rable que de telles constantes n'apparaissent pas directement dans un code source afin de faciliter la lecture et la modification.

A. SystÚme de coordonnées

C'est le moment de faire le point sur le systÚme de coordonnées utilisé par SDL. SDL définit la position d'un pixel \((x, y)\) ainsi :

Coordonnées en SDL)
SystÚme de coordonnées en SDL (source : Wikipedia)

L'origine \((0, 0)\) est située en haut à gauche. La coordonnée \(x\) représente l'abscisse du point et la coordonnée \(y\) sont ordonnée, sauf que l'axe des ordonnées est orienté vers le bas.

Note

Nous utiliserons par la suite toujours ces conventions pour parler de coordonnées. En particulier les coordonnées des blocs. Ainsi le bloc de coordonnées (0, 0) sera celui situé en haut à gauche et le bloc de coordonnées (0, 1) celui situé juste en dessous de lui.

B. Dessin de l'arĂšne du jeu

Pour dessiner en SDL, il est nĂ©cessaire de crĂ©er un objet appelĂ© renderer qui est associĂ© Ă  une fenĂȘtre et qui est en charge du dessin dans cette fenĂȘtre. Le renderer est construit avec la fonction SDL_CreateRenderer et dĂ©truit avec la fonction SDL_DestroyRenderer. On met Ă  jour le code main.c ainsi :

Création et destruction du renderer

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char * argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *fenetre = SDL_CreateWindow("Snake974", 50, 50, 640, 480, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(fenetre, -1, SDL_RENDERER_ACCELERATED);

    SDL_Delay(5000); // Attendre 5 sec

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Il existe ensuite une multitude de fonctions de dessin, prenant en paramĂštre l'instance du renderer.

Fonction Effet
SDL_SetRenderDrawColor(renderer, r, g, b, a) Change la couleur de dessin \((r,g,b,a)\)
SDL_RenderClear(renderer) Efface tout et color le fond avec la couleur actuelle
SDL_RenderDrawPoint(renderer, x, y) Dessine un point
SDL_RenderDrawLine(renderer, x1, y1, x2, y2) Dessine une ligne
SDL_RenderDrawRect(renderer, &rect) Dessine un rectangle
SDL_RenderFillRect(renderer, &rect) Remplit un rectangle
SDL_RenderPresent(renderer) Met Ă  jour l'affichage

Attention

Noter l'importance de la fonction SDL_RenderPresent. Tout dessin ne sera visible qu'une fois la mise à jour de l'affichage effectué.

Notre grille étant rectangulaire, ces fonctions primitives de dessin devraient suffire. Voici quelques remarques pour bien comprendre ces fonctions :

Les couleurs

En SDL les couleurs sont codées sur 4 octets, c'est-à-dire 4 valeurs entiÚres \((r, g, b, a)\) entre 0 et 255 pour les composantes rouge (r), vert (g) et bleu (b); la composante alpha (a) représente le niveau de transparence (0 = transparent, 255 = opaque).

Les rectangles

SDL implémente son propre type SDL_Rect pour représenter un rectangle. Voici un exemple de fonctionnement :

SDL_Rect rectangle = {20, 45, 100, 200}; // (x, y, width, height)
SDL_RenderDrawRect(renderer, &rectangle);

Exercice : dessiner l'arĂšne

Étudiez le code ci-dessous, puis complĂ©ter la fonction draw_arena, qui s'occupe de dessiner l'arĂšne qu'on lui passe en paramĂštre. Cette fonction devra colorer chaque type de bloc avec une couleur diffĂ©rente. On rappelle que les blocs doivent avoir une taille de B_SIZE pixels.

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include "main.h"


/* Construit une arĂšne entiĂšrement vide */
void init_arena(Arena mat) {
    for (int x = 0; x < ARENA_W; x++) {
        for (int y = 0; y < ARENA_H; y++) {
            mat[x][y] = VIDE;
        }
    }
}

void draw_arena(SDL_Renderer *renderer, Arena mat) {
    /* A VOUS DE JOUER */
}

int main(int argc, char * argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *fenetre = SDL_CreateWindow("Snake974", WIN_X, WIN_Y, WIN_W, WIN_H, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(fenetre, -1, SDL_RENDERER_ACCELERATED);

    Arena mat;
    init_arena(mat);
    mat[3][5] = WALL; // Pour tester
    mat[6][6] = SNAKE; // Toujours pour tester
    mat[6][7] = SNAKE;
    mat[6][8] = SNAKE;
    mat[15][2] = COCO;
    draw_arena(renderer, mat);

    SDL_RenderPresent(renderer);

    SDL_Delay(5000); // Attendre 5 sec

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Voilà ce que vous devriez obtenir à l'état actuel du jeu :

ArÚne dessinée
Dessin de l'arĂšne
Aide

Si vous n'y parvenez pas, pensez le problĂšme ainsi :

  • commencez par choisir la couleur de fond puis tout effacer (clear)
  • on parcourt chaque case de la matrice mat on construit un bloc qui est un SDL_Rect de bonnes dimensions et placĂ© au bon endroit
  • On remplit (fill) bloc avec la bonne couleur
  • On dessine (draw) bloc avec la couleur de contour des blocs

Bon, nous avonçons bien mais il reste encore un petit problĂšme : il n'y a pas de murs au bord de notre grille de jeu : le serpent pourrait s'Ă©chapper. Écrivons une petite fonction pour ajouter des murs tout autour de l'arĂšne.

Exercice : mur d'enceinte

Écrire une fonction void mur_enceinte(Arena mat) qui assigne la valeur WALL à toutes les cases sur le pourtour de la grille mat.

4. Une structure de données pour le serpent

Dans cette partie, nous allons proposer une structure de donnĂ©es pour gĂ©rer le serpent. Le serpent peut ĂȘtre implĂ©mentĂ© sous forme d'une liste simplement chaĂźnĂ©e de blocs. Cette structure permet aisĂ©ment de faire Ă©voluer le serpent lorsqu'il avance ou lorsqu'il grandit.

A. Compilation séparée

Comme le code se complexifie, il serait bon de commencer Ă  le sĂ©parer dans plusieurs fichiers. On introduit un nouveau fichier d'en-tĂȘte :

Fichier serpent.h
#ifndef SERPENT_H
#define SERPENT_H

#include <stdbool.h>
#include "main.h"

struct maillon_s {
    /* Coordonnées du bloc */
    int x;
    int y;

    /* Maillon suivant (de la queue vers la tĂȘte) */
    struct maillon_s *suivant;
};
typedef struct maillon_s Maillon;

/* Sens de déplacement */
#define RIGHT 0
#define UP 1
#define DOWN 2
#define LEFT 3

struct serpent_s {
    Maillon *queue; /* Premier maillon : fin du serpent */
    Maillon *tete; /* Dernier maillon : tĂȘte du serpent */
    int direction; /* Sens de déplacement */
};
typedef struct serpent_s Serpent;

/* Coordonnées du bloc vers lequel va le serpent */
extern int prochain_x(const Serpent *s);
extern int prochain_y(const Serpent *s);

/* Teste si la case est occupée par le serpent */
extern bool appartient(const Serpent *s, int x, int y);

/* Fait avancer le serpent */
extern void avancer(Serpent *s);

/* Fait grandir le serpent */
extern void grandir(Serpent *s);

/* Créer un serpent de longueur 1 positionné en (x, y) et
   se déplacant dans la direction dir */
Serpent *creer_serpent(int x, int y, int dir);

/* Marque dans l'arene mat les cases occupees par le serpent */
extern void place_serpent(const Serpent *s, Arena mat);

#endif

Ce fichier d'en-tĂȘte dĂ©clare les structures de donnĂ©es implĂ©mentant le serpent ainsi que les fonctions utiles pour le manipuler. Ces fonctions seront implĂ©mentĂ©es dans le fichier serpent.c, ressemblant Ă  :

serpent.c
#include "serpent.h"

int prochain_x(const Serpent* s) {
    /* A VOUS DE JOUER */
}
/* etc */

Votre projet contient maintenant 4 fichiers : main.c, main.h, serpent.h, serpent.c. Et la compilation s'obtient avec les commandes :

    gcc -c main.c $(sdl2-config --cflags)
    gcc -c serpent.c
    gcc main.o serpent.o -o snake $(sdl2-config --cflags --libs)
Cela devient fastidieux ! De plus Ă  chaque modification du projet, on peut se torturer Ă  se demander quel(s) fichier(s) doivent ĂȘtre recompilĂ©s ou pas et dans quel ordre...

Nous allons utiliser le logiciel make pour nous faciliter la tĂąche. Pour fonctionner, cet outil a besoin de disposer d'un fichier nommĂ© Makefile Ă  la racine de notre projet. Ce fichier dĂ©crit un ensemble de cibles, c'est-Ă -dire de fichiers Ă  compiler, suivi de ses dĂ©pendances et de la commande de compilation. Écrivons ce fichier :

Fichier Makefile

Attention, les tabulations utilisĂ©es sont importantes et doivent ĂȘtre de vraies tabulation (pas une succession d'espaces). Il faudra aussi adapter les valeurs de CC, CFLAGS et LIBS selon votre propre configuration.

Makefile
CC=gcc # nom du compilateur

# Resultat de la commande sdl2-config --cflags
CFLAGS= -I/usr/local/include -I/usr/local/include/SDL2 -D_REENTRANT -D_THREAD_SAFE

# Resultat de la commande sdl2-config --libs
LIBS= -L/usr/local/lib -lSDL2

.PHONY: clean

snake: main.o serpent.o
    $(CC) main.o serpent.o -o snake $(CFLAGS) $(LIBS)

main.o: main.h serpent.h main.c
    $(CC) -c main.c $(CFLAGS)

serpent.o: main.h serpent.h serpent.c
    $(CC) -c serpent.c

clean:
    rm -f *.o

Une fois ce fichier renseignĂ©, l'appel Ă  la commande make compile automatiquement la premiere cible. Ici, la premiĂšre cible est l'exĂ©cutable snake. En cas de modification du code, make est capable de savoir exactement ce qui a besoin d'ĂȘtre recompilĂ© : cela Ă©vite de recompiler entiĂšrement un projet Ă  chaque une petite modification.

De plus nous avons ajouté une fausse (PHONY) cible clean qui fait le ménage en supprimant les fichiers intermédiaires devenus inutiles.

Résumons, désormais la commande make permet compiler ou recompuler tout le projet. La commande make clean permet de faire le ménage.

B. Implémentation des opérations

Nous allons maintenant implĂ©menter dans serpent.c les opĂ©rations dĂ©crites dans le fichier d'en-ĂȘte serpent.h. Rappelons les structures de donnĂ©es mises en jeu :

Rappel de serpent.h
    struct maillon_s {
        /* Coordonnées du bloc */
        int x;
        int y;

        /* Maillon suivant (de la queue vers la tĂȘte) */
        struct maillon_s *suivant;
    };
    typedef struct maillon_s Maillon;

    /* Sens de déplacement */
    #define RIGHT 0
    #define UP 1
    #define DOWN 2
    #define LEFT 3

    struct serpent_s {
        Maillon *queue; /* Premier maillon : fin du serpent */
        Maillon *tete; /* Dernier maillon : tĂȘte du serpent */
        int direction; /* Sens de déplacement */
    };
    typedef struct serpent_s Serpent;

Voici une reprĂ©sentation schĂ©matique d'un serpent qui occupe les blocs de coordonnĂ©es \((4,4)\), \((5, 4)\), \((6, 4)\) et qui se dĂ©place vers la droite. Notez bien que la liste commence avec l'Ă©lĂ©ment de fin du serpent, et remonte jusque la case de tĂȘte du serpent (en derniĂšre position dans la liste). La structure Serpent contient un pointeur permettant d'atteindre la tĂȘte du serpent en temps \(O(1)\).

flowchart LR
    direction LR
    subgraph Serpent
        queue
        tete
        direction[direction = RIGHT]
    end
    queue --> M1
    tete --> M3
    subgraph M1[Maillon]
        direction LR
        x1[x = 4]
        y1[y = 4]
        suivant1[suivant]
    end
    suivant1 --> M2
    subgraph M2[Maillon]
        direction LR
        x2[x = 5]
        y2[y = 4]
        suivant2[suivant]
    end
    suivant2 --> M3
    subgraph M3[Maillon]
        direction LR
        x3[x = 6]
        y3[y = 4]
        suivant3[suivant]
    end
    N@{shape: lin-rect, label: "NULL", color: blue}
    suivant3 --> N

Exercice : Implémentation des opérations

Dans le fichier serpent.c, implémentez les opérations :

  1. creer_serpent
  2. prochain_x et prochain_y
  3. appartient
  4. avancer
  5. grandir
  6. place_serpent

Des indications sont fournies ci-dessous.

Voici quelques indications pour vous aider dans votre tĂąche :

  1. creer_serpent : il faudra allouer de la mémoire sur le tas pour la structure et pour le premier et unique maillon
  2. prochain_x et prochain_y : les coordonnĂ©es de la prochaine case se calculent Ă  partir de celles de la tĂȘte et de la direciton actuelle
  3. appartient : c'est un parcours de liste
  4. avancer : c'est ici que la structure proposĂ©e est vraiment intĂ©ressante : il suffit de calculer la prochaine case occupĂ©e et d'ajouter un maillon pour cet emplacement qui sera la nouvelle tĂȘte, le maillon de queue quant Ă  lui sera supprimĂ© (ne pas oublier de libĂ©rer la mĂ©moire...).
  5. grandir : mĂȘme chose que l'opĂ©ration prĂ©cĂ©dente, mais on ne supprime pas cette fois l'Ă©lĂ©ment de queue : la longueur de la liste augmente donc de 1.
  6. place_serpent : pas de difficulté particuliÚre, on parcourt la liste et on modifie les cases correspondantes de l'arÚne.

Remarque

Pour le moment on ne se préoccupe pas de savoir si les déplacements sont effectivement possibles (murs, collisions, sortie de l'arÚne, ...).

Attention

Pensez bien aux cas limites de votre structure de liste !

Exercice : Nettoyage

Ajouter une opération destroy_serpent dont le but est de libérer la mémoire allouée par un serpent. Vous ajouterez cette opération dans le fichier serpent.h en vous inspirant des autres prototypes, puis vous coderez l'implémentation dans serpent.c.

C. Un serpent animé !

Nous sommes maintenant prĂȘt pour crĂ©er une petite animation de serpent qui se dĂ©place.

Nous allons commencer par créer un serpent de taille 1 en position (3, 3) avec un déplacement vers la droite, puis le faire grandir 5 fois pour obtenir un serpent de taille 5.

Ensuite, nous dessinons la scÚne du jeu : on part d'une arÚne vide, on ajoute le mur d'enceinte, puis on place le serpent. On met à jour l'affichage pour voir le résultat.

Pour faire déplacer le serpent, on utilise la fonction avance mais il faut alors recommencer le dessin complet de la scÚne : remettre tout à vide, mettre les murs, placer le serpent, actualiser l'affichage.

Bref, en réalisant plusieurs fois ces opérations et en ajoutant des petites pauses de 0,5s entre chaque image, on obtient une animation. Testez maintenant votre code avec l'animation proposée ci-dessous !

Example

fonction main
int main(int argc, char * argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *fenetre = SDL_CreateWindow("Snake974", WIN_X, WIN_Y, WIN_W, WIN_H, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(fenetre, -1, SDL_RENDERER_ACCELERATED);

    Serpent *s = creer_serpent(3, 3, RIGHT);
    for (int k = 0; k < 5; k++) {
        grandir(s);
    }

    Arena mat;

    for (int k = 0; k < 8; k++) {
        init_arena(mat);
        mur_enceinte(mat);
        place_serpent(s, mat);
        draw_arena(renderer, mat);
        SDL_RenderPresent(renderer);

        SDL_Delay(500);
        avancer(s);
    }
    s->direction = DOWN;
    for (int k = 0; k < 3; k++) {
        init_arena(mat);
        mur_enceinte(mat);
        place_serpent(s, mat);
        draw_arena(renderer, mat);
        SDL_RenderPresent(renderer);

        SDL_Delay(500);
        avancer(s);
    }
    s->direction = LEFT;
    for (int k = 0; k < 8; k++) {
        init_arena(mat);
        mur_enceinte(mat);
        place_serpent(s, mat);
        draw_arena(renderer, mat);
        SDL_RenderPresent(renderer);

        SDL_Delay(500);
        avancer(s);
    }


    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return EXIT_SUCCESS;
}

5. La boucle principale

Nous nous approchons du but. Il nous reste Ă  programmer la boucle principale du jeu. En effet, le jeu est en fait une boucle infinie dans laquelle on effectue les actions suivantes :

  1. Dessiner la scĂšne
  2. Attendre
  3. Lire et prendre en compte les actions du joueur
  4. Mettre Ă  jour le serpent

A. Lire les événements clavier

Les Ă©vĂ©nements sont toutes les actions extĂ©rieures qui peuvent ĂȘtre prises en compte par notre programme : appui sur des touches clavier, actions de la souris ou de la manette de jeu, utilisation de boutons de la fenĂȘtre, etc.

SDL a un fonctionnement trĂšs simple pour gĂ©rer les Ă©vĂ©nements. À chaque fois qu'un Ă©vĂ©nement se produit il est enregistrĂ© dans une file FIFO interne. Il existe ensuite une fonction de signature int SDL_PollEvent(SDL_Event *event) qui a pour but d'extraire un Ă©lĂ©ment de cette file. Elle retourne 1 si un Ă©vĂ©nement est effectivement dĂ©filĂ© et 0 si la file est vide.

De plus, cette fonction attend un argument un pointeur vers une structure de type SDL_Event qui permet d'enregistrer les informations sur l'Ă©vĂ©nement qui vient d'ĂȘtre extrait de la file. Notons event cette variable, nous aurons besoin des champs suivants :

  • event.type : dĂ©crit le type d'Ă©vĂ©nement produit. On Ă©coutera les Ă©vĂ©nements de type SDL_KEYDOWN (une touche est pressĂ©e)
  • event.key.keysim.sym : dans le cas oĂč l'Ă©vĂ©nement concerne une touche, ce champ contient le caractĂšre concernĂ©

Pour résumer, voilà à quoi ressemble notre boucle principale :

Boucle principale

int main(int argc, char * argv[]) {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *fenetre = SDL_CreateWindow("Snake974", WIN_X, WIN_Y, WIN_W, WIN_H, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(fenetre, -1, SDL_RENDERER_ACCELERATED);

    Serpent *s = creer_serpent(3, 3, RIGHT);
    for (int k = 0; k < 5; k++) {
        grandir(s);
    }

    Arena mat;

    bool run = true;

    while (run) {
        init_arena(mat);
        mur_enceinte(mat);
        place_serpent(s, mat);
        draw_arena(renderer, mat);
        SDL_RenderPresent(renderer);

        SDL_Delay(200);
        SDL_Event event;
        while (SDL_PollEvent(&event) != 0) {
            if (event.type == SDL_KEYDOWN) {
                fprintf(stderr, "Touche pressée : %c \n", event.key.keysym.sym); 
            }
            if (event.key.keysym.sym == 'x') {
                run = false; // touche pour quitter le jeu
            }
        }
        avancer(s);
    }

    destroy_serpent(s);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return EXIT_SUCCESS;
}

Dans le code proposĂ©, le jeu est une boucle principale qui fait avancer le serpent Ă  chaque itĂ©ration et qui s'arrĂȘte lorsque le joueur appuie sur la touche x.

Exercice : Diriger le serpent

Modifiez la boucle principale proposée pour qu'elle puisse prendre en compte des commandes de déplacement q (gauche), s (bas), d (droite), z (haut). L'action a effectué consiste à modifier la direction de déplacement du serpent.

Et voilĂ , notre jeu commence Ă  ĂȘtre jouable ! Essayez et Ă©tudiez le comportement du jeu lorsque le serpent percute un mur, marche sur lui-mĂȘme ou sors de la zone de jeu...

B. Gestion des collisions

Nous allons maintenant gérer la mise à jour du serpent. Pour l'instant le serpent ne fait qu'avancer... nous allons affiner son comportement.

Exercice : Gestion des collisions

Modifiez votre code pour que le jeu s'arrĂȘte lorsque le bloc vers lequel on va avancer est un mur ou appartient dĂ©jĂ  au serpent. Dans ce cas, la partie est perdue.

C. Gestion de la noix de coco

Exercice : Noix de coco

  1. Ajouter dans votre fonction main deux variables locales :
    int coco_x;
    int coco_y;
    
    qui reprĂ©sentent la position d'une noix de coco. Cette position doit ĂȘtre choisie alĂ©atoirement pour correspondre Ă  une case vide de l'arĂšne.
  2. Programmer le comportement suivant : si la case suivante est une noix de coco, alors plutĂŽt qu'avancer on fait grandir le serpent. On choisit ensuite une nouvelle position valide de noix de coco.

6. Aller plus loin

Voilà ! vous avez un jeu fonctionnel minimal et il vous appartient maintenant de le faire évoluer ! Voici des pistes non limitatives :

  • Programmer une touche pause
  • Proposer des notions de score et de niveau. Plus le niveau augmente, plus le serpent est rapide.
  • Ajouter des sons.
  • Si la noix de coco reste trop longtemps en jeu alors elle devient un cocotier (= 1 mur)
  • PossibilitĂ© d'avoir plusieurs noix de coco
  • PossibilitĂ© d'avoir des pommes empoisonnĂ©es en plus des noix de coco
  • PossibilitĂ© d'avoir un bonus Ă  ramasser qui fait diminuer la longueur du serpent
  • ...