C语言实现Linux终端贪吃蛇:多线程与ncurses库实战
1. 项目概述一个Linux终端里的经典游戏如果你刚开始学C语言或者想找一个能综合运用数据结构、多线程和Linux系统编程的练手项目那在Linux终端里用C语言写一个贪吃蛇游戏绝对是个经典又过瘾的选择。这玩意儿听起来简单不就是一条蛇吃豆子变长嘛但真动起手来你会发现它几乎涵盖了C语言初阶到中阶的所有核心知识点指针、结构体、链表、内存管理、多线程、终端I/O控制还有最关键的——如何把零散的知识点串成一个能跑起来的、有交互的程序。我当年也是从这个项目开始真正理解了“程序是跑起来的逻辑”这句话。这个项目最终的目标是在你的Linux终端比如Ubuntu的GNOME Terminal或者更轻量的xterm里呈现一个字符界面的贪吃蛇游戏。蛇身用“[]”表示食物用“##”表示边界是“|”和“--”。你通过键盘的方向键控制蛇的移动吃到食物后蛇身变长撞到边界或者自己的身体则游戏重置。整个过程不需要任何图形库完全依赖ncurses库来处理终端的字符绘制和键盘输入这使得它非常轻量几乎能在任何Linux发行版上编译运行。为什么推荐这个项目因为它“麻雀虽小五脏俱全”。对于新手它能帮你巩固C语言基础语法和数据结构对于想深入理解Linux环境编程的开发者它涉及了进程/线程、非阻塞输入、定时刷新等概念。更重要的是当你看到自己写的代码能让一条“蛇”在终端里活起来那种成就感是看多少本书都换不来的。接下来我就带你从零开始拆解这个项目的每一个技术环节并分享我踩过的坑和总结的技巧。2. 核心设计思路与架构拆解在动手写代码之前我们先得把整个游戏的运行逻辑和代码结构想清楚。一个贪吃蛇游戏本质上是一个持续运行的状态机它包含几个核心模块游戏世界的表示地图、蛇、食物、游戏逻辑的驱动蛇的移动、碰撞检测、食物生成、用户输入的响应以及界面的实时渲染。在Linux字符终端这个特定环境下我们还需要解决如何在不使用图形界面的情况下实现“动画”效果和即时键盘响应。2.1 数据结构选型为什么是链表首先如何表示那条会不断变长的蛇最直观的数据结构就是链表。数组当然也可以但数组长度固定每次蛇变长都需要移动大量元素或者重新分配内存效率低下且实现麻烦。而链表则非常灵活蛇的每一节身体都是一个节点吃到食物时我们只需要在链表尾部动态添加一个新节点蛇移动时我们去掉链表头部的一个节点蛇尾离开旧位置并在尾部根据移动方向添加一个新节点蛇头进入新位置。这个“去头添尾”的操作完美契合了贪吃蛇移动的视觉效果。在C语言中我们用一个结构体struct Snake来表示每个节点它至少需要包含hang行坐标和lie列坐标用来确定该节身体在终端屏幕上的位置。注意终端坐标通常以左上角为原点(0,0)。next指针指向下一节身体的地址从而将单个节点串联成链表。有了这个结构整条蛇就是由head和tail两个指针维护的一个单向链表。head指向蛇头tail指向蛇尾。这个设计是游戏逻辑的核心骨架。2.2 游戏循环与多线程模型游戏需要同时做两件事一是以固定的时间间隔比如每秒4-6次更新游戏状态移动蛇、检查碰撞并重绘界面二是随时监听玩家的键盘输入并立即改变蛇的移动方向。如果只用单线程顺序执行就会遇到一个难题getch()这类获取键盘输入的函数默认是“阻塞”的程序会停在那里等待你按键期间游戏画面就卡住不动了。因此多线程是更优雅的解决方案。我们可以创建两个线程游戏逻辑与渲染线程在一个无限循环中每隔150-200毫秒调用moveSnake()更新蛇的位置调用gamePic()重绘整个地图然后调用refresh()将画面真正输出到终端。键盘监听线程在另一个无限循环中不断调用getch()获取按键并根据按键值如方向键来更新一个全局的方向变量dir。主线程main函数则负责初始化工作初始化ncurses、创建蛇和食物、绘制初始地图并启动这两个线程然后通过一个简单的while(1)循环或pthread_join来保持程序运行。两个线程通过共享的全局变量dir方向和key按键值进行通信这就是一个典型的“生产者-消费者”模型只不过生产的是键盘事件消费的是方向指令。2.3 终端图形化ncurses库简介在纯文本终端里画图需要精细控制光标位置和输出字符。自己处理转义序列如\033[2J清屏非常繁琐且容易出错。ncurses库就是为此而生的它提供了一套高级API让我们可以方便地在终端上创建“窗口”、移动光标、输出带属性的字符、处理键盘事件等。在这个项目中我们主要用到ncurses的几个核心函数initscr(): 初始化ncurses模式进入“全屏”管理状态。keypad(stdscr, TRUE): 启用标准屏幕stdscr对功能键如方向键的识别。noecho(): 关闭输入回显这样按键时字符不会显示在屏幕上。printw(): 类似printf但在ncurses窗口内格式化输出。move(y, x): 将光标移动到指定坐标。refresh(): 将内存中的屏幕内容刷新到物理终端上。getch(): 获取一个字符输入。endwin(): 结束ncurses模式恢复终端原始状态。使用ncurses后绘制游戏界面就变成了在双重循环中根据坐标判断该位置是边界、蛇身、食物还是空地然后调用printw输出对应字符的过程。2.4 模块化设计规划根据以上分析我们可以将代码划分为清晰的几个模块数据结构定义模块定义Snake结构体、全局变量蛇头head、蛇尾tail、食物food、方向dir等。初始化模块包含初始化蛇initSnake()和初始化食物initFood()的函数。游戏逻辑模块包含移动蛇moveSnake()、添加节点addNode()、删除节点deleteNode()、碰撞检测ifSnakedie()的核心函数。画面渲染模块主要是gamePic()函数负责遍历整个地图区域并打印出对应的字符。输入处理模块包含方向判断turn()和键盘监听线程函数changeDir()。线程函数模块包含游戏刷新线程函数refreshjiemian()。主程序模块main()函数负责串联所有模块初始化并启动线程。这样的划分使得代码结构清晰便于调试和维护。任何一个功能出问题你都能快速定位到对应的模块。3. 环境准备与核心代码实现详解理论说得再多不如一行代码。我们现在就进入实战环节我会逐模块解释代码并穿插我调试时遇到的坑和解决方法。3.1 开发环境搭建与ncurses库安装首先确保你有一个Linux环境。可以是物理机安装的Ubuntu、CentOS也可以是虚拟机里的系统甚至Windows下的WSLWindows Subsystem for Linux也完全没问题。我个人的开发环境是Ubuntu 22.04 LTS GCC编译器。第一步安装编译器和ncurses开发库。打开终端执行以下命令# 更新软件包列表 sudo apt update # 安装GCC编译器和make工具 sudo apt install build-essential # 安装ncurses开发库头文件和链接库 sudo apt install libncurses5-dev libncursesw5-dev对于CentOS/RHEL/Fedora系统命令类似sudo yum groupinstall Development Tools sudo yum install ncurses-devel安装完成后你可以写一个简单的测试程序test_ncurses.c来验证#include ncurses.h int main() { initscr(); // 初始化ncurses printw(Hello, Ncurses!); // 在屏幕上打印 refresh(); // 刷新屏幕 getch(); // 等待按键 endwin(); // 结束ncurses模式 return 0; }编译并运行gcc test_ncurses.c -o test_ncurses -lncurses ./test_ncurses如果屏幕清空并在左上角显示“Hello, Ncurses!”按任意键后程序退出说明环境配置成功。这里有个关键点编译时必须加上-lncurses参数来链接ncurses库否则会报“未定义的引用”错误。3.2 数据结构与全局变量定义我们创建一个名为snake.c的文件开始编写游戏代码。首先是头文件引入和全局数据结构的定义#include curses.h // ncurses库头文件 #include stdlib.h // 用于malloc, free, rand, srand #include pthread.h // 用于多线程编程 #include unistd.h // 用于usleep函数 #include time.h // 用于srand初始化随机种子 // 定义方向常量用正负值方便判断反向操作 #define UP 1 #define DOWN -1 #define LEFT 2 #define RIGHT -2 // 蛇身节点结构体 struct Snake { int hang; // 行坐标Y轴 int lie; // 列坐标X轴 struct Snake *next; // 指向下一个节点的指针 }; // 全局变量声明 struct Snake *head NULL; // 链表头指针指向蛇头 struct Snake *tail NULL; // 链表尾指针指向蛇尾 struct Snake food; // 食物结构体只需要坐标 int dir; // 当前蛇的移动方向 int key; // 存储键盘输入的键值调试时可显示注意事项与心得方向定义技巧我定义了UP1,DOWN-1,LEFT2,RIGHT-2。这样设计有个妙用通过取绝对值abs(dir)可以判断是否为水平方向绝对值为2或垂直方向绝对值为1。在turn()函数中可以用abs(dir) ! abs(direction)来防止蛇直接掉头比如向右时按左键无效这是一个经典的游戏体验优化。全局变量的使用在小型单文件项目中使用全局变量简化了参数传递。但在大型项目中应尽量避免过多的全局变量可以考虑封装到一个Game结构体中。这里为了清晰我们沿用简单设计。食物结构食物只需要坐标不需要链表所以用一个简单的结构体即可。3.3 核心功能函数实现3.3.1 初始化函数initSnake与initFood游戏开始或重置时需要初始化蛇和食物。// 初始化食物位置 void initFood() { // 设置随机种子通常用当前时间确保每次运行食物位置不同 // 注意srand(time(NULL))最好在main函数中只调用一次 int x rand() % 20; // 地图宽度为20生成0-19的随机数 int y rand() % 20; // 地图高度为20生成0-19的随机数 // 避免食物生成在第0列因为第0列是边界 if (y 0) { y 1 rand() % 19; // 重新生成1-19的随机数 } food.hang x; food.lie y; } // 初始化蛇 void initSnake() { struct Snake *p; dir RIGHT; // 初始方向向右 // 安全释放旧蛇占用的内存游戏重置时调用 while (head ! NULL) { p head; head head-next; free(p); } initFood(); // 初始化第一个食物 // 创建蛇头节点 head (struct Snake *)malloc(sizeof(struct Snake)); head-hang 2; // 初始位置(2,2)避免紧贴边界 head-lie 2; head-next NULL; tail head; // 初始时蛇只有一节头尾相同 // 通过addNode函数添加初始的几节身体 addNode(); addNode(); addNode(); }避坑指南内存泄漏在initSnake开头释放旧蛇内存的循环至关重要。如果游戏重置蛇死亡时没有释放之前malloc的节点就会造成内存泄漏。每次游戏重启内存占用都会增加最终可能导致程序崩溃。随机数rand()函数产生的是一系列伪随机数如果不调用srand设置种子每次程序启动生成的序列是一样的食物位置就会固定。所以要在main函数开头调用一次srand((unsigned int)time(NULL))。食物生成位置要确保食物不会生成在边界墙上即列坐标lie不能为0或20。上面的代码简单处理了lie0的情况但更严谨的做法是循环生成直到位置合法不在墙上且不在蛇身上。3.3.2 蛇的移动与生长addNode与deleteNode这是游戏逻辑的核心理解了它就理解了贪吃蛇的本质。// 在蛇尾添加一个新节点蛇移动或吃食物后生长 void addNode() { struct Snake *new_node (struct Snake *)malloc(sizeof(struct Snake)); if (new_node NULL) { // 内存分配失败处理简单打印错误并退出 endwin(); fprintf(stderr, Memory allocation failed!\n); exit(EXIT_FAILURE); } // 根据当前移动方向计算新节点的位置 switch (dir) { case UP: new_node-hang tail-hang - 1; new_node-lie tail-lie; break; case DOWN: new_node-hang tail-hang 1; new_node-lie tail-lie; break; case LEFT: new_node-hang tail-hang; new_node-lie tail-lie - 1; break; case RIGHT: new_node-hang tail-hang; new_node-lie tail-lie 1; break; default: // 默认情况理论上不会进入保持原位置 new_node-hang tail-hang; new_node-lie tail-lie; break; } new_node-next NULL; tail-next new_node; // 将新节点链接到当前尾部 tail new_node; // 更新尾指针指向新节点 } // 删除蛇头节点蛇移动时尾部前进头部离开 void deleteNode() { if (head NULL) return; // 防御性编程防止空指针 struct Snake *temp head; head head-next; // 头指针指向下一节 free(temp); // 释放原头节点内存 // 如果删除后链表为空尾指针也应置空虽然游戏中蛇不会完全消失 if (head NULL) { tail NULL; } }核心逻辑解析 贪吃蛇的移动动画在数据结构层面是通过“去头添尾”实现的但视觉效果是蛇头向前走。移动每次移动先addNode()在蛇尾方向前方添加一个新节点这将成为新的蛇头位置然后检查新位置是否有食物。吃到食物如果有食物则initFood()生成新食物不调用deleteNode()。这样链表长度增加1实现了蛇的生长。没吃到食物如果没有食物则调用deleteNode()移除链表最旧的一个节点原蛇尾。链表长度不变实现了蛇的整体前移。addNode函数中根据dir计算新节点坐标的逻辑是理解移动的关键。注意新节点的坐标是基于当前tail蛇尾的位置计算的这符合“蛇头向前生长”的视觉。实际上在移动时tail指向的是即将成为新蛇头的节点。3.3.3 碰撞检测与游戏状态判断游戏需要判断两种失败条件撞墙和撞到自己。// 辅助函数1判断坐标(i,j)是否是蛇身的一部分 int isSnakeNode(int i, int j) { struct Snake *p head; while (p ! NULL) { if (p-hang i p-lie j) { return 1; // 是蛇身 } p p-next; } return 0; // 不是蛇身 } // 辅助函数2判断坐标(i,j)是否是食物位置 int isFood(int i, int j) { return (food.hang i food.lie j); } // 碰撞检测判断蛇是否死亡撞墙或撞自身 int isGameOver() { // 情况1撞墙。地图边界是[0,19]蛇头(tail)坐标超出即撞墙。 // 注意我们的地图绘制时第0行和第19行是上下边界第0列和第20列是左右边界。 // 因此蛇头坐标hang或lie等于这些边界值即视为撞墙。 if (tail-hang 0 || tail-hang 20 || tail-lie 0 || tail-lie 20) { return 1; } // 情况2撞自身。遍历蛇身链表从head到tail的前一个节点检查是否有节点坐标与蛇头(tail)坐标相同。 struct Snake *p head; // 注意tail是蛇头所以我们只需检查head到倒数第二节即p ! tail while (p ! NULL p ! tail) { if (p-hang tail-hang p-lie tail-lie) { return 1; } p p-next; } return 0; // 游戏继续 }重要细节与优化边界判断地图坐标范围需要与绘制函数gamePic严格对应。如果gamePic中边界符号绘制在第0行、第19行、第0列、第20列那么蛇头坐标hang为0或19lie为0或20时视觉上就已经撞上了。所以判断条件用tail-hang 0 || tail-hang 20 || tail-lie 0 || tail-lie 20。这里的0和20需要根据你的绘制逻辑微调。自撞判断优化原始代码中遍历了整个链表while(p-next ! NULL)这包括了蛇头tail自身所以多了一次无意义的比较。更高效的写法是遍历head到tail的前一个节点如上面代码所示。虽然在这个小游戏中性能差异可忽略但养成精确的习惯很重要。isSnakeNode和isFood函数这两个辅助函数会被gamePic绘制函数频繁调用用于判断每个格子该画什么。将它们独立出来使代码更清晰。3.3.4 游戏画面绘制函数gamePic这是将内存中的数据状态转换为终端字符画面的函数。void gamePic() { int hang, lie; move(0, 0); // 关键将光标移回屏幕左上角实现“覆盖式”刷新避免画面滚动。 // 绘制上边界 for (lie 0; lie 20; lie) { printw(--); } printw(\n); // 绘制中间区域行1到行18 for (hang 0; hang 20; hang) { printw(|); // 左边界 for (lie 1; lie 20; lie) { // 列从1开始因为0是边界 if (isSnakeNode(hang, lie)) { printw([]); // 蛇身 } else if (isFood(hang, lie)) { printw(##); // 食物 } else { printw( ); // 空地两个空格 } } printw(|\n); // 右边界并换行 } // 绘制下边界 for (lie 0; lie 20; lie) { printw(--); } printw(\n); // 可选的在底部打印一些状态信息如当前方向、长度、得分等便于调试。 // printw(Direction: %d | Press q to quit.\n, dir); }绘制技巧与坑点move(0,0)这是实现“动画”效果的关键。每次重绘前将光标移回原点然后重新绘制整个画面新的画面会覆盖旧的从而产生动态效果。如果没有这个每次printw都会接着上次的内容往后打印屏幕会不断向下滚动体验极差。坐标映射注意for循环中的坐标范围。我们定义了20x20的游戏区域不含边界。hang从0到19lie从1到20因为第0列是左边界|。这个映射关系必须与isSnakeNode和isFood函数中的判断逻辑完全一致否则会出现蛇或食物显示位置错乱。字符宽度我们使用[]、##、--、|、 两个空格这些占两个字符宽度的元素来绘制以保证对齐。如果使用单字符如*、边界和空地也需要用单字符否则排版会乱。刷新效率在简单的字符界面中全屏重绘是可以接受的。如果地图很大可以考虑局部刷新优化但本项目无需。3.3.5 方向控制与线程函数方向控制需要处理即时输入和防止反向移动。// 改变方向防止180度直接反向 void turn(int direction) { if (abs(dir) ! abs(direction)) { dir direction; } // 如果abs(dir) abs(direction)说明是按了反方向键忽略此次输入 } // 键盘监听线程函数 void* keyMonitorThread(void* arg) { while (1) { int ch getch(); // 阻塞等待按键但由于在独立线程不影响主线程刷新 switch (ch) { case KEY_UP: turn(UP); break; case KEY_DOWN: turn(DOWN); break; case KEY_LEFT: turn(LEFT); break; case KEY_RIGHT: turn(RIGHT); break; case q: case Q: // 增加退出机制 endwin(); exit(0); break; default: // 可忽略其他按键或处理暂停等功能 break; } } return NULL; } // 游戏逻辑刷新线程函数 void* gameRefreshThread(void* arg) { while (1) { moveSnake(); // 移动蛇并处理食物碰撞 gamePic(); // 重绘画面 refresh(); // 刷新到屏幕 usleep(200000); // 休眠200毫秒控制游戏速度。值越小蛇移动越快。 } return NULL; }多线程编程要点线程安全两个线程共享dir和key等全局变量。turn函数中的方向修改是原子的一个简单的赋值在这个简单场景下不会引发严重的数据竞争。但如果涉及更复杂的共享状态就需要使用互斥锁pthread_mutex_t来保护。getch()的阻塞性在keyMonitorThread中getch()是阻塞的但因为它运行在独立线程所以不会阻塞gameRefreshThread的定时刷新。这是多线程解决输入阻塞问题的典型应用。游戏速度控制usleep(200000)让线程休眠200毫秒即0.2秒这控制了游戏刷新的帧率也就是蛇的移动速度。你可以通过调整这个值来改变游戏难度。优雅退出原始代码的main函数是while(1);死循环只能通过CtrlC强制结束。我在键盘监听线程中添加了对q键的检测调用endwin()恢复终端并退出这是一种更友好的退出方式。3.3.6 主函数main的整合最后在main函数中将所有模块串联起来。int main() { pthread_t refresh_tid, keymonitor_tid; // 设置随机数种子 srand((unsigned int)time(NULL)); // 1. 初始化ncurses initscr(); keypad(stdscr, TRUE); // 启用功能键识别 noecho(); // 关闭输入回显 curs_set(0); // 可选隐藏光标使界面更干净 // 2. 初始化游戏状态 initSnake(); // 3. 创建线程 pthread_create(refresh_tid, NULL, gameRefreshThread, NULL); pthread_create(keymonitor_tid, NULL, keyMonitorThread, NULL); // 4. 主线程等待或简单循环。更佳实践是使用pthread_join等待线程结束。 // while(1) { sleep(1); } // 简单保持主线程不退出 pthread_join(refresh_tid, NULL); // 等待游戏刷新线程实际上它不会结束 pthread_join(keymonitor_tid, NULL); // 等待键盘监听线程 // 以下代码通常不会执行因为线程是无限循环。 // 但为保持良好习惯仍保留清理代码。 endwin(); return 0; }编译命令 将以上所有代码段整合到snake.c文件中后使用以下命令编译gcc snake.c -o snake -lncurses -lpthread-lncurses链接ncurses库。-lpthread链接POSIX线程库。运行游戏./snake现在你应该能看到一个在终端中运行的贪吃蛇游戏了用方向键控制吃到##食物会变长撞墙或撞到自己身体游戏会重置。4. 常见问题、调试技巧与深度优化代码能跑起来只是第一步让它跑得稳定、高效、体验好才是进阶之路。下面是我在开发和调试过程中遇到的一些典型问题及解决方案以及一些可以尝试的优化方向。4.1 编译与运行常见问题编译错误undefined reference to ‘initscr’等ncurses函数问题原因忘记链接ncurses库。解决方案确保编译命令中包含-lncurses参数。如果还报错尝试-lncursesw宽字符版本或确认库已正确安装sudo apt install libncurses5-dev。编译错误undefined reference to ‘pthread_create’问题原因忘记链接pthread库。解决方案在编译命令中添加-lpthread参数。注意-pthreadGCC和-lpthread链接器在大多数情况下等效但-pthread会定义一些宏更推荐使用-pthread。运行时方向键失灵或输入异常问题原因ncurses没有正确初始化或功能键未启用。解决方案检查main函数中是否按顺序调用了initscr()、keypad(stdscr, TRUE)、noecho()。确保keypad(stdscr, TRUE)在initscr()之后调用。keypad启用后getch()才能返回KEY_UP、KEY_DOWN等特殊键值。如果按方向键输出的是^[[A这样的转义序列说明keypad未启用或终端类型不支持。游戏画面闪烁或残留问题原因刷新逻辑有问题。可能是没有在每次绘制前调用move(0,0)或者refresh()调用时机不对。解决方案确保gamePic()函数开头有move(0,0)。确保在gamePic()绘制完所有内容后调用一次refresh()。如果仍有轻微闪烁可以尝试使用ncurses的双缓冲机制wnoutrefresh()和doupdate()但对于这个小游戏refresh通常足够。蛇的移动速度不稳定或过快问题原因usleep的休眠时间设置不当或者线程调度导致时间不精确。解决方案usleep(200000)是200毫秒即每秒移动5次。你可以根据感觉调整这个值1000000.1秒会更快。usleep的精度是微秒级但受系统负载和调度影响。对于游戏循环这是一个简单有效的方法。如果需要更精确的定时可以考虑使用nanosleep或基于时钟的定时器但复杂度会增加。4.2 逻辑Bug与调试技巧蛇会穿墙或出现在奇怪位置排查思路首先检查坐标计算在addNode函数中打印出新节点的坐标(hang, lie)看是否符合方向逻辑。检查边界条件确认isGameOver函数中的边界判断条件是否与gamePic绘制的实际边界匹配。例如如果地图宽20列索引0-19边界画在0和19那么蛇头坐标lie等于0或19时就应判为撞墙。检查初始化位置initSnake中蛇的初始位置(2,2)是否在合法区域内。吃到食物后蛇没有变长或者变长异常排查思路核心逻辑检查回顾moveSnake函数。正确的逻辑是先addNode新蛇头然后判断新头位置是否有食物hasFoodnode(tail-hang, tail-lie)。如果有食物只调用initFood生成新食物不调用deleteNode。如果没有食物则调用deleteNode。食物坐标判断确保hasFoodnode或isFood函数比较的是蛇头tail和食物food的坐标。链表操作验证在addNode和deleteNode函数中加入调试打印输出链表长度或节点坐标观察链表变化是否符合预期。蛇可以180度直接掉头问题原因turn函数中的方向判断逻辑有误或未生效。解决方案确保turn函数中使用了abs(dir) ! abs(direction)进行判断。例如当前方向dir是RIGHT-2其绝对值是2。如果按下LEFT键2绝对值相等则忽略此次输入。游戏重置后旧蛇内存未释放导致内存泄漏验证方法在initSnake函数的开头和结尾打印head的地址。如果每次重置后地址都不同且旧地址没有被free则存在泄漏。更专业的工具是使用valgrind。valgrind --leak-checkfull ./snake运行游戏玩一会儿后退出按qvalgrind会报告是否有内存泄漏。确保initSnake开头的while循环正确释放了所有节点。4.3 功能扩展与优化建议基础版本完成后你可以尝试添加更多功能让游戏更完善计分系统增加一个全局变量int score。在moveSnake函数中当蛇吃到食物时if (hasFoodnode(...))除了initFood再执行score 10;。在gamePic函数底部用printw打印当前分数例如printw(Score: %d\n, score);。游戏重置时initSnake将score归零。游戏暂停/继续功能增加一个全局状态变量int is_paused 0;。在键盘监听线程中监听空格键 或p键。按下时将is_paused取反is_paused !is_paused;。在游戏刷新线程gameRefreshThread的循环开头检查if (is_paused) { usleep(100000); continue; }如果暂停则短时间休眠并跳过本次逻辑更新和绘制。游戏难度分级将usleep的延时时间作为一个变量int game_speed 200000;单位微秒。随着分数增加逐渐减小game_speed例如每得100分game_speed - 20000但设置一个下限如50000。在游戏刷新线程中使用这个变量usleep(game_speed);。更友好的游戏结束与重启当isGameOver返回1时不要立即调用initSnake重置。可以设置一个游戏结束标志int game_over 1;。在gameRefreshThread中如果game_over为1则绘制一个“Game Over”画面并提示“Press R to restart, Q to quit”。在键盘监听线程中检测到r键时调用initSnake并重置score和game_over0。代码结构优化将全局变量封装到一个Game结构体中如struct Game { struct Snake *head, *tail; struct Snake food; int dir, score, speed, is_paused, is_over; };。这样更利于管理状态也便于未来扩展为多局游戏。将函数声明放到头文件snake.h中实现分离使代码更模块化。这个基于C语言和Linux的贪吃蛇项目虽然代码量不大但它像一颗棱镜折射出了系统编程的多个基本面。从数据结构的灵活运用到多线程对并发问题的初步解决再到与终端这种“古老”而强大的设备交互每一步都踩在实用的点上。我建议你在能流畅运行基础版本后不要停下试着去实现上面提到的一两个扩展功能。这个过程里遇到的编译错误、逻辑bug、性能问题才是真正让你从“知道”到“理解”的阶梯。当你亲手解决了蛇身闪烁、内存泄漏或者按键响应不跟手这些问题之后你对程序运行的理解会深刻得多。编程的乐趣一半在于构思另一半就在于这样一点点调试、优化最终让作品按照你的意愿完美运行起来。
