Erstellen eines Schlangenspiels in C

CCIntermediate
Jetzt üben

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

Einführung

In diesem Projekt lernen Sie, wie Sie mithilfe der ncurses-Bibliothek ein einfaches Snake-Spiel in C erstellen können. Bei diesem klassischen Spiel geht es darum, eine Schlange zu steuern, um Nahrung zu essen und länger zu werden, während man Kollisionen mit Wänden und der Schlange selbst vermeidet. Die Funktionalität des Spiels ist in mehrere Schlüsselkomponenten aufgeteilt: Initialisierung, Spielschleife, Bewegung der Schlange, Kollisionserkennung usw. Am Ende dieses Projekts haben Sie ein einfaches Snake-Spiel, das in einem Terminal ausgeführt werden kann.

👀 Vorschau

Snake Game

🎯 Aufgaben

In diesem Projekt lernen Sie:

  • Wie Sie die Spielschleife implementieren, um die Position der Schlange zu aktualisieren und Benutzereingaben zu verarbeiten.
  • Wie Sie Funktionen erstellen, um das Spiel zu initialisieren, das Spielfenster zu zeichnen und "Spiel vorbei"-Meldungen anzuzeigen.
  • Wie Sie die Kollisionserkennung implementieren, um Kollisionen mit Wänden, dem Körper der Schlange selbst und Nahrung zu überprüfen.
  • Wie Sie Funktionen entwickeln, wie z. B. das Verlängern der Schlange, wenn sie Nahrung isst.

🏆 Errungenschaften

Nach Abschluss dieses Projekts können Sie:

  • Die ncurses-Bibliothek in C verwenden, um ein terminalbasiertes Spiel zu erstellen.
  • Spiellogik implementieren, einschließlich der Aktualisierung des Spielzustands und der Verarbeitung von Benutzereingaben.
  • Datenstrukturen erstellen und manipulieren, um Spielobjekte wie die Schlange und die Nahrung darzustellen.
  • Kollisionserkennung implementieren, um die Spielregeln festzulegen und zu bestimmen, wann das Spiel enden sollte.
Dies ist ein Guided Lab, das schrittweise Anweisungen bietet, um Ihnen beim Lernen und Üben zu helfen. Befolgen Sie die Anweisungen sorgfältig, um jeden Schritt abzuschließen und praktische Erfahrungen zu sammeln. Historische Daten zeigen, dass dies ein Labor der Stufe Fortgeschrittener mit einer Abschlussquote von 77% ist. Es hat eine positive Bewertungsrate von 78% von den Lernenden erhalten.

Grundlagenwissen

In der Zeit der weit verbreiteten Verwendung von Teletype-Schreibmaschinen dienten diese als Ausgabeterminals, die über Kabel mit Zentralrechnern verbunden waren. Benutzer mussten eine Reihe spezifischer Steuerbefehle an das Terminalprogramm senden, um die Ausgabe auf dem Terminalscreen zu steuern. Beispielsweise das Ändern der Cursorposition auf dem Bildschirm, das Löschen des Inhalts eines bestimmten Bildschirmbereichs, das Scrollen des Bildschirms, das Umschalten der Anzeigemodi, das Unterstreichen von Text, das Ändern des Aussehens, der Farbe, der Helligkeit usw. von Zeichen. Diese Steuerungen werden über eine Zeichenfolge namens Escape-Sequenz implementiert. Die Escape-Sequenzen werden so genannt, weil diese aufeinanderfolgenden Bytes mit einem 0x1B-Zeichen beginnen, dem Escape-Zeichen (dem Zeichen, das durch Drücken der ESC-Taste eingegeben wird). Selbst heute können wir den Ausgabeeffekt von Teletype-Terminals aus jener Zeit simulieren, indem wir Escape-Sequenzen in Terminalemulationsprogramme eingeben. Wenn Sie einen Text mit farbigem Hintergrund auf dem Terminal (oder Terminalemulationsprogramm) anzeigen möchten, können Sie die folgende Escape-Sequenz in die Eingabeaufforderung Ihres Befehlszeilentools eingeben:

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

Hierbei sind ^ und [ die sogenannten Escape-Zeichen. (Hinweis: in diesem Fall ist ^[ ein einzelnes Zeichen. Es wird nicht durch sequentielles Eingeben der Zeichen ^ und [ erzeugt. Um dieses Zeichen zu drucken, müssen Sie zunächst Ctrl+V und dann die ESC-Taste drücken.) Nach Ausführung des obigen Befehls sollten Sie sehen, dass der Hintergrund von In Color auf rot geändert wurde. Ab diesem Zeitpunkt wird all der angezeigte Text mit diesem Effekt ausgegeben. Wenn Sie diesen Effekt beenden und zum ursprünglichen Format zurückkehren möchten, können Sie den folgenden Befehl verwenden:

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

Wissen Sie jetzt, wofür diese Zeichen (Escape-Sequenzen) gut sind? (Versuchen Sie, die Parameter zwischen den Semikolons zu ändern und sehen Sie, welche Ergebnisse Sie erhalten.) Vielleicht ist es anders, als Sie sich das vorstellen? Das kann daran liegen, dass die Terminalumgebung unterschiedlich ist, was von den verschiedenen Terminals oder Betriebssystemen abhängt. (Sie können einem monochromen Terminal ja nicht farbige Zeichen anzeigen lassen, oder?) Um solche Kompatibilitätsprobleme zu vermeiden und eine konsistente Ausgabe auf verschiedenen Terminals zu erzielen, erfanden die Designer von UNIX einen Mechanismus namens termcap. termcap ist eigentlich eine Datei, die zusammen mit den Escape-Sequenzen herausgegeben wird. In dieser Datei sind alle Escape-Sequenzen aufgeführt, die das aktuelle Terminal korrekt ausführen kann, wodurch sichergestellt wird, dass das Ausführungsergebnis der eingegebenen Escape-Sequenzen den Spezifikationen in dieser Datei entspricht. Allerdings ersetzte in den Jahren nach der Erfindung dieses Mechanismus allmählich ein anderer Mechanismus namens terminfo termcap. Seitdem müssen Benutzer bei der Programmierung nicht mehr die komplexen Escape-Sequenz-Spezifikationen in termcap konsultieren, sondern können einfach auf die terminfo-Datenbank zugreifen, um die Bildschirmausgabe zu steuern.

Angenommen, alle Anwendungen greifen unter Verwendung von terminfo auf die terminfo-Datenbank zu, um die Ausgabe zu steuern (z. B. Steuerzeichen zu senden usw.). Bald würden diese Codeaufrufe das gesamte Programm schwierig zu kontrollieren und zu verwalten machen. Die Entstehung dieser Probleme führte zur Entstehung von CURSES. Der Name CURSES stammt von einem Wortspiel mit cursor optimization (Cursor-Optimierung).

Die CURSES-Bibliothek bietet Benutzern eine flexible und effiziente API (Anwendungs-Programmierschnittstelle) durch die Kapselung der unterliegenden Steuercodes (Escape-Sequenzen) des Terminals. Dadurch können Benutzer den Cursor steuern, Fenster erstellen, Vordergrund- und Hintergrundfarben ändern und Mausoperationen verarbeiten. Dies ermöglicht es Benutzern, diese lästigen unteren Ebenenmechanismen zu umgehen, wenn sie Anwendungen auf Zeichenterminals schreiben.

NCURSES ist eine Kopie von CURSES aus System V Release 4.0 (SVr4). Es ist eine frei konfigurierbare Bibliothek, die vollständig mit älteren Versionen von CURSES kompatibel ist. Kurz gesagt, es ist eine Bibliothek, die es Anwendungen ermöglicht, die Anzeige des Terminalscreens direkt zu steuern. Wenn später von der CURSES-Bibliothek die Rede ist, wird damit auch die NCURSES-Bibliothek gemeint.

NCURSES kapselt nicht nur die unterliegenden Terminalfunktionen, sondern bietet auch einen ziemlich stabilen Arbeitsrahmen, um schöne Schnittstellen zu erstellen. Es umfasst Funktionen zum Erstellen von Fenstern. Und seine Schwesterbibliotheken Menu, Panel und Form sind Erweiterungen der CURSES-Basisklassenbibliothek. Diese Bibliotheken werden in der Regel zusammen mit CURSES verteilt. Wir können eine Anwendung erstellen, die mehrere Fenster, Menüs, Panels und Formulare enthält. Die Fenster können unabhängig voneinander verwaltet werden, z. B. gescrollt oder ausgeblendet werden. Menüs ermöglichen es Benutzern, Befehlsoptionen zu erstellen, um Befehle einfach ausführen zu können. Formulare ermöglichen es Benutzern, Fenster für die einfache Dateneingabe und -anzeige zu erstellen. Panels sind Erweiterungen der NCURSES-Fensterverwaltungsfunktionen und können Fenster überlagern oder stapeln.

✨ Lösung prüfen und üben

Konstanten definieren

Zuerst öffnen Sie das Terminal und führen Sie den folgenden Befehl aus, um die ncurses-Bibliothek zu installieren:

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

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

cd ~/project
touch snake.c

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

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

Bevor wir die main()-Funktion schreiben, sollten wir einige grundlegende Aufgaben erledigen. Da wir eine Terminal-Zeichen-Schnittstelle verwenden, sind ASCII-Zeichen unerlässlich. Daher müssen wir einige Konstanten definieren:

#define TBool            int
#define True             1
#define False            0
#define SHAPE_FOOD       '@'  // Essen
#define SHAPE_SNAKE      '#'  // Schlangenkörper
#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; // Deklariere ein Log-Fenster
#define INITRUNLOG()     logwin = newlogw() // Erstelle ein Log-Fenster durch Aufruf der benutzerdefinierten Funktion newlogw()
#define RUNLOG(str)      runlog(logwin, str) // Führe das Log-Fenster aus, um Spielhinweise anzuzeigen
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // Spielerlevel, eine globale Variable

Wir haben auch einige struct- und enum-Definitionen hinzugefügt:

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

Jetzt werden wir die Funktionen deklarieren, die wir erstellen werden:

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);
✨ Lösung prüfen und üben

main-Funktion

int main()
{
    initscr();  /* Initialisieren, treten in den ncurses-Modus ein */
    raw();      /* Zeilenpufferung deaktivieren, um sofortige Ergebnisse zu sehen */
    noecho();   /* Steuerzeichen wie Ctrl+C nicht auf dem Terminal anzeigen */
    keypad(stdscr, TRUE);   /* Dem Benutzer erlauben, die Tastatur im Terminal zu verwenden */
    curs_set(0);    /* Sichtbarkeit des Cursors festlegen, 0 = unsichtbar, 1 = sichtbar, 2 = vollständig sichtbar */
    refresh();      /* Den Inhalt des virtuellen Bildschirms auf den Display schreiben und aktualisieren */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  Drücken Sie 'q' oder 'Q', um das Spiel zu beenden.");
    RUNLOG("  Drücken Sie 'w/s/a/d' oder 'W/S/A/D', um die Schlange zu bewegen.");
    RUNLOG("Info:");

    WINDOW *gwin = newgamew(); /* Spielfenster erstellen, implementiert durch die benutzerdefinierte Funktion newgamew */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

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

    /* getch() unterscheidet sich von getchar() */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* Das Spielfenster löschen und den Speicher und die Informationen der Fenster-Datenstruktur freigeben */
    DESTROYRUNLOG(); /* Das Informationsanzeigefenster löschen */
    endwin();        /* Den ncurses-Modus verlassen */

    return 0;
}

In keypad(stdscr, TRUE) bezieht sich stdscr auf ein virtuelles Fenster, in das zunächst alle unsere Operationen geschrieben werden. Anschließend wird der Inhalt von stdscr mithilfe der refresh-Funktion auf dem Bildschirm angezeigt.

Wenn wir printw verwenden, wird die Daten tatsächlich auf einem virtuellen Fenster namens stdscr ausgegeben, das nicht direkt auf den Bildschirm ausgegeben wird. Zweck der printw()-Funktion besteht darin, kontinuierlich einige Anzeigemarker und zugehörige Datenstrukturen auf der virtuellen Anzeige zu schreiben und diese Daten in den Puffer von stdscr zu schreiben. Um die Daten in diesem Puffer anzuzeigen, müssen wir daher die refresh()-Funktion verwenden, um das curses-System anzuweisen, den Inhalt des Puffers auf den Bildschirm auszugeben. Dieser Mechanismus ermöglicht es dem Programmierer, kontinuierlich Daten auf dem virtuellen Bildschirm zu schreiben und es so aussehen zu lassen, als würde alles auf einmal geschehen, wenn refresh() aufgerufen wird. Da die refresh()-Funktion nur die Teile des Fensters und der Daten überprüft, die sich geändert haben, bietet dieses flexible Design einen effizienten Rückmeldemechanismus.

✨ Lösung prüfen und üben

Fenster-Mechanismus

Der Fenster-Mechanismus ist ein Kernkonzept von CURSES. Wie Sie aus den vorherigen Beispielen gesehen haben, arbeiten alle Funktionen standardmäßig auf einem "Fenster" (stdscr). Selbst wenn Sie die einfachste grafische Benutzeroberfläche (Graphical User Interface, GUI) entwerfen, müssen Sie Fenster verwenden. Einer der Hauptgründe für die Verwendung von Fenstern besteht darin, dass Sie den Bildschirm in verschiedene Teile aufteilen und gleichzeitig darin arbeiten können. Dies kann die Effizienz verbessern. Ein weiterer Grund ist, dass Sie in Ihrem Programm immer danach streben sollten, ein besseres und besser zu verwaltendes Design zu erreichen. Wenn Sie eine große und komplexe Benutzeroberfläche entwerfen, verbessert das Vorentwerfen dieser Teile Ihre Effizienz.

WINDOW* newlogw()
{
    /* Parameter sind: Höhe, Breite, Startposition (y,x) des Fensters */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* Parameter sind: bekannter Fensterzeiger, 0 und 0 sind die Standard-Reihen- und Spalten-Startpositionen der Zeichen */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // Das angegebene Fenster aktualisieren

    return win;
}

In der Funktion WINDOW* newlogw() können Sie sehen, dass die Erstellung eines Fensters mit newwin() beginnt. Obwohl wir ein Fenster erstellt haben, ist es noch nicht sichtbar. Dies ähnelt einem <div>-Element in HTML. Wenn Sie einem <div>-Element keine Stile hinzufügen, sehen Sie auf der Webseite nichts. Daher müssen wir die box-Funktion verwenden, um einem bekannten Fenster Rahmen hinzuzufügen.

mvwprintw gibt den angegebenen Inhalt an den angegebenen Koordinaten (y,x) innerhalb des angegebenen Fensters aus.

✨ Lösung prüfen und üben

Anzeige von Spielinformationen

RUNLOG ist ein Makro, das die benutzerdefinierte Funktion runlog aufruft.

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) {

        /* Benutzerdefinierte Funktion, cleanline */
        cleanline(win, i+1, 1);

        /* Zeichenkette in jeder Zeile ausgeben */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

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

runlog nimmt ein bekanntes Fenster und die anzuzeigende Information als Parameter. Die Zeichenkette str wird mithilfe der strcpy-Funktion in einem zweidimensionalen Array (logbuf) gespeichert. Anschließend wird die benutzerdefinierte Funktion cleanline aufgerufen, um die Zeile zu löschen, die als nächstes ausgegeben werden soll. Die mvwprintw-Funktion wird verwendet, um die Information auszugeben.

/* Löscht die Koordinaten (x,y) des Fensters win */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* Setzt die Positionen 0-56 des Arrays auf Leerzeichen */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* Bewegt den Cursor an die Position (y,x) im Fenster win und gibt die Zeichenkette EMPTYLINE aus */
    mvwprintw(win, y, x, EMPTYLINE);

    /* Zeigt den Inhalt auf dem angegebenen Fenster an */
    wrefresh(win);
}
✨ Lösung prüfen und üben

Initialisierung der Schlangenwarteschlange

Nun werden Sie die Datenstruktur der Schlange definieren und Funktionen erstellen, um sie zu initialisieren und auf dem Spielfenster zu zeichnen.

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

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // Initialisiere die Länge der Schlange auf 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;
}

Zweck dieser beiden Funktionen besteht darin, die Datenstruktur der Schlange in einem Schlangenspiel zu initialisieren, einschließlich der Einstellung der Anfangsrichtung und -länge der Schlange sowie der Erstellung einer Schlange mit einem anfänglichen Körperknoten. Die Funktion newsnakenode wird verwendet, um einzelne Knoten zu erstellen, und diese Knoten werden dann durch verschachtelte Funktionsaufrufe miteinander verbunden, um die Anfangsschlange zu bilden.

✨ Lösung prüfen und üben

Anzeige der Schlange auf dem Spielfenster

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

Um die (y,x)-Koordinaten jedes Knotens anzuzeigen, können wir die Makrodefinition GetSnakeTail verwenden, um die Koordinaten abzurufen. Der verbleibende Schritt besteht darin, eine Schleife zu verwenden, um sie mit mvwaddch anzuzeigen. mvwaddch wird verwendet, um den Cursor an die angegebene Position (taily,tailx) im angegebenen Fenster (win) zu bewegen und dann ein Zeichen auszugeben.

Um es tatsächlich auf dem Bildschirm anzuzeigen, müssen wir wrefresh(win) verwenden.

✨ Lösung prüfen und üben

Schreiben des Spielkerns

Schauen Sie sich das Semikolon nach while (refreshgamew(gwin, psnake) >= 0); an. Dies etabliert eine Schleife, um die Bewegung und die Längenänderung der Schlange sowie die Grenzprüfung und andere Probleme zu behandeln.

Die while-Schleife ruft die Funktion refreshgamew auf:

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* Beim Starten des Spiels oder wenn das Essen gegessen wurde, ist ffood = False, und drawfoodw wird ausgeführt, um das Essen neu zu zeichnen */
    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;

    // Als nächstes werden wir die select-Kern-API verwenden, die auf eine Reihe von Tastatur- und Mauseingaben lauscht. Wir verwenden `key = getch()` und `switch`, um den Eingabetyp zu bestimmen. Der zweite `switch` wird verwendet, um die Stufe und die aktuelle Bewegungsgeschwindigkeit der Schlange anzuzeigen.
    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;

    // Das Essen wurde gegessen, setze ffood auf 0, um das Essen neu zu zeichnen.
    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;
}
✨ Lösung prüfen und üben

Die Bewegung der Schlangenspiel-Schlange

Die Bewegung der Schlangenspiel-Schlange wird mit der Funktion movesnake implementiert. Wir werden den case DIR_UP-Abschnitt analysieren, da die anderen ähnlich sind:

/* Die Struktur TSnake ist eine umgekehrte verkettete Liste, bei der Kopf und Schwanz verbunden sind.
 * Beispiel: [a]<-[b]<-[c]<-[d]    a ist der Kopf
 *          |              ^     Wenn die Schlange sich bewegt, zeigt nur der Kopf auf d,
 *          `--------------'     und die (y,x)-Koordinaten von d werden auf die Position geändert, wohin der Kopf sich bewegt. */
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;
    }
}

Da der Koordinatenursprung in der oberen linken Ecke liegt und die Werte von (y,x) nach unten und nach rechts zunehmen, ist das Koordinatensystem um 180 Grad entlang der x-Achse gedreht. Daher müssen wir 1 subtrahieren, um die Schlange nach oben zu bewegen. Somit bewegt sich die Schlange nach oben, wenn der Spieler die Taste w/W drückt.

✨ Lösung prüfen und üben

Verlängerung der Schlangenkörper

Um die Länge des Schlangenkörpers zu erhöhen, können Sie dies durch die Implementierung der Funktion snakegrowup erreichen, die relativ einfach ist. Sie müssen einfach einen neuen Knoten in die Warteschlange einfügen.

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // Reserviere Speicher für einen neuen Schlangenknoten (struct TSnakeNode) und weise den Zeiger pnode auf den neuen Knoten zu. Dieser neue Knoten wird der neue Kopf der Schlange.
    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;
    }

    // Setze den Frontzeiger des neuen Knotens (pnode) auf den aktuellen Schlangenschwanz, um eine Verbindung zwischen neuem Kopf und Schwanz herzustellen.
    pnode->front = GetSnakeTail(psnake);

    // Setze den Frontzeiger des aktuellen Schlangenkopfs auf den neuen Knoten, um sicherzustellen, dass der neue Knoten der neue Kopf wird.
    psnake->head->front = pnode;
    psnake->head = pnode;

    // Erhöhe die Länge der Schlange, um anzuzeigen, dass der Schlangenkörper um eine Einheit verlängert wurde.
    ++psnake->length;
}

Die switch-Anweisung bestimmt die Koordinaten des neuen Kopf-Knotens basierend auf der aktuellen Bewegungsrichtung der Schlange (psnake->dir). Je nach verschiedenen Richtungen aktualisiert sie die Koordinaten des neuen Knotens, um ihn eine Zelle nach oben, unten, links oder rechts vom aktuellen Schlangenkopf (psnake->head) zu verschieben.

Zweck dieses Codes ist es, die Länge des Schlangenkörpers im Schlangenspiel zu erhöhen. Er erstellt einen neuen Kopf-Knoten basierend auf der aktuellen Bewegungsrichtung der Schlange und fügt diesen neuen Kopf-Knoten vorne in die Schlange ein, wodurch der Schlangenkörper länger wird. Dies ist die Kernlogik für die Verlängerung des Schlangenkörpers, nachdem die Schlange im Schlangenspiel Essen konsumiert hat.

✨ Lösung prüfen und üben

Die Position der Nahrungsherstellung

Die Funktion drawfoodw wird verwendet, um Nahrung im Spielfenster (win) zu zeichnen:

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);
}
  • Die Funktion random() wird verwendet, um zufällige Koordinatenwerte zu generieren, wodurch die Nahrung an einer zufälligen Position innerhalb des Spielfensters erscheinen wird.
  • Mit einer do-while-Schleife wird überprüft, ob die generierte Nahrungsposition mit einem beliebigen Körperknoten der Schlange überlappt. Wenn es eine Überlappung gibt, wird eine neue Nahrungsposition generiert, bis eine Position gefunden wird, die nicht mit dem Körper der Schlange überlappt.
  • Die Funktion mvwaddch wird verwendet, um die Form der Nahrung an der angegebenen Position im Spielfenster zu zeichnen.

Die Funktion checkfood wird verwendet, um sicherzustellen, dass die Nahrung nicht auf der Schlange erscheint:

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;
}
  • Zuerst wird der Schwanzknoten der Schlange abgerufen. Dann wird eine Schleife verwendet, um alle Körperknoten der Schlange zu durchlaufen und zu überprüfen, ob die Koordinaten der Nahrung mit den Koordinaten eines beliebigen Knotens übereinstimmen.
  • Wenn die Koordinaten der Nahrung mit den Koordinaten eines beliebigen Schlangenknotens übereinstimmen, bedeutet dies, dass die Position der Nahrung mit einem Körperknoten der Schlange überlappt, und die Funktion gibt False zurück.
  • Wenn keine Überlappung gefunden wird, gibt die Funktion True zurück, was bedeutet, dass die Nahrungsposition gültig ist.
✨ Lösung prüfen und üben

Grenzprüfung

checksnake wird verwendet, um zu überprüfen, ob die Schlange mit den Spielfeldgrenzen kollidiert ist. Dies umfasst die Prüfung auf Kollisionen mit den oberen, unteren, linken und rechten Grenzen des Spielfelds. Auch Kollisionen mit dem eigenen Körper der Schlange werden überprüft, da beide Situationen zum Spielende führen würden.

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

	/* Überprüfe, ob die Koordinaten des Schlangenkopfs mit den oberen, unteren, linken oder rechten Spielfeldgrenzen kollidiert haben */
    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;

	/* Überprüfe, dass der Schlangenkopf nicht mit einem beliebigen Teil seines Körpers kollidiert */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

  	/* Natürlich ist es erlaubt, mit der Nahrung zu kollidieren */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // Keine Kollision aufgetreten
}
✨ Lösung prüfen und üben

Spiel vorbei

Die Funktion gameover wird verwendet, um die "Spiel vorbei"-Nachricht in einem angegebenen Fenster (win) anzuzeigen, wenn das Schlangenspiel endet:

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);
}
  • Verwende die Funktion mvwprintw, um den "Spiel vorbei"-Nachrichtentext str in der Mitte des Fensters auszugeben. Dadurch wird sichergestellt, dass die "Spiel vorbei"-Nachricht zentriert im Fenster angezeigt wird.
  • Gib dann in der nächsten Zeile in der Mitte des Fensters "Press any key to quit..." aus, um den Spieler darauf hinzuweisen, dass er eine beliebige Taste drücken kann, um das Spiel zu beenden.
  • Verwende schließlich die Funktion wrefresh, um das Fenster zu aktualisieren und sicherzustellen, dass die "Spiel vorbei"-Nachricht und der Beenden-Hinweis korrekt auf dem Bildschirm gezeichnet werden.

Um Speicherlecks zu vermeiden, verwende die Funktion destroysnake, um den vom Schlangenspiel belegten Speicher freizugeben, einschließlich der Schlangenknoten und der Schlangenstruktur selbst. Dies ist ein üblicher Aufräumschritt, wenn das Spiel endet oder neu startet.

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

Innerhalb der Funktion werden zunächst zwei Zeiger, psnode und ptmp, deklariert, um über die Schlangenknotenverkettete Liste zu iterieren.

Es beginnt eine Schleife, die so oft durchläuft, wie die Schlange lang (Anzahl der Knoten) ist. In jeder Iteration werden folgende Operationen durchgeführt:

  • Setzt ptmp so, dass es auf den aktuellen Knoten psnode zeigt, um später den Speicher für den aktuellen Knoten freizugeben.
  • Verschiebt psnode zum nächsten Knoten (auf den von front gezeigten Knoten).

Nachdem die Schleife beendet ist, wurden alle Schlangenknoten freigegeben.

Schließlich gibt die Funktion den Speicher für die Schlangenstruktur selbst frei, free(psnake), und setzt den Zeiger psnake, der auf die Schlangenstruktur zeigt, auf NULL, um sicherzustellen, dass der freigegebene Speicher nicht mehr verwendet wird.

✨ Lösung prüfen und üben

Kompilierung und Ausführung

Der Kompilierungsbefehl unterscheidet sich etwas von dem üblichen. Es ist erforderlich, die Option -l bei gcc hinzuzufügen, um die ncurses-Bibliothek einzubeziehen:

cd ~/project
gcc -o snake snake.c -l ncurses
./snake
Snake Game
✨ Lösung prüfen und üben

Zusammenfassung

Sie haben erfolgreich ein einfaches Schlangenspiel in der Programmiersprache C unter Verwendung der ncurses-Bibliothek erstellt. Das Spiel umfasst ein Spielfenster, die Bewegung der Schlange, die Generierung von Nahrung und die Kollisionserkennung. Indem Sie die obigen Schritte befolgen, können Sie das Spiel im Terminal ausführen und genießen. Viel Spaß beim Spielen!