ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于linux上的终端贪吃蛇

基于linux上的终端贪吃蛇 摘要不依赖图形库用 C 语言在 Linux 终端实现贪吃蛇。本文从墙体绘制、蛇节点打印与移动、非阻塞键盘输入、食物随机生成到碰撞检测逐步拆解核心逻辑并附上关键代码与常见问题解决方案。前言本文面向具备 C 语言基础、希望了解终端游戏开发思路的读者。阅读前建议先掌握 printf、结构体、链表和指针等基本概念。本项目不依赖任何图形库仅通过 ANSI 转义序列控制光标位置在 Linux 终端中实现完整的贪吃蛇游戏代码结构清晰、逻辑直观适合作为链表与终端编程的入门实战项目。目录一、大致逻辑二、具体实现1. 墙体设置2. 光标定位3. 蛇节点的打印4. 蛇节点的移动5. 食物生成6. 墙体的生成7. 碰撞检测7.1 蛇自己的碰撞检测7.2 边缘墙体检测7.3 游戏生成的墙体碰撞检测7.4 蛇吃食物的碰撞检测8. 游戏功能与难度控制三、常见问题与解决方案本文主要讲解贪吃蛇代码的逻辑实现如果有源码需求可通过下面的 GitHub 链接访问我的仓库下载https://github.com/wwx-taffy/taffy-s-git本项目不依赖图形库实现所以代码可能存在冗余或部分 bug望见谅。示例图一、大致逻辑我们所谓的蛇其实本质上就是一个 printf 打印出来的几个特殊字符和我们平时打印数字或字母没什么本质区别。之所以我们的蛇呈现的是移动的效果是因为我们通过一个 while(1) 循环不断地 printf 这几个特殊字符和我们平时 printf 唯一有区别的是它们指定了位置打印通过 fflush 不断刷新缓冲区来实现所谓的移动效果。我们所谓的蛇其实本质上就是一个 printf 打印出来的几个特殊字符和我们平时打印数字或字母没什么本质区别。之所以我们的蛇呈现的是移动的效果是因为我们通过一个 while(1) 循环不断地 printf 这几个特殊字符和我们平时 printf 唯一有区别的是它们指定了位置打印通过 fflush 不断刷新缓冲区来实现所谓的移动效果。// 如果不了解 fflush建议自行查阅资料它是这里的核心并不难理解。二、具体实现1. 墙体设置贪吃蛇的活动范围是清晰的撞到墙体游戏就会结束所以墙体的设置必不可少。下面通过讲解墙体的打印来梳理思路。2. 光标定位平时使用 printf 时输出内容通常固定在第一行或者用几个空格隔开无法像平面直角坐标系那样指定位置打印。因此我们需要借助下面的方式来实现。printf(\033[%d;%dH, x, y) 这样一个东西来实现类似这个功能这里的 \033 是 8 进制 esc 的转义。我们通过 \033[%d;%dH我们这里面的 %d 一个代表 x 轴一个代表 y 轴y 和 x 的值就是打印的位置有一点需要注意这个坐标系与正常的坐标系有所不同。我这里用一幅图来表示( \033[%d;%dH, x, y) 这里的意思是把光标移动到横坐标为 x纵坐标为 y 的这个地方我们所要打印的东西要放在 H 后面比如 printf(\033[%d;%dH▶, x, y);所以墙体的设置估计能猜出来了设置一个墙体的长宽通过上述那样移动光标在边缘处进行打印墙体代码的实现如下void draw_edge(int row, int col) { int x 1, y 1; /* 循环只检查行号 x列的处理交给循环体内 */ while (x row y col) { if (x 1 || x row) { /* 第一行 或 最后一行画一整行 */ if (x 1) printf(\033[%d;%dH▄, x, y); /* 顶边 */ else printf(\033[%d;%dH▀, x, y); /* 底边 */ y; if (y col) { /* 这一行画完了去下一行 */ x; y 1; continue; } } else { /* 中间行只画左右两个点 */ printf(\033[%d;%dH█, x, 1); printf(\033[%d;%dH█, x, col); x; } } }这里或许和很多人想的不一样为什么这里面的 printf 里面打印的值怎么不一样呢这是因为 Linux 终端里面的长宽比是 1:2就是竖着的字符间隔是横着的两倍类似 * * 与 ** 的区别所以为了美观我采用了多种字符这个比例和字体等等有关系每个人的也许都不太一样但大致逻辑理解就可以。2. 蛇节点的打印在我们了解了墙体的打印我们大致猜出来了蛇也是这样打印出来的但不同点是我们的蛇是会动的所以我们用结构体来存储它里面包含了 xy 值和它的移动方向和指向下一个节点的指针。typedef struct snakenode { int x,y; int dir; struct snakenode *next; }sn;熟悉链表的人已经差不多猜出来了我们的蛇就是一个链表通过 printf 把所有节点打印在终端上就形成了一个蛇这个蛇的实现包括节点的创建赋值等等这里我只把打印的函数实现放出来。void draw_snake(sn *head) { sn *p head; while (p ! NULL) { if (p head) { /* 第一个节点是蛇头按方向画对应三角 */ if (head-dir right) printf(\033[%d;%dH▶, p-x, p-y); else if (head-dir left) printf(\033[%d;%dH◀, p-x, p-y); else if (head-dir up) printf(\033[%d;%dH▲, p-x, p-y); else if (head-dir down) printf(\033[%d;%dH▼, p-x, p-y); } else { /* 蛇身画 * */ printf(\033[%d;%dH*, p-x, p-y); } p p-next; } }具体对蛇节点的初始化和创建我就不一一讲解了。3. 蛇节点的移动通过 while 实现不断的移动如果我们蛇节点只是单纯的打印一次的话我们蛇节点和普通的打印就没什么区别了还是静态的我们要实现蛇的动态的话我们就要对蛇节点里面的 xy 值进行改变和对蛇的重新打印来达到蛇节点的移动。但是这里有一个疑问我们中途对这个 xy 值进行改变最后打印还不是最后一次的 xy 值所以这里我们需要 fflush 来对缓冲区进行刷新达到实时输出的效果。我们的蛇是移动的所以我们会输入 wasd 来操控蛇的移动其实就是更改 xy 值但这里有一个疑点如果我们是通过 getchar 来读取我们的输入此时的蛇就会卡在这里不动必须等你输入了才动这样就是一个遥控蛇了而不是贪吃蛇这个行为我们叫做阻塞要实现非阻塞也就是我们输入或不输入的时候这个蛇始终保持移动而非停下我们需要借助一个函数 select这个函数大家自行了解差不多就是 select 读到缓冲区有东西它就返回值大于 0并不会阻塞运动。非阻塞的实现如下int look_move(sn *head) { struct timeval tv; tv.tv_sec 0; tv.tv_usec 0; /* 超时 0 微秒 不等待 */ fd_set rfsd; FD_ZERO(rfsd); FD_SET(STDIN_FILENO, rfsd); /* 把标准输入键盘加进监视名单 */ if (select(1, rfsd, NULL, NULL, tv) 0) { /* 有输入 */ char ch; read(STDIN_FILENO, ch, 1); /* 读出一个字符 */ return ch; } return -1; /* 无输入 */ }这里有很多陌生的结构体这个是系统调用头文件自带的很简单大家去了解一下。这里我们解决了阻塞问题具体蛇的移动如下void node_move(sn *head, int dir) { int pre_x1 head-x, pre_y1 head-y, pre_dir1 head-dir; int pre_x2, pre_y2, pre_dir2; /* 1. 蛇头前进一格禁止 180° 掉头 */ if (dir up amp;amp; head-gt;dir ! down) { head-gt;x head-gt;x - 1; head-gt;dir up; } else if (dir down amp;amp; head-gt;dir ! up) { head-gt;x head-gt;x 1; head-gt;dir down; } else if (dir left amp;amp; head-gt;dir ! right) { head-gt;y head-gt;y - 1; head-gt;dir left; } else if (dir right amp;amp; head-gt;dir ! left) { head-gt;y head-gt;y 1; head-gt;dir right; } else { /* 掉头被禁止继续沿原方向走 */ if (head-gt;dir up) head-gt;x head-gt;x - 1; else if (head-gt;dir down) head-gt;x head-gt;x 1; else if (head-gt;dir left) head-gt;y head-gt;y - 1; else if (head-gt;dir right) head-gt;y head-gt;y 1; } /* 2. 身体依次跟随每个节点取前驱的旧位置 */ sn *cur head-gt;next; while (cur ! NULL) { pre_x2 cur-gt;x; pre_y2 cur-gt;y; pre_dir2 cur-gt;dir; /* 存自己的旧值 */ cur-gt;x pre_x1; cur-gt;y pre_y1; cur-gt;dir pre_dir1; /* 取前驱旧值 */ pre_x1 pre_x2; pre_y1 pre_y2; pre_dir1 pre_dir2; /* 接力给下一个 */ cur cur-gt;next; } }/* 2. 非阻塞读键更新方向或触发功能 */ ch look_move(head); if (ch w) node_move(head, up); else if (ch a) node_move(head, left); else if (ch s) node_move(head, down); else if (ch d) node_move(head, right); else if (ch p) time_p(); /* 暂停 */ else if (ch q) break; /* 退出 */ else node_move(head, head-dir); /* 无按键继续走 */这里就是蛇的移动功能的实现。4. 食物生成我们的食物是随机生成的我们可以采用 srand 随机生成一个种子配合 rand 达到随机因为我们的食物并不会马上被吃会长期存在所以我推荐用结构体来存储但要注意的一个点是食物的生成要避开几个位置一个是墙体内部一个是蛇身上不然就得重新生成代码如下struct food_place *rand_food(int x, int y) { int food_x rand() % x; if (food_x 0 || food_x 1) food_x 2; /* 避开顶/左边界 */ int food_y rand() % y; if (food_y 0 || food_y 1) food_y 2; /* 避开底/右边界 */ f_p *f malloc(sizeof(f_p)); if (f NULL) { printf(内存分配错误\n); return NULL; } f-x food_x; f-y food_y; return f; }if (food NULL) { food rand_food(weight, lenth); /* 检查食物是否落在蛇身上是就重新生成 */ sn *head_c head; int flag 0; while (head_c ! NULL) { if (head_c-x food-x head_c-y food-y) { flag 1; break; } head_c head_c-next; } if (flag) { free(food); food NULL; continue; } }5. 墙体的生成墙体的生成其实也类似于食物要避开蛇身和边缘墙体同时墙体生成后是要长期存在的所以我们采用的是和蛇一样的链表设计其实可以把墙体看作第二个蛇我这里由于是随机墙体并且是随机单个墙体和十字墙体代码实现比较复杂就不作展示。6. 碰撞检测6.1 蛇自己的碰撞检测我们知道贪吃蛇碰撞到自己的蛇身时会死亡所以要设计这个碰撞检测就是头节点每次移动完进行一个函数的碰撞检测如果蛇头的 xy 值等于它的某个节点的 xy 值直接结束游戏。代码如下int node_over(sn *head) { sn *n head-next; while (n ! NULL) { if (head-x n-x head-y n-y) { gameover(); return -1; } n n-next; } return 1; }6.2 边缘墙体检测这个是比较简单的只要 y 或 x 大于等于墙体的值或小于等于都直接 gameover。代码如下if (head-x 1 || head-x weight || head-y 1 || head-y lenth) { gameover(); return; }6.3 游戏生成的墙体碰撞检测原理类似就是不能让蛇头 xy 值等于墙链上任何一点的 xy 值否则直接 gameover。int all_node_hit(sn *head, wd *w_head) { sn *n head; wd *cur w_head; while (cur ! NULL) { if (cur-x n-x cur-y n-y) { gameover(); return -1; } cur cur-next; } return 1; }6.4 蛇吃食物的碰撞检测蛇吃食物也可以看作碰撞但是不同的是并不 gameover而是增加节点和释放食物。代码如下if (head-x food-x head-y food-y) { /* 在蛇尾追加一节方向决定坐标整体前移会自动修正 */ if ((*tail)-dir up) create_node(head_copy, tail, (*tail)-x 1, (*tail)-y); else if ((*tail)-dir down) create_node(head_copy, tail, (*tail)-x - 1, (*tail)-y); else if ((*tail)-dir left) create_node(head_copy, tail, (*tail)-x, (*tail)-y 1); else if ((*tail)-dir right) create_node(head_copy, tail, (*tail)-x, (*tail)-y - 1); free(food); food NULL; }我只复制了部分剩下部分的是增加游戏难度用的。7. gameover 和暂停功能积分功能以及难度增加功能这些属于简单的功能属于锦上添花的功能的实现很简单这里不多赘述。会出现的问题和解决方案我们知道我们是通过移动光标来打印的但这个光标是会一闪一闪的可能会把我们的节点给遮住导致观感极差所以我们需要在游戏开始前关闭这个光标在结束游戏后打开。具体实现如下也可以自己实现/* 终端模式切换 */ /* enable_raw_mode切换到原始模式 效果按键立刻到达程序不用等回车、按键不回显、隐藏光标 / void enable_raw_mode(void) { struct termios raw; tcgetattr(STDIN_FILENO, g_orig); / 1. 读出当前配置并保存 / raw g_orig; raw.c_lflag ~(ICANON | ECHO); / 2. 关规范模式和回显 / tcsetattr(STDIN_FILENO, TCSANOW, raw); / 3. 写回新配置 / printf(\033[?25l); / 隐藏光标避免光标方块盖住画面 */ fflush(stdout); } /* disable_raw_mode恢复原始终端配置 必须在游戏结束时调用否则终端不回显、光标不显示 / void disable_raw_mode(void) { tcsetattr(STDIN_FILENO, TCSANOW, g_orig); / 恢复之前保存的配置 / printf(\033[?25h); / 恢复光标显示 */ fflush(stdout); }还有记得游戏结束后记得释放节点避免出现内存泄漏。
返回列表