Création de Flappy Bird avec C

CCBeginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Flappy Bird est un jeu mobile populaire et addictif qui a connu un immense succès grâce à son gameplay simple mais difficile. Dans ce projet, nous allons apprendre à implémenter notre propre version de Flappy Bird en utilisant le langage de programmation C.

En suivant ce projet, vous allez :

  • Apprendre à utiliser la bibliothèque ncurses pour le dessin d'écran basé sur le texte.
  • Acquérir des connaissances sur les structures de données et les appels système sous Linux.
  • Gagner de l'expérience dans la gestion des événements clavier et des mises à jour en temps réel dans un programme C.

👀 Aperçu

Flappy Bird Aperçu

🎯 Tâches

Dans ce projet, vous allez apprendre :

  • Comment implémenter la version basée sur des caractères de Flappy Bird en utilisant C.
  • Comment gérer les événements clavier pour contrôler le mouvement de l'oiseau.
  • Comment créer l'illusion de mouvement en avant en déplaçant les obstacles d droite à gauche.
  • Comment utiliser la bibliothèque ncurses pour dessiner l'interface basée sur des caractères.

🏆 Réalisations

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

  • Monter en compétence dans le langage de programmation C.
  • Développer des compétences dans la gestion des événements clavier.
  • Implémenter des mises à jour en temps réel dans un programme C.
  • Utiliser la bibliothèque ncurses pour le dessin d'écran basé sur le texte.
  • Comprendre les structures de données et les appels système sous Linux.

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("C")) -.-> c/BasicsGroup(["Basics"]) c(("C")) -.-> c/CompoundTypesGroup(["Compound Types"]) c(("C")) -.-> c/FunctionsGroup(["Functions"]) c(("C")) -.-> c/UserInteractionGroup(["User Interaction"]) c(("C")) -.-> c/ControlFlowGroup(["Control Flow"]) c/BasicsGroup -.-> c/variables("Variables") c/BasicsGroup -.-> c/constants("Constants") c/ControlFlowGroup -.-> c/if_else("If...Else") c/ControlFlowGroup -.-> c/while_loop("While Loop") c/CompoundTypesGroup -.-> c/structures("Structures") c/FunctionsGroup -.-> c/function_declaration("Function Declaration") c/FunctionsGroup -.-> c/function_parameters("Function Parameters") c/UserInteractionGroup -.-> c/user_input("User Input") c/UserInteractionGroup -.-> c/output("Output") subgraph Lab Skills c/variables -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/constants -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/if_else -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/while_loop -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/structures -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/function_declaration -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/function_parameters -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/user_input -.-> lab-298823{{"Création de Flappy Bird avec C"}} c/output -.-> lab-298823{{"Création de Flappy Bird avec C"}} end

Connaissances de base

Notre projet nécessite certaines connaissances sur les structures de données et implique quelques appels système sous Linux.

De plus, nous utilisons également une bibliothèque de dessin d'écran basée sur le texte appelée ncurses. Ainsi, lors de la compilation, l'option -lcurses doit être ajoutée.

Idées de conception

Pour implémenter la version basée sur des caractères de Flappy Bird, nous commençons par l'implémentation des trois points clés suivants :

  1. Le programme doit être capable de répondre aux événements clavier.
  2. L'interface basée sur des caractères doit être capable de se mettre à jour en temps réel.
  3. L'oiseau doit donner l'illusion visuelle de voler en avant.

Pour les trois problèmes ci-dessus, nos solutions sont les suivantes :

  1. Utiliser les interfaces système fournies par Linux pour capturer les événements clavier.
  2. Utiliser les fonctions de la bibliothèque ncurses pour dessiner l'interface basée sur des caractères.
  3. Pour créer l'illusion que l'oiseau vole en avant :

L'approche la plus directe serait de faire déplacer l'oiseau de gauche à droite dans la direction horizontale, mais cela ferait en sorte que l'oiseau dépasse la limite droite à un moment donné.

Au lieu de cela, réfléchissons à l'inverse : lorsqu'une personne voit le paysage à l'extérieur de la voiture en se rendant en avant, il semble qu'il recule (le mouvement est relatif). Donc, nous faisons déplacer les obstacles de droite à gauche, ce qui atteint le même effet visuel et évite le problème du dépassement de la limite par l'oiseau.

Définir des constantes

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

sudo apt update
sudo apt-get install libncurses5-dev

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

cd ~/project
touch flappy_bird.c

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

## include <curses.h>
## include <stdlib.h>
## include <signal.h>
## include <sys/time.h>

Avant d'écrire la fonction main(), terminons quelques tâches de base. Puisque nous travaillons avec une interface de caractères de terminal, les caractères ASCII sont essentiels. Par conséquent, nous devons définir quelques constantes.

Nous utiliserons * pour représenter les piliers d'arrière-plan et O pour représenter l'oiseau. Le code est le suivant :

## define CHAR_BIRD 'O'  // Définir le caractère de l'oiseau
## define CHAR_STONE '*'  // Définir les pierres qui composent les piliers
## define CHAR_BLANK ' '  // Définir le caractère vide

Les piliers d'arrière-plan seront stockés à l'aide d'une liste chaînée simple. La structure est définie comme suit :

typedef struct node {
    int x, y;
    struct node *next;
}node, *Node;

Définissons également quelques variables globales :

Node head, tail;
int bird_x, bird_y;
int ticker;

Nous allons maintenant déclarer les fonctions que nous allons créer :

void init();  // Fonction d'initialisation qui gère les tâches d'initialisation du jeu
void init_bird();  // Initialiser les coordonnées de position de l'oiseau
void init_draw();  // Initialiser l'arrière-plan
void init_head();  // Initialiser la tête de la liste chaînée qui stocke les piliers
void init_wall();  // Initialiser la liste chaînée qui stocke les piliers
void drop(int sig);  // Fonction de réception de signal pour recevoir les signaux du système et déplacer les piliers de droite à gauche
int set_ticker(int n_msec);  // Régler l'intervalle de tic du minuteur du noyau
✨ Vérifier la solution et pratiquer

Problème de temporisation

Maintenant, résolvons le problème de la manière de faire déplacer l'arrière-plan à intervalles réguliers. Nous utiliserons la fonctionnalité fournie par le système Linux, à savoir les signaux.

Pas sûr de ce qu'est un signal? Pas de souci, vous pouvez le considérer comme un minuteur dans le noyau Linux qui envoie un signal à notre programme toutes les certaines périodes de temps. Notre fonction de gestionnaire de signaux drop(int sig) sera automatiquement exécutée lorsque le signal est reçu. Nous n'avons qu'à déplacer le pilier dans la fonction drop(int sig). De plus, puisque le signal est envoyé par le noyau Linux, il n'y aura aucun blocage de notre réception de signaux clavier en raison de la réception du signal.

Maintenant, implémentons notre code et définissons la période du minuteur du noyau à l'aide de la fonction set_ticker(int n_msec) :

int set_ticker(int n_msec)
{
    struct itimerval timeset;
    long n_sec, n_usec;

    n_sec = n_msec / 1000;
    n_usec = (n_msec % 1000) * 1000L;

    timeset.it_interval.tv_sec = n_sec;
    timeset.it_interval.tv_usec = n_usec;

    timeset.it_value.tv_sec = n_sec;
    timeset.it_value.tv_usec = n_usec;

    return setitimer(ITIMER_REAL, &timeset, NULL);
}

Fonction de gestionnaire de signaux drop(int sig) :

void drop(int sig)
{
    int j;
    Node tmp, p;

    // Effacer le symbole à la position originale de l'oiseau
    move(bird_y, bird_x);
    addch(CHAR_BLANK);
    refresh();

    // Mettre à jour la position de l'oiseau et rafraîchir l'écran
    bird_y++;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();

    // Finir le jeu si l'oiseau entre en collision avec le pilier
    if((char)inch() == CHAR_STONE)
    {
        set_ticker(0);
        sleep(1);
        endwin();
        exit(0);
    }

    // Vérifier si le premier mur dépasse la limite
    p = head->next;
    if(p->x < 0)
    {
        head->next = p->next;
        free(p);
        tmp = (node *)malloc(sizeof(node));
        tmp->x = 99;
        tmp->y = rand() % 11 + 5;
        tail->next = tmp;
        tmp->next = NULL;
        tail = tmp;
        ticker -= 10;  // Accélérer
        set_ticker(ticker);
    }
    // Dessiner un nouveau pilier
    for(p = head->next; p->next!= NULL; p->x--, p = p->next)
    {
        // Remplacer CHAR_STONE par CHAR_BLANK
        for(j = 0; j < p->y; j++)
        {
            move(j, p->x);
            addch(CHAR_BLANK);
            refresh();
        }
        for(j = p->y+5; j <= 23; j++)
        {
            move(j, p->x);
            addch(CHAR_BLANK);
            refresh();
        }

        if(p->x-10 >= 0 && p->x < 80)
        {
            for(j = 0; j < p->y; j++)
            {
                move(j, p->x-10);
                addch(CHAR_STONE);
                refresh();
            }
            for(j = p->y + 5; j <= 23; j++)
            {
                move(j, p->x-10);
                addch(CHAR_STONE);
                refresh();
            }
        }
    }
    tail->x--;
}

Dans la fonction de gestionnaire de signaux, nous déplaçons l'arrière-plan d'une colonne vers l'avant, et en même temps, nous faisons tomber l'oiseau d'une ligne. Nous vérifions également si l'oiseau entre en collision avec un pilier. Si c'est le cas, c'est la fin du jeu.

✨ Vérifier la solution et pratiquer

Fonction main()

Dans la fonction main(), nous appelons tout d'abord la fonction d'initialisation init(), puis nous entrons dans la boucle while(). La boucle se compose principalement de trois parties :

  1. Vérifier l'entrée de l'utilisateur : Si la touche "w" ou la barre d'espace est pressée, l'oiseau ira deux lignes en haut. Si la touche "q" est pressée, le jeu se terminera. Si la touche "z" est pressée, le jeu se mettra en pause.
  2. Déplacer l'oiseau et le redessiner.
  3. Vérifier si l'oiseau heurte les tuyaux.

Voici le code :

int main()
{
    char ch;

    init();
    while(1)
    {
        ch = getch();  // Obtenir l'entrée clavier
        if(ch == ' ' || ch == 'w' || ch == 'W')  // Si la barre d'espace ou la touche "w" est pressée
        {
            // Déplacer l'oiseau et le redessiner
            move(bird_y, bird_x);
            addch(CHAR_BLANK);
            refresh();
            bird_y--;
            move(bird_y, bird_x);
            addch(CHAR_BIRD);
            refresh();

            // Si l'oiseau heurte les tuyaux, finir le jeu
            if((char)inch() == CHAR_STONE)
            {
                set_ticker(0);
                sleep(1);
                endwin();
                exit(0);
            }
        }
        else if(ch == 'z' || ch == 'Z')  // Mettre en pause
        {
            set_ticker(0);
            do
            {
                ch = getch();
            } while(ch!= 'z' && ch!= 'Z');
            set_ticker(ticker);
        }
        else if(ch == 'q' || ch == 'Q')  // Quitter
        {
            sleep(1);
            endwin();
            exit(0);
        }
    }
    return 0;
}

Dans la fonction main(), nous initialisons d'abord l'écran, puis nous recevons l'entrée clavier dans une boucle. Si la touche "w" ou la barre d'espace est pressée, l'oiseau ira deux lignes en haut. Si la touche "q" est pressée, le jeu se terminera. Si la touche "z" est pressée, le jeu se mettra en pause.

Maintenant, voyons la fonction init() :

void init()
{
    initscr();
    cbreak();
    noecho();
    curs_set(0);
    srand(time(0));
    signal(SIGALRM, drop);

    init_bird();
    init_head();
    init_wall();
    init_draw();
    sleep(1);
    ticker = 500;
    set_ticker(ticker);
}

La fonction init() initialise d'abord l'écran à l'aide de fonctions fournies par ncurses. Ensuite, elle appelle plusieurs sous-fonctions pour effectuer des initialisations spécifiques. Notez que nous installons une fonction de gestionnaire de signaux drop(), et que nous définissons l'intervalle du minuteur.

Regardons chaque sous-fonction d'initialisation.

La fonction init_bird() initialise la position de l'oiseau :

void init_bird()
{
    bird_x = 5;
    bird_y = 15;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();
    sleep(1);
}

Les fonctions init_head() et init_wall() initialisent une liste chaînée pour stocker les tuyaux :

void init_head()
{
    Node tmp;

    tmp = (node *)malloc(sizeof(node));
    tmp->next = NULL;
    head = tmp;
    tail = head;
}
void init_wall()
{
    int i;
    Node tmp, p;

    p = head;
    for(i = 0; i < 5; i++)
    {
        tmp = (node *)malloc(sizeof(node));
        tmp->x = (i + 1) * 19;
        tmp->y = rand() % 11 + 5;
        p->next = tmp;
        tmp->next = NULL;
        p = tmp;
    }
    tail = p;
}

La fonction init_draw() initialise l'écran :

void init_draw()
{
    Node p;
    int i, j;

    // Parcourir la liste chaînée
    for(p = head->next; p->next!= NULL; p = p->next)
    {
        // Dessiner les tuyaux
        for(i = p->x; i > p->x-10; i--)
        {
            for(j = 0; j < p->y; j++)
            {
                move(j, i);
                addch(CHAR_STONE);
            }
            for(j = p->y+5; j <= 23; j++)
            {
                move(j, i);
                addch(CHAR_STONE);
            }
        }
        refresh();
        sleep(1);
    }
}

Avec cela, notre jeu flappy_bird est terminé.

✨ Vérifier la solution et pratiquer

Compilation et exécution

Exécutez la commande gcc pour compiler :

cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird
Compiling Flappy Bird code
✨ Vérifier la solution et pratiquer

Sommaire

Dans ce projet, nous avons utilisé le langage de programmation C pour implémenter un jeu Flappy Bird basé sur le texte. Les étudiants peuvent améliorer le jeu à partir de ce cours, par exemple en ajoutant des couleurs aux tuyaux ou en rendant les largeurs des tuyaux variables aléatoirement.