Das Erstellen von Flappy Bird mit C

CCIntermediate
Jetzt üben

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Flappy Bird ist ein beliebiges und süchtig machendes Mobilspiel, das aufgrund seines einfachen, aber herausfordernden Gameplays enorme Beliebtheit erlangte. In diesem Projekt werden wir lernen, wie man eine eigene Version von Flappy Bird mit der C-Programmiersprache implementiert.

Durch das Durcharbeiten dieses Projekts wirst du:

  • Lernen, wie man die ncurses-Bibliothek für die zeichenbasierte Bildschirmzeichnung verwendet.
  • Kenntnisse über Datenstrukturen und Systemaufrufe in Linux erwerben.
  • Erfahrungen bei der Behandlung von Tastaturevents und Echtzeitupdates in einem C-Programm sammeln.

👀 Vorschau

Flappy Bird Vorschau

🎯 Aufgaben

In diesem Projekt wirst du lernen:

  • Wie man die zeichenbasierte Version von Flappy Bird mit C implementiert.
  • Wie man Tastaturevents behandelt, um die Bewegung des Vogels zu steuern.
  • Wie man den Illusion von Vorwärtsbewegung erzeugt, indem man Hindernisse von rechts nach links bewegt.
  • Wie man die ncurses-Bibliothek verwendet, um die Zeichenoberfläche zu zeichnen.

🏆 Errungenschaften

Nach Abschluss dieses Projekts wirst du in der Lage sein:

  • Deine Fähigkeiten in der C-Programmiersprache zu demonstrieren.
  • Fähigkeiten bei der Behandlung von Tastaturevents zu entwickeln.
  • Echtzeitupdates in einem C-Programm zu implementieren.
  • Die ncurses-Bibliothek für die zeichenbasierte Bildschirmzeichnung zu nutzen.
  • Datenstrukturen und Systemaufrufe in Linux zu verstehen.

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("C")) -.-> c/BasicsGroup(["Basics"]) c(("C")) -.-> c/UserInteractionGroup(["User Interaction"]) c(("C")) -.-> c/ControlFlowGroup(["Control Flow"]) c(("C")) -.-> c/CompoundTypesGroup(["Compound Types"]) c(("C")) -.-> c/FunctionsGroup(["Functions"]) 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{{"Das Erstellen von Flappy Bird mit C"}} c/constants -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/if_else -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/while_loop -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/structures -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/function_declaration -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/function_parameters -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/user_input -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} c/output -.-> lab-298823{{"Das Erstellen von Flappy Bird mit C"}} end

Grundlagenwissen

Unser Projekt erfordert einige Kenntnisse über Datenstrukturen und beinhaltet einige Systemaufrufe in Linux.

Zusätzlich verwenden wir auch eine zeichenbasierte Bildschirmzeichnungsbibliothek namens ncurses. Beim Kompilieren muss daher die Option -lcurses hinzugefügt werden.

Designideen

Um die zeichenbasierte Version von Flappy Bird zu implementieren, beginnen wir mit der Implementierung der folgenden drei Schlüsselpunkte:

  1. Das Programm sollte auf Tastaturevents reagieren können.
  2. Die Zeichenoberfläche sollte in Echtzeit aktualisiert werden können.
  3. Der Vogel sollte den visuellen Eindruck eines Vorwärtsfluges erwecken.

Für die obigen drei Probleme sind unsere Lösungen wie folgt:

  1. Verwenden Sie die von Linux bereitgestellten Systemschnittstellen, um Tastaturevents zu erfassen.
  2. Verwenden Sie ncurses-Bibliotheksfunktionen, um die Zeichenoberfläche zu zeichnen.
  3. Um den Eindruck eines Vorwärtsfluges des Vogels zu erzeugen:

Der einfachste Ansatz wäre, den Vogel horizontal von links nach rechts zu bewegen, aber dadurch würde der Vogel irgendwann die rechte Grenze überschreiten.

Stattdessen denken wir um: Wenn eine Person die Landschaft außerhalb des Autos sieht, während sie vorwärts fährt, erscheint diese rückwärts zu bewegen (Bewegung ist relativ). Also lassen wir die Hindernisse von rechts nach links bewegen, was den gleichen visuellen Effekt erzielt und das Problem des Überschreitens der Grenze durch den Vogel vermeidet.

Konstanten definieren

Öffnen Sie zunächst das Xfce-Terminal und führen Sie den folgenden Befehl aus, um die ncurses-Bibliothek zu installieren:

sudo apt update
sudo apt-get install libncurses5-dev

Navigieren Sie in das Verzeichnis ~/project und erstellen Sie die Projekt-Datei flappy_bird.c:

cd ~/project
touch flappy_bird.c

Als Nächstes müssen wir den C-Code schreiben. Der erste Schritt besteht darin, die Header-Dateien einzuschließen:

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

Bevor wir die main()-Funktion schreiben, sollten wir einige grundlegende Aufgaben erledigen. Da wir mit einer terminalbasierten Zeichenoberfläche arbeiten, sind ASCII-Zeichen unerlässlich. Daher müssen wir einige Konstanten definieren.

Wir werden * verwenden, um die Säulen im Hintergrund zu repräsentieren, und O um den Vogel zu repräsentieren. Der Code lautet wie folgt:

## define CHAR_BIRD 'O'  // Definiere das Vogelzeichen
## define CHAR_STONE '*'  // Definiere die Steine, aus denen die Säulen bestehen
## define CHAR_BLANK ' '  // Definiere das Leerzeichen

Die Säulen im Hintergrund werden mithilfe einer einfach verketteten Liste gespeichert. Die Struktur wird wie folgt definiert:

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

Definieren wir auch einige globale Variablen:

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

Wir werden nun die Funktionen, die wir erstellen werden, deklarieren:

void init();  // Initialisierungsfunktion, die die Initialisierungstasks des Spiels verwaltet
void init_bird();  // Initialisiere die Positionskoordinaten des Vogels
void init_draw();  // Initialisiere den Hintergrund
void init_head();  // Initialisiere den Kopf der verketteten Liste, die die Säulen speichert
void init_wall();  // Initialisiere die verkettete Liste, die die Säulen speichert
void drop(int sig);  // Signalempfangsfunktion, um Systemsignale zu empfangen und die Säulen von rechts nach links zu bewegen
int set_ticker(int n_msec);  // Setze das Zeitintervall der Kernzeitzähler
✨ Lösung prüfen und üben

Zeitproblematik

Lösen wir nun das Problem, wie wir den Hintergrund in regelmäßigen Abständen bewegen können. Wir werden die Funktionalität der Linux-System nutzen, nämlich Signale.

Sind Sie sich nicht sicher, was ein Signal ist? Kein Problem, Sie können sich ein Signal als einen Timer im Linux-Kern vorstellen, der unserem Programm alle bestimmte Zeit einen Signal sendet. Unsere Signalbehandlungsfunktion drop(int sig) wird automatisch ausgeführt, wenn das Signal empfangen wird. Wir müssen nur die Säule in der drop(int sig)-Funktion bewegen. Darüber hinaus wird aufgrund der von dem Linux-Kern gesendeten Signale die Empfangsfunktion unserer Tastatur nicht blockiert.

Implementieren wir nun unseren Code und setzen wir die Zeitperiode des Kernzeitzählers mit der set_ticker(int n_msec)-Funktion:

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);
}

Signalbehandlungsfunktion drop(int sig):

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

    // Lösche das Symbol an der ursprünglichen Vogelposition
    move(bird_y, bird_x);
    addch(CHAR_BLANK);
    refresh();

    // Aktualisiere die Position des Vogels und aktualisiere den Bildschirm
    bird_y++;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();

    // Beende das Spiel, wenn der Vogel mit der Säule kollidiert
    if((char)inch() == CHAR_STONE)
    {
        set_ticker(0);
        sleep(1);
        endwin();
        exit(0);
    }

    // Überprüfe, ob die erste Wand außerhalb der Grenze ist
    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;  // Beschleunige
        set_ticker(ticker);
    }
    // Zeichne eine neue Säule
    for(p = head->next; p->next!= NULL; p->x--, p = p->next)
    {
        // Ersetze CHAR_STONE mit 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--;
}

In der Signalbehandlungsfunktion bewegen wir den Hintergrund um eine Spalte nach vorne und lassen gleichzeitig den Vogel um eine Zeile nach unten fallen. Wir überprüfen auch, ob der Vogel mit einer Säule kollidiert. Wenn ja, ist das Spiel vorbei.

✨ Lösung prüfen und üben

main()-Funktion

In der main()-Funktion rufen wir zunächst die Initialisierungsfunktion init() auf und gehen dann in die while()-Schleife ein. Die Schleife besteht hauptsächlich aus drei Teilen:

  1. Überprüfen Sie die Benutzereingabe: Wenn die Taste "w" oder die Leertaste gedrückt wird, bewegt sich der Vogel um zwei Zeilen nach oben. Wenn die Taste "q" gedrückt wird, wird das Spiel beendet. Wenn die Taste "z" gedrückt wird, wird das Spiel pausiert.
  2. Bewegen Sie den Vogel und zeichnen Sie ihn neu.
  3. Überprüfen Sie, ob der Vogel die Rohre trifft.

Schauen wir uns den Code an:

int main()
{
    char ch;

    init();
    while(1)
    {
        ch = getch();  // Holen Sie die Tastatureingabe
        if(ch == ' ' || ch == 'w' || ch == 'W')  // Wenn die Leertaste oder die Taste "w" gedrückt wird
        {
            // Bewegen Sie den Vogel und zeichnen Sie ihn neu
            move(bird_y, bird_x);
            addch(CHAR_BLANK);
            refresh();
            bird_y--;
            move(bird_y, bird_x);
            addch(CHAR_BIRD);
            refresh();

            // Wenn der Vogel die Rohre trifft, beenden Sie das Spiel
            if((char)inch() == CHAR_STONE)
            {
                set_ticker(0);
                sleep(1);
                endwin();
                exit(0);
            }
        }
        else if(ch == 'z' || ch == 'Z')  // Pause
        {
            set_ticker(0);
            do
            {
                ch = getch();
            } while(ch!= 'z' && ch!= 'Z');
            set_ticker(ticker);
        }
        else if(ch == 'q' || ch == 'Q')  // Beenden
        {
            sleep(1);
            endwin();
            exit(0);
        }
    }
    return 0;
}

In der main()-Funktion initialisieren wir zunächst den Bildschirm und empfangen dann in einer Schleife die Tastatureingaben. Wenn die Taste "w" oder die Leertaste gedrückt wird, bewegt sich der Vogel um zwei Zeilen nach oben. Wenn die Taste "q" gedrückt wird, wird das Spiel beendet. Wenn die Taste "z" gedrückt wird, wird das Spiel pausiert.

Schauen wir uns nun die init()-Funktion an:

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);
}

Die init()-Funktion initialisiert zunächst den Bildschirm mit Funktionen von ncurses. Anschließend ruft sie mehrere Hilfsfunktionen auf, um spezifische Initialisierungen durchzuführen. Beachten Sie, dass wir eine Signalbehandlungsfunktion drop() installieren und das Zeitintervall des Timers festlegen.

Schauen wir uns jede Initialisierungs-Hilfsfunktion an.

Die init_bird()-Funktion initialisiert die Position des Vogels:

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

Die init_head()- und init_wall()-Funktionen initialisieren eine verkettete Liste, um die Rohre zu speichern:

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;
}

Die init_draw()-Funktion initialisiert den Bildschirm:

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

    // Iterieren Sie über die verkettete Liste
    for(p = head->next; p->next!= NULL; p = p->next)
    {
        // Zeichnen Sie die Rohre
        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);
    }
}

Mit diesem ist unser Flappy-Bird-Spiel abgeschlossen.

✨ Lösung prüfen und üben

Kompilieren und Ausführen

Führen Sie den gcc-Befehl aus, um zu kompilieren:

cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird
Compiling Flappy Bird code
✨ Lösung prüfen und üben

Zusammenfassung

In diesem Projekt haben wir die Programmiersprache C verwendet, um ein textbasiertes Flappy-Bird-Spiel zu implementieren. Schüler können das Spiel auf der Grundlage dieses Kurses weiter verbessern, indem sie beispielsweise den Rohren Farben hinzufügen oder die Breite der Rohre zufällig verändern.