用 C 语言创建贪吃蛇游戏

CBeginner
立即练习

介绍

在这个项目中,你将学习如何使用 ncurses 库在 C 语言中创建一个简单的贪吃蛇游戏。这个经典游戏需要控制一条蛇去吃食物,使其变长,同时避免与墙壁和自身发生碰撞。游戏功能被分解为几个关键组件:初始化、游戏循环、蛇的移动、碰撞检测等等。在这个项目结束时,你将拥有一个可以在终端上运行的基本贪吃蛇游戏。

👀 预览

Snake Game

🎯 任务

在这个项目中,你将学习:

  • 如何实现游戏循环以更新蛇的位置并处理用户输入。
  • 如何创建函数来初始化游戏、绘制游戏窗口以及显示游戏结束消息。
  • 如何实现碰撞检测以检查与墙壁、蛇自身身体和食物的碰撞。
  • 如何开发诸如蛇吃到食物时增加其长度的功能。

🏆 成果

完成这个项目后,你将能够:

  • 使用 C 语言中的 ncurses 库创建一个基于终端的游戏。
  • 实现游戏逻辑,包括更新游戏状态和处理用户输入。
  • 创建并操作数据结构来表示游戏对象,如蛇和食物。
  • 实现碰撞检测以提供游戏规则并确定游戏何时结束。

基础知识

在电传打字机广泛使用的时代,电传打字机充当通过电缆连接到中央计算机的输出终端。用户必须向终端程序发送一系列特定的控制命令,以控制终端屏幕上的输出。例如,更改屏幕上的光标位置、清除屏幕上某个区域的内容、滚动屏幕、切换显示模式、给文本加下划线、更改字符的外观、颜色、亮度等等。这些控制是通过一个称为转义序列的字符串来实现的。之所以称为转义序列,是因为这些连续的字节以 0x1B 字符开头,它是转义字符(通过按下 ESC 键输入的字符)。即使在现在,我们也可以通过在终端仿真程序中输入转义序列来模拟那个时代电传打字机终端的输出效果。如果你想在终端(或终端仿真程序)上以彩色背景显示一段文本,可以在命令提示符中输入以下转义序列:

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

这里,^[ 就是所谓的转义字符。(注意:在这种情况下,^[ 是一个字符。它不是通过依次输入 ^[ 字符来输入的。要输入这个字符,你必须先按下 Ctrl+V,然后再按下 ESC 键。)执行上述命令后,你应该会看到 In Color 的背景变为红色。从那时起,所有显示的文本都将以这种效果输出。如果你想终止这种效果并恢复到原来的格式,可以使用以下命令:

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

现在你知道这些字符(转义序列)的用途了吧?(试着更改分号之间的参数,看看会得到什么结果。)也许结果会和你想象的不一样?这可能是因为终端环境不同,这取决于不同的终端或操作系统。(你不能让单色终端显示彩色字符,对吧?)为了避免这种兼容性问题,并在不同终端上实现一致的输出,UNIX 的设计者发明了一种称为 termcap 的机制。termcap 实际上是一个与转义序列一起发布的文件。这个文件列出了当前终端可以正确执行的所有转义序列,确保输入的转义序列的执行结果符合此文件中的规范。然而,在这种机制发明后的几年里,另一种称为 terminfo 的机制逐渐取代了 termcap。从那时起,用户在编程时不再需要查阅 termcap 中复杂的转义序列规范,只需要访问 terminfo 数据库来控制屏幕输出。

假设在使用 terminfo 的情况下,所有应用程序都通过访问 terminfo 数据库来控制输出(例如发送控制字符等),很快这些代码调用就会使整个程序变得难以控制和管理。这些问题的出现导致了 CURSES 的诞生。CURSES 这个名字来自一个叫做 cursor optimization 的双关语。

CURSES 库通过封装终端的底层控制代码(转义序列),为用户提供了一个灵活高效的 API(应用程序编程接口),允许用户控制光标、创建窗口、更改前景色和背景色以及处理鼠标操作。这使得用户在字符终端上编写应用程序时可以绕过那些烦人的底层机制。

NCURSES 是来自 System V Release 4.0 (SVr4)CURSES 的克隆。它是一个可自由配置的库,与旧版本的 CURSES 完全兼容。简而言之,它是一个允许应用程序直接控制终端屏幕显示的库。后面提到 CURSES 库时,也指的是 NCURSES 库。

NCURSES 不仅封装了底层终端功能,还提供了一个相当稳定的工作框架来生成漂亮的界面。它包括用于创建窗口的函数。它的姊妹库 MenuPanelFormCURSES 基础库的扩展。这些库通常与 CURSES 一起分发。我们可以构建一个包含多个窗口、菜单、面板和表单的应用程序。窗口可以独立管理,例如滚动或隐藏它们。菜单允许用户创建命令选项以便于执行命令。表单允许用户创建用于简单数据输入和显示的窗口。面板是 NCURSES 窗口管理功能的扩展,可以覆盖或堆叠窗口。

定义常量

首先,打开终端并执行以下命令来安装 ncurses 库:

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

导航到 ~/project 目录并创建项目文件 snake.c

cd ~/project
touch snake.c

接下来,我们需要编写 C 代码。第一步是包含头文件:

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

在编写 main() 函数之前,让我们完成一些基本任务。由于我们使用的是终端字符界面,ASCII 字符至关重要。因此,我们需要定义一些常量:

#define TBool            int
#define True             1
#define False            0
#define SHAPE_FOOD       '@'  // 食物
#define SHAPE_SNAKE      '#'  // 蛇身
#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; // 声明一个日志窗口
#define INITRUNLOG()     logwin = newlogw() // 通过调用自定义函数 newlogw() 创建一个日志窗口
#define RUNLOG(str)      runlog(logwin, str) // 运行日志窗口以显示游戏提示
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // 玩家等级,全局变量

我们还添加了一些结构体和枚举定义:

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

现在我们将声明我们要创建的函数:

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

主函数

int main()
{
    initscr();  /* 初始化,进入 ncurses 模式 */
    raw();      /* 禁用行缓冲,立即查看结果 */
    noecho();   /* 不在终端上显示控制字符,如 Ctrl+C */
    keypad(stdscr, TRUE);   /* 允许用户在终端中使用键盘 */
    curs_set(0);    /* 设置光标可见性,0 为不可见,1 为可见,2 为完全可见 */
    refresh();      /* 将虚拟屏幕的内容写入显示器并刷新 */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  按 'q' 或 'Q' 退出。");
    RUNLOG("  按 'w/s/a/d' 或 'W/S/A/D' 移动蛇。");
    RUNLOG("信息:");

    WINDOW *gwin = newgamew(); /* 创建游戏窗口,由自定义函数 newgamew 实现 */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

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

    /* getch() 与 getchar() 不同 */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* 清除游戏窗口并释放窗口数据结构的内存和信息 */
    DESTROYRUNLOG(); /* 清除信息显示窗口 */
    endwin();        /* 退出 ncurses 模式 */

    return 0;
}

keypad(stdscr, TRUE) 中,stdscr 指的是一个虚拟窗口,我们所有的操作首先会写入这个窗口,然后使用 refresh 函数将 stdscr 的内容显示在屏幕上。

当我们使用 printw 时,数据实际上是打印在一个名为 stdscr 的虚拟窗口上,而不是直接输出到屏幕。printw() 函数的目的是在虚拟显示器上不断写入一些显示标记和相关数据结构,并将这些数据写入 stdscr 的缓冲区。所以,为了显示这个缓冲区中的数据,我们必须使用 refresh() 函数告诉 curses 系统将缓冲区的内容输出到屏幕。这种机制允许程序员在虚拟屏幕上不断写入数据,并在调用 refresh() 时使其看起来像是一次性完成的。因为 refresh() 函数只检查窗口和数据中已更改的部分,这种灵活的设计提供了一种高效的反馈机制。

窗口机制

窗口机制是 CURSES 的核心概念。从前面的示例中你已经看到,所有函数默认都在一个“窗口”(stdscr)上操作。即使你在设计最简单的图形用户界面(GUI),也仍然需要使用窗口。使用窗口的主要原因之一是,你可以将屏幕划分为不同部分,并在其中同时进行操作。这可以提高效率。另一个原因是,在程序设计中你应该始终追求更好、更易于管理的设计。如果你正在设计一个大型复杂的用户界面,预先设计这些部分会提高你的效率。

WINDOW* newlogw()
{
    /* 参数依次为:窗口高度、宽度、窗口起始位置 (y,x) */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* 参数依次为:已知窗口指针、0 和 0 是字符的默认行和列起始位置 */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // 刷新指定窗口

    return win;
}

WINDOW* newlogw() 函数中,你可以看到窗口的创建从 newwin() 开始。虽然我们创建了一个窗口,但它还不可见,这类似于 HTML 中的 <div> 元素。如果你不给 <div> 元素添加样式,在网页上你将看不到任何东西。所以我们需要使用 box 函数为已知窗口添加边框。

mvwprintw 在指定窗口内的指定坐标 (y,x) 处打印指定内容。

显示游戏信息

RUNLOG 是一个调用自定义函数 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) {

        /* 自定义函数,cleanline */
        cleanline(win, i+1, 1);

        /* 在每一行打印字符串 */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

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

runlog 以一个已知窗口和要显示的信息作为参数。str 将使用 strcpy 函数存储在一个二维数组(logbuf)中。然后,调用自定义函数 cleanline 来清除接下来要打印的行。mvwprintw 函数用于打印信息。

/* 清除窗口 win 的坐标 (x,y) 处的内容 */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* 将数组的 0 - 56 位置设置为空字符 */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* 将光标移动到窗口 win 的 (y,x) 位置并打印字符串 EMPTYLINE */
    mvwprintw(win, y, x, EMPTYLINE);

    /* 在指定窗口显示内容 */
    wrefresh(win);
}

初始化贪吃蛇队列

现在,你将定义蛇的数据结构,并创建用于初始化它以及在游戏窗口上绘制它的函数。

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

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // 将蛇的长度初始化为 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;
}

这两个函数的目的是在蛇游戏中初始化蛇的数据结构,包括设置蛇的初始方向和长度,以及创建具有初始身体节点的蛇。函数 newsnakenode 用于创建单个节点,然后通过嵌套函数调用将这些节点连接在一起,形成初始的蛇。

在游戏窗口中显示贪吃蛇

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

为了显示每个节点的 (y,x) 坐标,我们可以使用宏定义 GetSnakeTail 来获取坐标。剩下的步骤是使用循环通过 mvwaddch 来显示它们。mvwaddch 用于将光标移动到指定窗口(win)中的指定位置 (taily,tailx),然后输出一个字符。

要实际在屏幕上显示它,我们需要使用 wrefresh(win)

编写游戏核心

看看 while (refreshgamew(gwin, psnake) >= 0); 后面的分号。这建立了一个循环来处理蛇的移动、长度变化以及边界检测等问题。

while 循环调用 refreshgamew 函数:

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* 当开始游戏或食物被吃掉时,ffood 为 False,执行 drawfoodw 重新绘制食物 */
    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;

    // 接下来,我们将使用 select 内核 API,它会监听一系列键盘和鼠标输入。我们使用 `key=getch()` 和 `switch` 来确定输入类型。第二个 `switch` 用于显示等级和蛇当前的移动速度。
    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;

    // 食物被吃掉,将 ffood 设置为 0 以重新绘制食物。
    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;
}

贪吃蛇的移动

贪吃蛇的移动是通过函数 movesnake 实现的。我们将分析 case DIR_UP 部分,因为其他部分类似:

/* 结构体 TSnake 是一个首尾相连的反向链表。
 * 示例:[a]<-[b]<-[c]<-[d]    a 是头部
 *          |              ^     当蛇移动时,只有头部指向 d,
 *          `--------------'     并且 d 的 (y,x) 被修改为头部移动到的位置。 */
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;
    }
}

由于坐标原点位于左上角,(y,x) 的值向下和向右增加,所以坐标系沿 x 轴翻转了 180 度。因此,为了使蛇向上移动,我们减去 1。这样,当玩家按下 w/W 键时,蛇就会向上移动。

延长蛇身

要增加蛇身的长度,可以通过实现 snakegrowup 函数来达成,这相对比较简单。只需在队列中添加一个新节点即可。

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // 为新的蛇节点(struct TSnakeNode)分配内存,并将指针 pnode 指向新节点。这个新节点将成为蛇的新头部。
    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;
    }

    // 将新节点(pnode)的前指针设置为当前蛇尾,以便在新头部和尾部之间建立连接。
    pnode->front = GetSnakeTail(psnake);

    // 将当前蛇头的前指针设置为新节点,以确保新节点成为新的头部。
    psnake->head->front = pnode;
    psnake->head = pnode;

    // 增加蛇的长度,以表明蛇身已延长一个单位。
    ++psnake->length;
}

switch 语句根据蛇当前的移动方向 (psnake->dir) 来确定新头部节点的坐标。根据不同的方向,它将新节点的坐标更新为从当前蛇头 (psnake->head) 向上、向下、向左或向右移动一个单元格的位置。

这段代码的目的是在贪吃蛇游戏中增加蛇身的长度。它根据蛇当前的移动方向创建一个新的头部节点,并将这个新头部节点插入到蛇的前端,使蛇身变长。这是贪吃蛇游戏中蛇吃掉食物后延长蛇身的核心逻辑。

食物生成的位置

drawfoodw 函数用于在游戏窗口(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);
}
  • random() 函数用于生成随机坐标值,这会使食物出现在游戏窗口内的随机位置。
  • 使用 do-while 循环,它检查生成的食物位置是否与蛇的任何身体节点重叠。如果有重叠,它会生成一个新的食物位置,直到找到一个不与蛇身重叠的位置。
  • mvwaddch 函数用于在游戏窗口的指定位置绘制食物的形状。

checkfood 函数用于检查食物是否不会出现在蛇身上:

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;
}
  • 首先,它获取蛇的尾节点。然后,它使用一个循环遍历蛇的所有身体节点,并检查食物的坐标是否与任何节点的坐标匹配。
  • 如果食物的坐标与任何蛇节点的坐标匹配,这意味着食物的位置与蛇的一个身体节点重叠,函数返回 False
  • 如果没有找到重叠,函数返回 True,表示食物位置有效。

边界检测

checksnake 用于检查蛇是否与游戏边界发生碰撞。这包括对游戏顶部、底部、左侧和右侧边界的检测。同时也会检查蛇是否与自身身体发生碰撞,因为这两种情况都会导致游戏结束。

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

    /* 检查蛇头的坐标是否与游戏顶部、底部、左侧或右侧的边界发生碰撞 */
    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;

    /* 检查蛇头是否与身体的任何部分发生碰撞 */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

    /* 当然,与食物碰撞是允许的 */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // 没有发生碰撞
}

游戏结束

gameover 函数用于在贪吃蛇游戏结束时,在指定窗口(win)上显示游戏结束的消息:

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);
}
  • 使用 mvwprintw 函数在窗口中间打印游戏结束消息文本 str。这确保游戏结束消息在窗口中居中显示。
  • 接下来,在窗口中间的下一行打印“Press any key to quit...”,提示玩家按任意键退出游戏。
  • 最后,使用 wrefresh 函数刷新窗口,确保游戏结束消息和退出提示正确绘制在屏幕上。

为避免内存泄漏,使用 destroysnake 函数释放贪吃蛇游戏占用的内存,包括蛇节点和蛇结构本身。这是游戏结束或重新开始时常见的清理步骤。

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

在函数内部,它首先声明两个指针 psnodeptmp,用于遍历蛇节点链表。

它进入一个循环,循环次数等于蛇的长度(节点数量)。在每次迭代中,它执行以下操作:

  • ptmp 设置为指向当前节点 psnode,以便稍后释放当前节点的内存。
  • psnode 移动到下一个节点(由 front 指向)。

循环结束后,所有蛇节点都已被释放。

最后,函数释放蛇结构本身的内存,free(psnake),并将指向蛇结构的指针 psnake 设置为 NULL,以确保不再使用已释放的内存。

编译与执行

编译命令与通常情况稍有不同。它需要在 gcc 中添加 -l 选项以包含 ncurses 库:

cd ~/project
gcc -o snake snake.c -l ncurses
./snake

贪吃蛇游戏

总结

你已经成功地使用 ncurses 库用 C 语言创建了一个简单的贪吃蛇游戏。该游戏包括一个游戏窗口、蛇的移动、食物生成和碰撞检测。按照上述步骤,你可以在终端上运行并享受这个游戏。玩得开心!

✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习✨ 查看解决方案并练习