Créer un jeu du serpent en C

CBeginner
Pratiquer maintenant

Introduction

Dans ce projet, vous apprendrez à créer un simple jeu du serpent en C en utilisant la bibliothèque ncurses. Ce jeu classique consiste à contrôler un serpent pour qu'il mange de la nourriture, s'allonge, tout en évitant les collisions avec les murs et lui-même. La fonctionnalité du jeu est divisée en plusieurs composants clés : initialisation, boucle de jeu, mouvement du serpent, détection de collisions, etc. À la fin de ce projet, vous aurez un jeu du serpent de base qui peut être exécuté dans un terminal.

👀 Aperçu

Snake Game

🎯 Tâches

Dans ce projet, vous apprendrez :

  • Comment implémenter la boucle de jeu pour mettre à jour la position du serpent et gérer les entrées utilisateur.
  • Comment créer des fonctions pour initialiser le jeu, dessiner la fenêtre de jeu et afficher les messages de fin de jeu.
  • Comment implémenter la détection de collisions pour vérifier les collisions avec les murs, le corps du serpent lui-même et la nourriture.
  • Comment développer des fonctionnalités telles que l'augmentation de la longueur du serpent lorsqu'il mange de la nourriture.

🏆 Réalisations

Après avoir terminé ce projet, vous serez en mesure de :

  • Utiliser la bibliothèque ncurses en C pour créer un jeu basé sur le terminal.
  • Implémenter la logique du jeu, y compris la mise à jour de l'état du jeu et la gestion des entrées utilisateur.
  • Créer et manipuler des structures de données pour représenter les objets du jeu, tels que le serpent et la nourriture.
  • Implémenter la détection de collisions pour définir les règles du jeu et déterminer quand le jeu doit se terminer.

Connaissances de base

À l'époque de l'utilisation généralisée des télécopieurs, les télécopieurs servaient de terminaux de sortie connectés à des ordinateurs centraux par des câbles. Les utilisateurs devaient envoyer une série de commandes de contrôle spécifiques au programme terminal pour contrôler l'affichage sur l'écran du terminal. Par exemple, changer la position du curseur sur l'écran, effacer le contenu d'une certaine zone de l'écran, faire défiler l'écran, changer le mode d'affichage, souligner le texte, changer l'apparence, la couleur, la luminosité, etc. des caractères. Ces contrôles sont mis en œuvre grâce à une chaîne appelée une séquence d'échappement. Les séquences d'échappement sont ainsi appelées car ces octets consécutifs commencent par un caractère 0x1B, qui est le caractère d'échappement (le caractère entré en appuyant sur la touche ESC). Même aujourd'hui, nous pouvons simuler l'effet d'affichage des terminaux télécopieurs de cette époque en entrant des séquences d'échappement dans des programmes d'émulation de terminal. Si vous souhaitez afficher un texte sur le terminal (ou le programme d'émulation de terminal) avec un arrière-plan coloré, vous pouvez entrer la séquence d'échappement suivante dans votre invite de commande :

echo "^[[0;31;40mIn Color"

Ici, ^ et [ sont les caractères d'échappement. (Notez que dans ce cas, ^[ est un seul caractère. Il n'est pas entré en tapant successivement les caractères ^ et [. Pour imprimer ce caractère, vous devez d'abord appuyer sur Ctrl+V, puis sur la touche ESC.) Après avoir exécuté la commande ci-dessus, vous devriez voir l'arrière-plan de In Color changer en rouge. À partir de là, tout le texte affiché sera sorti avec cet effet. Si vous souhaitez terminer cet effet et revenir au format d'origine, vous pouvez utiliser la commande suivante :

echo "^[[0;37;40m"

Maintenant, savez-vous à quoi servent ces caractères (séquences d'échappement)? (Essayez de changer les paramètres entre les points-virgules et voyez quels résultats vous obtenez.) Peut-être sera-t-il différent de ce que vous imaginez? Cela peut être dû au fait que l'environnement du terminal est différent, ce qui dépend des différents terminaux ou systèmes d'exploitation. (Vous ne pouvez pas faire afficher des caractères colorés sur un terminal monochrome, n'est-ce pas?) Afin d'éviter ces problèmes de compatibilité et d'obtenir une sortie cohérente sur différents terminaux, les concepteurs d'UNIX ont inventé un mécanisme appelé termcap. termcap est en fait un fichier qui est publié avec les séquences d'échappement. Ce fichier répertorie toutes les séquences d'échappement que le terminal actuel peut exécuter correctement, garantissant que le résultat d'exécution des séquences d'échappement entrées correspond aux spécifications de ce fichier. Cependant, au cours des années suivant l'invention de ce mécanisme, un autre mécanisme appelé terminfo a progressivement remplacé termcap. Depuis, les utilisateurs n'ont plus besoin de consulter les spécifications complexes des séquences d'échappement dans termcap lors de la programmation, et n'ont qu'à accéder à la base de données terminfo pour contrôler l'affichage de l'écran.

En supposant que toutes les applications accèdent à la base de données terminfo pour contrôler la sortie (par exemple, en envoyant des caractères de contrôle, etc.) dans le cas de l'utilisation de terminfo, ces appels de code rendront bientôt tout le programme difficile à contrôler et à gérer. L'apparition de ces problèmes a conduit à la naissance de CURSES. Le nom CURSES vient d'un calembour appelé cursor optimization (optimisation du curseur).

La bibliothèque CURSES offre aux utilisateurs une API (interface de programmation d'application) flexible et efficace en encapsulant les codes de contrôle de bas niveau (séquences d'échappement) du terminal, ce qui permet aux utilisateurs de contrôler le curseur, de créer des fenêtres, de changer les couleurs de premier et de second plan et de gérer les opérations de la souris. Cela permet aux utilisateurs d'éviter ces mécanismes de bas niveau ennuyeux lorsqu'ils écrivent des applications sur des terminaux à caractères.

NCURSES est une version clonée de CURSES issue de System V Release 4.0 (SVr4). C'est une bibliothèque librement configurable qui est entièrement compatible avec les anciennes versions de CURSES. En résumé, c'est une bibliothèque qui permet aux applications de contrôler directement l'affichage de l'écran du terminal. Lorsque la bibliothèque CURSES est mentionnée par la suite, cela fait également référence à la bibliothèque NCURSES.

NCURSES non seulement encapsule les fonctions de bas niveau du terminal, mais offre également un cadre de travail assez stable pour générer de belles interfaces. Elle inclut des fonctions pour créer des fenêtres. Et ses bibliothèques sœurs, Menu, Panel et Form, sont des extensions de la bibliothèque de base CURSES. Ces bibliothèques sont généralement distribuées avec CURSES. Nous pouvons construire une application qui contient plusieurs fenêtres, menus, panneaux et formulaires. Les fenêtres peuvent être gérées indépendamment, par exemple en les faisant défiler ou en les masquant. Les menus permettent aux utilisateurs de créer des options de commande pour exécuter facilement des commandes. Les formulaires permettent aux utilisateurs de créer des fenêtres pour une saisie et un affichage de données simples. Les panneaux sont des extensions des fonctions de gestion des fenêtres de NCURSES et peuvent superposer ou empiler des fenêtres.

✨ Vérifier la solution et pratiquer

Définir des constantes

Tout d'abord, ouvrez le terminal et exécutez la commande suivante pour installer la bibliothèque ncurses :

sudo apt-get update
sudo apt-get install libncurses5-dev

Accédez au répertoire ~/project et créez le fichier de projet snake.c :

cd ~/project
touch snake.c

Ensuite, nous devons écrire le code C. La première étape consiste à inclure les fichiers d'en-tête :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ncurses.h>

Avant d'écrire la fonction main(), effectuons quelques tâches de base. Étant donné que nous utilisons une interface en caractères de terminal, les caractères ASCII sont essentiels. Par conséquent, nous devons définir certaines constantes :

#define TBool            int
#define True             1
#define False            0
#define SHAPE_FOOD       '@'  // Nourriture
#define SHAPE_SNAKE      '#'  // Corps du serpent
#define GAMEWIN_YLEN     15
#define GAMEWIN_XLEN     60
#define LOGWIN_YLEN      7
#define LOGWIN_XLEN      (GAMEWIN_XLEN)
#define LOGBUF_NUM       (LOGWIN_YLEN-2)
#define LOGBUF_LEN       (GAMEWIN_XLEN-2)
#define MAXLEVEL         12

#define GetSnakeTail(s)  ((s)->head->front)

WINDOW *logwin; // Déclaration d'une fenêtre de journal
#define INITRUNLOG()     logwin = newlogw() // Création d'une fenêtre de journal en appelant la fonction personnalisée newlogw()
#define RUNLOG(str)      runlog(logwin, str) // Exécution de la fenêtre de journal pour afficher les messages du jeu
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // Niveau du joueur, une variable globale

Nous avons également ajouté quelques définitions de structures et d'énumérations :

enum TDirection {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
};

struct TFood {
    int y;
    int x;
};

struct TSnakeNode {
    int y;
    int x;
    struct TSnakeNode *front;
};

struct TSnake {
    int    length;
    struct TSnakeNode *head;
    enum   TDirection  dir;
};

Maintenant, nous allons déclarer les fonctions que nous allons créer :

int refreshgamew(WINDOW *win, struct TSnake *psnake);
void movesnake(struct TSnake *psnake);
int checksnake(struct TFood *pfood, struct TSnake *psnake);
void snakegrowup(struct TFood *pfood, struct TSnake *psnake);
void gameover(WINDOW *win, char *str);
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x);
WINDOW* newgamew();
struct TSnake* initsnake();
void destroysnake(struct TSnake *psnake);
void drawsnakew(WINDOW *win, struct TSnake *psnake);
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake);
TBool checkfood(struct TFood *pfood, struct TSnake *psnake);
WINDOW* newlogw();
void runlog(WINDOW *win, char *str);
void cleanline(WINDOW *win, int y, int x);
✨ Vérifier la solution et pratiquer

Fonction principale

int main()
{
    initscr();  /* Initialisation, entrée en mode ncurses */
    raw();      /* Désactivation de la mise en mémoire tampon par ligne, affichage immédiat des résultats */
    noecho();   /* Ne pas afficher les caractères de contrôle sur le terminal, comme Ctrl+C */
    keypad(stdscr, TRUE);   /* Autoriser l'utilisateur à utiliser le clavier dans le terminal */
    curs_set(0);    /* Définir la visibilité du curseur, 0 pour invisible, 1 pour visible, 2 pour complètement visible */
    refresh();      /* Écrire le contenu de l'écran virtuel sur l'affichage et rafraîchir */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  Appuyez sur 'q' ou 'Q' pour quitter.");
    RUNLOG("  Appuyez sur 'w/s/a/d' ou 'W/S/A/D' pour déplacer le serpent.");
    RUNLOG("Info:");

    WINDOW *gwin = newgamew(); /* Création de la fenêtre de jeu, implémentée par une fonction personnalisée appelée newgamew */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

    while (refreshgamew(gwin, psnake) >= 0)
        ;

    /* getch() est différent de getchar() */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* Effacer la fenêtre de jeu et libérer la mémoire et les informations de la structure de données de la fenêtre */
    DESTROYRUNLOG(); /* Effacer la fenêtre d'affichage des informations */
    endwin();        /* Quitter le mode ncurses */

    return 0;
}

Dans keypad(stdscr, TRUE), stdscr fait référence à une fenêtre virtuelle où toutes nos opérations sont d'abord écrites, puis le contenu de stdscr est affiché sur l'écran à l'aide de la fonction refresh.

Lorsque nous utilisons printw, les données sont en fait imprimées sur une fenêtre virtuelle appelée stdscr, qui n'est pas directement affichée à l'écran. Le but de la fonction printw() est d'écrire continuellement des marqueurs d'affichage et des structures de données associées sur l'affichage virtuel et d'écrire ces données dans le tampon de stdscr. Ainsi, pour afficher les données de ce tampon, nous devons utiliser la fonction refresh() pour indiquer au système curses d'afficher le contenu du tampon à l'écran. Ce mécanisme permet au programmeur d'écrire continuellement des données sur l'écran virtuel et de les afficher toutes d'un coup lorsque refresh() est appelée. Étant donné que la fonction refresh() ne vérifie que les parties de la fenêtre et des données qui ont changé, cette conception flexible offre un mécanisme de rétroaction efficace.

✨ Vérifier la solution et pratiquer

Mécanisme de fenêtre

Le mécanisme des fenêtres est un concept clé de CURSES. Comme vous l'avez vu dans les exemples précédents, toutes les fonctions opèrent par défaut sur une « fenêtre » (stdscr). Même si vous concevez l'interface utilisateur graphique (GUI) la plus simple, vous devez toujours utiliser des fenêtres. L'une des principales raisons d'utiliser des fenêtres est que vous pouvez diviser l'écran en différentes parties et y opérer simultanément. Cela peut améliorer l'efficacité. Une autre raison est que vous devriez toujours chercher à concevoir votre programme de manière plus efficace et plus gérable. Si vous concevez une interface utilisateur large et complexe, concevoir ces parties à l'avance améliorera votre efficacité.

WINDOW* newlogw()
{
    /* Les paramètres sont : hauteur, largeur, position de départ (y,x) de la fenêtre */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* Les paramètres sont : pointeur de la fenêtre connue, 0 et 0 sont les positions de départ par défaut des caractères en ligne et en colonne */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // Rafraîchir la fenêtre spécifiée

    return win;
}

Dans la fonction WINDOW* newlogw(), vous pouvez voir que la création d'une fenêtre commence par newwin(). Bien que nous ayons créé une fenêtre, elle n'est pas encore visible, ce qui est similaire à un élément <div> en HTML. Si vous n'ajoutez pas de styles à un élément <div>, vous ne verrez rien sur la page web. Nous devons donc utiliser la fonction box pour ajouter des bordures à la fenêtre connue.

mvwprintw imprime le contenu spécifié aux coordonnées (y,x) spécifiées dans la fenêtre spécifiée.

✨ Vérifier la solution et pratiquer

Afficher les informations du jeu

RUNLOG est une macro qui appelle la fonction personnalisée runlog.

void runlog(WINDOW *win, char *str)
{
    static char logbuf[LOGBUF_NUM][LOGBUF_LEN] = {0};
    static int  index = 0;

    strcpy(logbuf[index], str);

    int i = 0;

    /* #define LOGBUF_NUM  (LOGWIN_YLEN-2); LOGBUF_NUM=5 */
    for (; i < LOGBUF_NUM; ++i) {

        /* Fonction personnalisée, cleanline */
        cleanline(win, i+1, 1);

        /* Afficher la chaîne de caractères sur chaque ligne */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

    index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}

runlog prend une fenêtre connue et les informations à afficher comme paramètres. La chaîne str sera stockée dans un tableau bidimensionnel (logbuf) à l'aide de la fonction strcpy. Ensuite, la fonction personnalisée cleanline est appelée pour effacer la ligne qui sera imprimée ensuite. La fonction mvwprintw est utilisée pour afficher les informations.

/* Effacer les coordonnées (x,y) de la fenêtre win */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* Initialiser les positions 0-56 du tableau avec des caractères d'espace */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* Déplacer le curseur à la position (y,x) dans la fenêtre win et afficher la chaîne EMPTYLINE */
    mvwprintw(win, y, x, EMPTYLINE);

    /* Afficher le contenu sur la fenêtre spécifiée */
    wrefresh(win);
}
✨ Vérifier la solution et pratiquer

Initialisation de la file d'attente du serpent

Maintenant, vous allez définir la structure de données du serpent et créer des fonctions pour l'initialiser et le dessiner dans la fenêtre de jeu.

struct TSnake* initsnake()
{
    struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // Initialiser la longueur du serpent à 4

    newsnakenode (
        &newsnakenode (
            &newsnakenode (
                &newsnakenode( &psnake->head, 4, 50 )->front, 4, 53
            )->front, 4, 52
        )->front, 4, 51
    )->front = psnake->head;

    return psnake;
}

struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x)
{
    *ppsnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
    (*ppsnode)->y = y;
    (*ppsnode)->x = x;
    (*ppsnode)->front = NULL;

    return *ppsnode;
}

Le but de ces deux fonctions est d'initialiser la structure de données du serpent dans un jeu de serpent, y compris la définition de la direction et de la longueur initiales du serpent, ainsi que la création d'un serpent avec un nœud de corps initial. La fonction newsnakenode est utilisée pour créer des nœuds individuels, puis ces nœuds sont connectés ensemble via des appels imbriqués de fonctions pour former le serpent initial.

✨ Vérifier la solution et pratiquer

Afficher le serpent dans la fenêtre de jeu

void drawsnakew(WINDOW *win, struct TSnake *psnake)
{
    static int taily = 0;
    static int tailx = 0;
    if (taily!= 0 && tailx!= 0) {
        mvwaddch(win, taily, tailx, ' ');
    }

    /* #define GetSnakeTail(s)  ((s)->head->front) */
    struct TSnakeNode *psnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i) {
        mvwaddch(win, psnode->y, psnode->x, SHAPE_SNAKE);
        psnode = psnode->front;
    }

    taily = GetSnakeTail(psnake)->y;
    tailx = GetSnakeTail(psnake)->x;

    wrefresh(win);
}

Pour afficher les coordonnées (y,x) de chaque nœud, nous pouvons utiliser la définition de macro GetSnakeTail pour récupérer les coordonnées. L'étape restante consiste à utiliser une boucle pour les afficher à l'aide de mvwaddch. mvwaddch est utilisé pour déplacer le curseur à la position spécifiée (taily,tailx) dans la fenêtre spécifiée (win), puis pour afficher un caractère.

Pour l'afficher réellement à l'écran, nous devons utiliser wrefresh(win).

✨ Vérifier la solution et pratiquer

Écrire le noyau du jeu

Regardez le point-virgule après while (refreshgamew(gwin, psnake) >= 0);. Cela établit une boucle pour gérer le mouvement et le changement de longueur du serpent, ainsi que la détection des limites et d'autres problèmes.

La boucle while appelle la fonction refreshgamew :

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* Lorsque le jeu démarre ou lorsque la nourriture est mangée, ffood=False, et drawfoodw est exécuté pour redessiner la nourriture */
    if (!ffood) {
        drawfoodw(win, &pfood, psnake);
        ffood = True;
    }
    int key = -1;

    fd_set set;
    FD_ZERO(&set);
    FD_SET(0, &set);

    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec= (6 - (int)(g_level/3)) * 100*1000;

    // Ensuite, nous allons utiliser l'API noyau select, qui écoutera une série d'entrées clavier et souris. Nous utilisons `key=getch()` et `switch` pour déterminer le type d'entrée. Le deuxième `switch` est utilisé pour afficher le niveau et la vitesse de déplacement actuelle du serpent.
    if (select(1, &set, NULL, NULL, &timeout) < 0)
        return -1;

    if (FD_ISSET(0, &set)) {
        while ((key = getch()) == -1);

        switch (key) {
        case 'w':
        case 'W':
            (psnake->dir == DIR_DOWN)? : (psnake->dir = DIR_UP);
            break;

        case 's':
        case 'S':
            (psnake->dir == DIR_UP)? : (psnake->dir = DIR_DOWN);
            break;

        case 'a':
        case 'A':
            (psnake->dir == DIR_RIGHT)? : (psnake->dir = DIR_LEFT);
            break;

        case 'd':
        case 'D':
            (psnake->dir == DIR_LEFT)? : (psnake->dir = DIR_RIGHT);
            break;

        case 'q':
        case 'Q':
            RUNLOG("Quit Game!");
            gameover(win, "Quit Game!");
            return -1;

        default:
            break;
        }
    }
    movesnake(psnake);
    drawsnakew(win, psnake);
    switch (checksnake(&pfood, psnake)) {
    case 0:
        break;

    // La nourriture a été mangée, définissez ffood sur 0 pour redessiner la nourriture.
    case 1:
        ffood = False;
        if (++g_level > MAXLEVEL) {
            RUNLOG("Win!!!");
            gameover(win, "Win!!!");
            return -1;
        }
        mvwprintw(win, GAMEWIN_YLEN-1, 2, " Level: %d ", g_level);
        mvwprintw(win, GAMEWIN_YLEN-1, 30, " Speed: %d ", (int)(g_level/3));
        wrefresh(win);
        RUNLOG("Level UP!");
        snakegrowup(&pfood, psnake);
        break;

    default:
        RUNLOG("Game over!");
        gameover(win, "Game over!");
        return -1;
    }

    return 1;
}
✨ Vérifier la solution et pratiquer

Le mouvement du serpent gourmand

Le mouvement du serpent 贪吃蛇 (Greedy Snake) est implémenté à l'aide de la fonction movesnake. Nous allons analyser le segment case DIR_UP, car les autres sont similaires :

/* La structure TSnake est une liste chaînée inversée avec la tête et la queue connectées.
 * Exemple : [a]<-[b]<-[c]<-[d]    a est la tête
 *          |              ^     Lorsque le serpent se déplace, seule la tête pointe vers d,
 *          `--------------'     et les coordonnées (y,x) de d sont modifiées pour correspondre à la position où la tête se déplace. */
void movesnake(struct TSnake *psnake)
{
    int hy = psnake->head->y;
    int hx = psnake->head->x;

    psnake->head = GetSnakeTail(psnake);

    switch (psnake->dir) {
    case DIR_UP:
        psnake->head->y = hy - 1;
        psnake->head->x = hx;
        break;

    case DIR_DOWN:
        psnake->head->y = hy + 1;
        psnake->head->x = hx;
        break;

    case DIR_LEFT:
        psnake->head->y = hy;
        psnake->head->x = hx - 1;
        break;

    case DIR_RIGHT:
        psnake->head->y = hy;
        psnake->head->x = hx + 1;
        break;

    default:
        break;
    }
}

Étant donné que l'origine des coordonnées est située dans le coin supérieur gauche, avec les valeurs de (y,x) augmentant vers le bas et vers la droite, le système de coordonnées est retourné de 180 degrés le long de l'axe des x. Par conséquent, pour faire déplacer le serpent vers le haut, nous soustrayons 1. Ainsi, lorsque le joueur appuie sur la touche w/W, le serpent se déplacera vers le haut.

✨ Vérifier la solution et pratiquer

Allongement du corps du serpent

Pour augmenter la longueur du corps du serpent, vous pouvez le faire en implémentant la fonction snakegrowup, qui est relativement simple. Il suffit d'ajouter un nouveau nœud à la file.

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // Allouer de la mémoire pour un nouveau nœud de serpent (struct TSnakeNode) et assigner le pointeur pnode au nouveau nœud. Ce nouveau nœud deviendra la nouvelle tête du serpent.
    struct TSnakeNode *pnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));

    switch (psnake->dir) {
    case DIR_UP:
        pnode->y = psnake->head->y - 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_DOWN:
        pnode->y = psnake->head->y + 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_LEFT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x - 1;
        break;

    case DIR_RIGHT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x + 1;
        break;

    default:
        break;
    }

    // Définir le pointeur front du nouveau nœud (pnode) sur la queue actuelle du serpent afin d'établir une connexion entre la nouvelle tête et la queue.
    pnode->front = GetSnakeTail(psnake);

    // Définir le pointeur front de la tête actuelle du serpent sur le nouveau nœud pour s'assurer que le nouveau nœud devient la nouvelle tête.
    psnake->head->front = pnode;
    psnake->head = pnode;

    // Augmenter la longueur du serpent pour indiquer que le corps du serpent s'est allongé d'une unité.
    ++psnake->length;
}

L'instruction switch détermine les coordonnées du nouveau nœud de tête en fonction de la direction de mouvement actuelle du serpent (psnake->dir). Selon les différentes directions, elle met à jour les coordonnées du nouveau nœud pour le déplacer d'une case vers le haut, le bas, la gauche ou la droite par rapport à la tête actuelle du serpent (psnake->head).

Le but de ce code est d'augmenter la longueur du corps du serpent dans le jeu du serpent. Il crée un nouveau nœud de tête en fonction de la direction de mouvement actuelle du serpent et insère ce nouveau nœud de tête à l'avant du serpent, rendant le corps du serpent plus long. C'est la logique de base pour augmenter la longueur du corps du serpent après qu'il a consommé de la nourriture dans le jeu du serpent.

✨ Vérifier la solution et pratiquer

L'emplacement de production de la nourriture

La fonction drawfoodw est utilisée pour dessiner la nourriture dans la fenêtre de jeu (win) :

void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake)
{
    do {
        pfood->y = random() % (GAMEWIN_YLEN - 2) + 1;
        pfood->x = random() % (GAMEWIN_XLEN - 2) + 1;
    } while (False == checkfood(pfood, psnake));
    checkfood(pfood, psnake);

    mvwaddch(win, pfood->y, pfood->x, SHAPE_FOOD);
    wrefresh(win);
}
  • La fonction random() est utilisée pour générer des valeurs de coordonnées aléatoires, ce qui fera apparaître la nourriture à une position aléatoire dans la fenêtre de jeu.
  • En utilisant une boucle do-while, elle vérifie si la position de la nourriture générée chevauche un nœud du corps du serpent. S'il y a un chevauchement, elle génère une nouvelle position pour la nourriture jusqu'à trouver une position qui ne chevauche pas le corps du serpent.
  • La fonction mvwaddch est utilisée pour dessiner la forme de la nourriture à la position spécifiée dans la fenêtre de jeu.

La fonction checkfood est utilisée pour vérifier que la nourriture n'apparaît pas sur le serpent :

TBool checkfood(struct TFood *pfood, struct TSnake *psnake)
{
    struct TSnakeNode *pnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i, pnode = pnode->front)
        if (pfood->y == pnode->y && pfood->x == pnode->x)
            return False;

    return True;
}
  • Tout d'abord, elle obtient le nœud de queue du serpent. Ensuite, elle utilise une boucle pour parcourir tous les nœuds du corps du serpent et vérifie si les coordonnées de la nourriture correspondent aux coordonnées de n'importe quel nœud.
  • Si les coordonnées de la nourriture correspondent aux coordonnées d'un nœud du serpent, cela signifie que la position de la nourriture chevauche un nœud du corps du serpent, et la fonction retourne False.
  • Si aucun chevauchement n'est trouvé, la fonction retourne True, indiquant que la position de la nourriture est valide.
✨ Vérifier la solution et pratiquer

Détection de limite

checksnake est utilisée pour vérifier si le serpent a heurté les bordures du jeu. Cela inclut la détection des bordures supérieure, inférieure, gauche et droite du jeu. Les collisions avec le corps du serpent lui-même sont également vérifiées, car ces deux situations entraîneraient la fin du jeu.

int checksnake(struct TFood *pfood, struct TSnake *psnake)
{

 /* Vérifier si les coordonnées de la tête du serpent ont heurté les bordures du jeu en haut, en bas, à gauche ou à droite */
    if ( psnake->head->y <= 0 || psnake->head->y >= GAMEWIN_YLEN
      || psnake->head->x <= 0 || psnake->head->x >= GAMEWIN_XLEN)
    {
        return -1;
    }

    struct TSnakeNode *pnode = GetSnakeTail(psnake);
    int i = 0;

 /* Vérifier que la tête du serpent ne heurte aucune partie de son corps */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

   /* Bien sûr, la collision avec la nourriture est autorisée */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // Aucune collision n'est survenue
}
✨ Vérifier la solution et pratiquer

Fin de jeu

La fonction gameover est utilisée pour afficher le message de fin de jeu dans la fenêtre spécifiée (win) lorsque le jeu du serpent se termine :

void gameover(WINDOW *win, char *str)
{
    mvwprintw(win, (int)(GAMEWIN_YLEN/2), (GAMEWIN_XLEN/2 - strlen(str)/2), str);
    mvwprintw(win, (int)(GAMEWIN_YLEN/2 + 1), 20, "Press any key to quit...");
    wrefresh(win);
}
  • Utilisez la fonction mvwprintw pour afficher le texte du message de fin de jeu str au milieu de la fenêtre. Cela garantit que le message de fin de jeu est affiché au centre de la fenêtre.
  • Ensuite, affichez "Press any key to quit..." sur la ligne suivante au milieu de la fenêtre, invitant le joueur à appuyer sur n'importe quelle touche pour quitter le jeu.
  • Enfin, utilisez la fonction wrefresh pour rafraîchir la fenêtre et vous assurer que le message de fin de jeu et l'invitation à quitter sont correctement dessinés sur l'écran.

Pour éviter les fuites de mémoire, utilisez la fonction destroysnake pour libérer la mémoire occupée par le jeu du serpent, y compris les nœuds du serpent et la structure du serpent elle - même. C'est une étape de nettoyage courante lorsque le jeu se termine ou redémarre.

void destroysnake(struct TSnake *psnake)
{
    struct TSnakeNode *psnode = GetSnakeTail(psnake);
    struct TSnakeNode *ptmp   = NULL;

    int i = 0;
    for (; i < psnake->length; ++i) {
        ptmp   = psnode;
        psnode = psnode->front;
        free(ptmp);
    }

    free(psnake);
    psnake = NULL;
}

À l'intérieur de la fonction, elle déclare d'abord deux pointeurs, psnode et ptmp, pour parcourir la liste chaînée des nœuds du serpent.

Elle entre dans une boucle qui itère un nombre de fois égal à la longueur (nombre de nœuds) du serpent. À chaque itération, elle effectue les opérations suivantes :

  • Affecte ptmp pour qu'il pointe vers le nœud actuel psnode afin de libérer plus tard la mémoire du nœud actuel.
  • Déplace psnode vers le nœud suivant (pointé par front).

Après la fin de la boucle, tous les nœuds du serpent ont été libérés.

Enfin, la fonction libère la mémoire de la structure du serpent elle - même, free(psnake), et affecte le pointeur psnake qui pointe vers la structure du serpent à NULL pour s'assurer que la mémoire libérée n'est plus utilisée.

✨ Vérifier la solution et pratiquer

Compilation et exécution

La commande de compilation est légèrement différente de l'habitude. Il est nécessaire d'ajouter l'option -l à gcc pour inclure la bibliothèque ncurses :

cd ~/project
gcc -o snake snake.c -l ncurses
./snake
Jeu du serpent
✨ Vérifier la solution et pratiquer

Résumé

Vous avez créé avec succès un simple jeu du serpent en langage C en utilisant la bibliothèque ncurses. Le jeu comprend une fenêtre de jeu, le mouvement du serpent, la génération de nourriture et la détection de collisions. En suivant les étapes ci - dessus, vous pouvez exécuter et profiter du jeu dans le terminal. Amusez - vous bien!