当前位置: 首页 > news >正文

基于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. 蛇节点的打印

在我们了解了墙体的打印,我们大致猜出来了,蛇也是这样打印出来的,但不同点是我们的蛇是会动的,所以我们用结构体来存储它,里面包含了 x,y 值,和它的移动方向,和指向下一个节点的指针。

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 实现不断的移动)

如果我们蛇节点只是单纯的打印一次的话,我们蛇节点和普通的打印就没什么区别了,还是静态的,我们要实现蛇的动态的话,我们就要对蛇节点里面的 x,y 值进行改变,和对蛇的重新打印,来达到蛇节点的移动。但是这里有一个疑问,我们中途对这个 x,y 值进行改变,最后打印还不是最后一次的 x,y 值,所以这里我们需要 fflush 来对缓冲区进行刷新,达到实时输出的效果。

我们的蛇是移动的,所以我们会输入 wasd 来操控蛇的移动,其实就是更改 x,y 值,但这里有一个疑点,如果我们是通过 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 蛇自己的碰撞检测

我们知道贪吃蛇碰撞到自己的蛇身时会死亡,所以要设计这个碰撞检测,就是头节点每次移动完,进行一个函数的碰撞检测,如果蛇头的 x,y 值等于它的某个节点的 x,y 值,直接结束游戏。

代码如下:

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 游戏生成的墙体碰撞检测

原理类似,就是不能让蛇头 x,y 值等于墙链上任何一点的 x,y 值,否则直接 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); }

还有记得游戏结束后记得释放节点,避免出现内存泄漏。

http://www.cnnetsun.cn/news/4025276.html

相关文章:

  • 【论文翻译】SCNET: SPARSE COMPRESSION NETWORK FOR MUSIC SOURCE SEPARATION
  • 华硕笔记本控制工具G-Helper打不开?完整启动排查手册:5招让双击重新有反应
  • Socket编程:客户端与服务器通信全解析(网络编程)
  • 如何为Cocos Creator +微信小游戏项目建立一套可长期执行的性能治理体系?
  • SAP Task Gateway 扩展实战,如何为统一任务入口增加新的 Provider
  • Win11Debloat实测:半小时卸载预装软件、关闭遥测,新电脑终于不卡了
  • 性能优化:连接池、缓存、批量处理
  • 开源的报文分析平台:12 个规则库全接引擎,附在线体验
  • 从一句主题到一支成片:Pixelle-Video 零门槛全自动短视频引擎
  • Prompts原语:标准化提示词模板
  • 正则分组/php5版本下preg_replace /e模式下的代码执行
  • Kimi LeetCode 3906. 统计网格路径中好整数的数目 Rust实现
  • maxGraph零基础入门:纯客户端JavaScript图表库,零成本5分钟画出可交互流程图
  • Portainer:Docker可视化Web管理面板的新手首选方案
  • 华硕笔记本控制权争夺战:G-Helper一天上手,性能、散热与续航全面解放
  • Dism++完整上手指南:免费清理系统垃圾、修复更新失败的终极优化工具,5分钟就能见效
  • 【Proteus仿真设计】基于stm32单片机的智能家居系统设计
  • Dify 企业级实验(03):事件驱动流水线——Webhook 与定时触发如何组成异步处理链?
  • 一条命令给 Win11 系统优化瘦身,Win11Debloat 把预装软件和广告一次清干净
  • Windows APK安装器完全指南:免模拟器在电脑上安装安卓应用
  • SOLIDWORKS 正版软件价格全解析:商业版、教育版、科研版报价指南
  • 相机缓冲数据三种数据格式(数组、指针new、vector)
  • 贵州微信网站建设全流程解析:中小企业如何利用私域流量实现低成本高增长
  • 预算不够不用全套打包!生产自动化与 AI 管理支持分开采购、分步落地
  • 永嘉网站建设几年才见效?资深从业者揭秘低成本高效获客真相
  • 深入解析南海网站建设报价背后的逻辑与行业内幕揭秘
  • 揭秘城乡规划建设网站背后的真相:为什么它不仅是信息枢纽更是城市发展的灵魂指南
  • 范县网站建设企业为何需要专业的数字化升级之路?本地老板必看攻略
  • 南阳网站建设价格揭秘:为什么有人几百元有人几万元?
  • 南京百度网站建设多少钱?深度解析中小企业如何通过南京百度网站建设实现低成本高效率获客与品牌升级