贪吃蛇/链表实现(C/C++)
本篇使用C语言实现贪吃蛇小游戏,我们将其分为了三个大部分,第一个部分游戏开始GameStart,游戏运行GameRun,以及游戏结束GameRun。对于整体游戏主要思想是基于链表实现,但若仅仅只有C语言的知识还不够,我们还需要学习控制台的一些相关操作,结合实现贪吃蛇游戏,所以我们先介绍了一些有关Win32 API的知识。
以下为整体实现的思路,以及对应的代码,在文章的末尾也给出了整体代码以及对应的测试,有需要的读者可以根据目录直接跳到对应的位置。
另外,这只是一个基础版本的,读者还可在此基础上进行升级,如:
1.将地图的进行升级,不在仅仅只是一个方框,加大难度;
2.写一个文件操作,记录历史最高得分记录;
3.将运行出来的效果加上各种动画,等等。
1.Win32 API介绍
1.1 Win32 API
Windows这个多作业系统除了协调应用程序的执行、内存分配、管理资源之外,同时也作为一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以将其称之为:Application Programming Interface,简称API函数,WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。
1.2 控制台程序
平时通过VS2022,或者Windows命令提示符(cmd)运行起来的黑框程序就是控制台程序;
我们可以通过使用cmd命令来控制控制台窗口的长宽:如下,控制台窗口的大小为30行、100列。
mode con cols=100 lines=30
如上图所示,可以通过cmd命令来设置控制台窗口的长宽。
还可以使用命令设置控制台窗口的名字:
如果要将以上的控制台窗口执行的命令调用在C语言中,我们可以使用C语言函数中的system来执行,如下:
#include <stdio.h>
#include <Windows.h>int main() {system("mode con cols=100 lines=30");system("title Snake");getchar();return 0;
}
如果使用上述的C语言命令无法实现该情况,请先将C语言中的默认终端应用程序 改为:Windows控制台主机,如下:
(对VS弹出的控制终端,鼠标右键,点击属性,点击终端,找到对应的位置修改即可)。
1.3 控制台屏幕上的坐标COORD
COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕上的坐标。
使用该结构体需要头文件 <Windows.h>。
typedef struct _COORD {SHORT X;SHORT Y;
}COORD, *PCOORD;COORD pos={ 10, 15 }; //给坐标赋值
通过以上的坐标结构体,我们可以实现控制一个字符在控制台屏幕的坐标,便于我们在控制台不同的位置打印该字符。
1.4 GetStdHandle
GetStdHandle 是一个Windows API函数。用于从一个特定的标准设备(标准输入,标准输出或者标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
即若我们我们需要对某一个打开的控制台程序进行操作,我们就必须通过GetStdHandle这个函数得到对应的控制台标识,获取权限,才可以对中国控制台程序进行操作。
HANDLE hOutput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
1.5 GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性信息
BOOL WINAPI GetConsoleCursorInfo{HANDLE hConsoleOutput;PCONSOLE_CURSOR_INFO lpConsoleCursorInfo;
};//PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构体的指针,
//该结构体接收有关主机游标的信息//CONSOLE_CURSOR_INFO 结构体
typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
}CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
对于 CONSOLE_CURSOR_INFO 结构体来说:
对应其中的dwSize:有光标填充的字符单元格百分比。此值介于1到100之间。光标的外观会变化,范围从填充单元格到单元格底部(消失),通常取值0、25、50、75、100.
bVisible:游标的可见性,若光标可见,则此成员为TURE,反之为FALSE。
对于SetConsoleCursorInfo函数来说,就是将设置的CONSOLE_CURSOR_INFO类型变量进行设置,设置指定控制台缓冲区的光标的大小和可见性。如下操作,我们可以将控制台缓冲区的光标隐藏。
int main()
{COORD pos = { 40, 10 };CONSOLE_CURSOR_INFO cursor_info = {0};HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);GetConsoleCursorInfo(handle, &cursor_info);cursor_info.dwSize = 100;cursor_info.bVisible = false;//隐藏SetConsoleCursorInfo(handle, &cursor_info);getchar(); //用于将程序暂停在此,要不然会一下运行结束return 0;
}
不仅仅可以将光标隐藏,还可以将光标的大小设置为任意位置,读者可以自行操作。
1.6 SetConsoleCursorPosition
该函数设置控制台屏幕缓冲区中光标的位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition 函数将光标位置设置到指定位置。所以该函数可以使我们在指定位置打印出我们想要的结果。如下:
int main()
{COORD pos = { 10, 5 };HANDLE hOutput = NULL;hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);printf("haha\n");return 0;
}
使用如上函数,我们就可以在指定的位置打印出我们想要的数据。 为了方便,我们可以封装一个设置光标位置的函数:
void SetPos(short x, short y) {COORD pos = { x,y };HANDLE hOutput = NULL;//获取标准输出的句柄hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置光标位置SetConsoleCursorPosition(hOutput, pos);
}
1.7 GetAsyncKeyState
该函数的作用为:可以将键盘上的每一个健的虚拟键值传递给函数,函数通过返回值,来区分按键的状态。
该函数的返回值为short类型,在上一次调用GetAsyncKeyState函数之后,,如果返回的16为的short类型数据中,最高位为1,说明按键的状态是按下,若最高层是0,说明按键的状态是抬起;如果最低位置被置为1,说明该按键被按过;若为0,则没有被按过。
所以我们可以定义一个宏,来判断虚拟按键VK,是否被按过:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
该宏就可以将我们的按键信息传达给程序,便于我们操作贪吃蛇。
2.贪吃蛇程序的设计
前面的一些基础知识介绍完之后,我们就需要开始进行正式的贪吃蛇程序的设计了,首先贪吃蛇游戏的板块一共分为三个部分,其中分别为游戏开始前(GameStart)、游戏运行时(GameRun)、游戏结束之后(GameEnd)三个大模块,在这三个大模块中也包含许多小模块,我将以层次图给出,如下:
上图则为我们大概将要实现的步骤,所以将按照当前思路进行讲解。
# C语言库函数本地化
在进行代码编写前,我们需要先对程序进行本地化,因为在C语言中,并不是适用于所有地区的字符,所以如果我们需要使用C语言库中允许打印之外的符号,那么我们需要先对当前的C语言环境进行本地化。
<locale.h>本地化:<locale.h>提供的函数用于控制C标准库对于不同地区会产生不一样的行为的部分。在标准可以中,依赖于地区部分一共有:数字量的格式、货币量的格式、字符集、日期和时间的表示形式。
类项:通过修改地区,程序可以改变它的行为来适应世界的不同区域,但是地区的改变可能将会影响库的许多部分,其中一部分可能是我们并不希望修改的,所以C语言中支持针对不同的类项进行修改,如这些宏:LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC、LC_TIME、LC_ALL—针对所有类项修改。
对于以上的宏的使用,我们需要使用setlocale函数,对于setlocale该函数的使用,如下
//执行C标准库环境
setlocale(LC_ALL, "C");//将C标准库本地化
setlocale(LC_ALL, " ");
2.1 贪吃蛇数据结构
蛇身:首先对于贪吃蛇结点的设计,因为贪吃蛇处于一个移动的形式,所以我们在贪吃蛇结构体的设置中需要定义出贪吃蛇的坐标,一级下一个结点的指针。
整个游戏:然后就是对整个游戏维护的结构体,对于整个游戏维护的结构体中,首先需要对整个蛇进行维护,所以设置一个结构体指针指向蛇头;然后就是对食物的维护,食物同样采用蛇身的结构体;然后是总分和每个食物的分数;还有每一次程序休眠的时间(休眠时间决定贪吃蛇的前进的速度),以及当前蛇头的方向,和蛇的状态。
代码如下:
//枚举当前的蛇的方向
enum DIRECTION { UP, DOWN, LEFT, RIGHT };
//当前蛇的状态,OK=正常允许,ESC=主动退出,KILL_BY_WALL=被墙杀死,KILL_BY_SELF=咬到自己死亡
enum STATUS { OK, ESC, KILL_BY_WALL, KILL_BY_SELF };#define WALL L'□' //打印墙
#define BODY L'●' //打印蛇身
#define FOOD L'★' //打印食物
#define POS_X 24 //蛇的初始位置横坐标
#define POS_y 5 //蛇的初始位置纵坐标//读取键盘信息的宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)typedef struct SnakeNode {int x; //x轴坐标int y; //y轴坐标struct SnakeNode* next;
}SnakeNode;typedef struct Snake {SnakeNode* SnakeHead; //蛇头,用来维护整个蛇身SnakeNode* pFood; //食物,维护食物的指针int score; //总分,用来计算当前得分int foodWeight; //食物分数,默认食物的分数int SleepTime; //每一步的休眠时间enum DIRECTION dir; //蛇头的方向,默认向右enum STATUS status; //蛇的状态,当前蛇的状态
}Snake;
2.2 GameStart
2.2.1 欢迎界面
在打印欢迎界面之前,我们需要将控制台的窗口大小设置为固定大小,并且设置窗口名称,同时获取标准输出句柄,然后将光标隐藏起来,具体操作如下:
void GameStart(Snake* ps) {//先设置当前窗口大小system("mode con cols=100 lines=30");//设置窗口名system("title Snake");//获取标准输出的句柄HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//隐藏当前光标CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态//打印欢迎界面WelcomeToGame();//打印地图CreateMap();//初始化蛇InitSnake(ps);//创建一个食物CreateFood(ps);
}
该操作都是通过以上对Win32 API介绍的知识。
然后就是对欢迎界面的设置,对于欢迎界面,我们要将其设置为如下形式:其中欢迎在中间位置,按任意位置继续在界面下端。第二个界面为操作方式讲解,也是位于中间位置。
设置以上形式的打印,我们只需要调用以上设置位置的函数,然后在打印即可,当然还涉及以下暂停函数和清屏函数,如下:
void SetPos(short x, short y) {COORD pos = { x,y };HANDLE hOutput = NULL;//获取标准输出句柄hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上的光标的位置SetConsoleCursorPosition(hOutput, pos);
}void WelcomeToGame() {SetPos(40, 15); //大概设置在中间位置,具体可看自己的偏好printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25); //在下端设置“请按任意位置继续. . .”system("pause");system("cls"); //清屏SetPos(25, 12); printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(25, 13);printf("加速将得到更高的分数.\n");SetPos(40,25);system("pause");system("cls");
}
2.2.2 打印地图
对于地图的设计,我们大概设计出下列的地图,大致呈现一个正方形,但是在控制台缓冲区的形式如下,对于一个光标的位置,高度大概是宽度的两倍,所以我们设计出来的宽必须是列的两倍,我们使用打印墙的字符为宽字符(本地字符),刚好占两个字符位,所以我们生成的宽度必须位偶数。
我们将分别打印墙的上半部分、下半部分、左边和右边,如下:
void CreateMap() {int i = 0;SetPos(0, 0);//打印上面for (i = 0; i < 58; i+=2) {wprintf(L"%lc", WALL);}//打印下面SetPos(0, 26);for (i = 0; i < 58; i += 2) {wprintf(L"%lc", WALL);}//打印左边for (i = 1; i < 26; i++) {SetPos(0, i);wprintf(L"%lc", WALL);}//打印右边for (i = 1; i < 26; i++) {SetPos(56, i);wprintf(L"%lc", WALL);}
}
2.2.3 初始化蛇
现在我们需要将蛇初始化,对于蛇的初始化,我们默认将蛇的身体设置为5个结点,每一个结点我们采用链表头插的方式进行插入,从第二十四列第5行开始向右头插。在设置完蛇身之后,我们将蛇给打印出来,然后将蛇的其他信息初始化:食物指针置为NULL,默认方向设置为RIGHT,分数为0,每个食物的分数为10分,默认状态为OK,休眠时间为200ms,如下:
注:对于蛇身结点的设置,对于x坐标必须为偶数,因为对于以上的墙,假若我们撞左墙或者右墙时刚好横坐标为偶数,便于我们识别撞墙。
void InitSnake(Snake* ps) {SnakeNode* cur = NULL;for (int i = 0; i < 5; i++) {cur = (SnakeNode*)malloc(sizeof(SnakeNode));if (cur == NULL) {perror("InitSnake malloc:");exit(1);}cur->x = POS_X + 2 * i;cur->y = POS_y;cur->next = NULL;if (ps->SnakeHead == NULL) {ps->SnakeHead = cur;}else {cur->next = ps->SnakeHead;ps->SnakeHead = cur;}}//打印蛇身cur = ps->SnakeHead;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//将其他信息完善ps->dir = RIGHT;ps->foodWeight = 10;ps->pFood = NULL;ps->score = 0;ps->SleepTime = 200;ps->status = OK;}
2.2.4 创建食物
食物创建的结点按照蛇身结点创建,其中仍然必须遵守横坐标为偶数,因为蛇的横坐标为偶数, 吃到食物的时候能更好接上。所以对于食物的生成我们使用rand函数进行生成,但是对于食物的生成有两点需要注意的:
1.食物的随机生成不能生成在墙外;
2.食物的随机生成不能生成在蛇的身上。
最后我们需要将食物打印在屏幕上,实现的代码如下:
void CreateFood(Snake* ps) {SnakeNode* cur = ps->SnakeHead;int x = 0;int y = 0;do {x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);while (cur) {if (cur->x == x && cur->y == y) {do {x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);}cur = cur->next;}//创建食物SnakeNode* Food = (SnakeNode*)malloc(sizeof(SnakeNode));if (Food == NULL) {perror("CreateFood malloc:");exit(1);}Food->x = x;Food->y = y;Food->next = NULL;ps->pFood = Food;SetPos(x, y);wprintf(L"%lc", FOOD);
}
实现完以上步骤之后,我们打印出来的界面就为:
2.3 GameRun
对于GameRun函数的基本框架如下:
void GameRun(Snake* ps) {ps->status = OK;//打印右侧帮助信息PrintHelpInfo();do {GainKeyInfo(ps);if (ps->status == ESC) {break;}SnakeMove(ps);Sleep(ps->SleepTime);} while (ps->status == OK);
}
2.3.1 打印帮助信息
打印出来的帮助信息其实就为一些游戏规则和游戏操作,还是按照以上类似的操作进行写代码:
void PrintHelpInfo() {SetPos(64, 15);printf("不能穿墙,不能咬到自己\n");SetPos(64, 16);printf("用↑.↓.←.→分别控制蛇的移动.");SetPos(64, 17);printf("F3:为加速,F4:为减速");SetPos(64, 18);printf("ESC: 退出游戏、space:暂停游戏.");SetPos(64, 20);printf("版权@桀桀桀桀桀桀");
}
2.3.2 获取键盘信息
从键盘获取信息是非常重要的一部分,其中涉及到我们之前定义的宏,在接收到键盘中上、下、左、右的信息时,我们还需要判断当前位置的情况,也就是说,蛇向下移动时不能向上进行掉头,向左移动时不能向右进行掉头……。
另外,因为是频繁接收到信息,得分和每一个食物所占分数是变化的。所以我们还需要在这一部分把当前得分给打印出来,以及当前每一个食物所占的分数,速度越快,获取的分数越多,速度越慢,获取的分数越少。
当按下空格健的时候,我们需要将游戏暂停,所以我们需要设计一个死循环,当再一次按下空格键的时候,跳出循环,每一次循环随眠100ms或者200ms。
获取到F3和F4时,我们需要将对应的休眠时间和每个食物的分数进行调整,每一次加速,休眠时间减少30ms,食物分数增加2,每一次减速,休眠时间增加30ms,食物分数减少2,休眠时间最少为80ms,食物分数最少为2分。
具体代码如下:
//空格键暂停
void pause() {while (1) {Sleep(100);if (KEY_PRESS(VK_SPACE)) {break;}}
}void GainKeyInfo(Snake* ps) {SetPos(64, 10); printf("得分:%5d", ps->score);SetPos(64, 11);printf("每个食物得分:%2d", ps->foodWeight);if (KEY_PRESS(VK_UP) && ps->dir != DOWN) {ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP) {ps->dir = DOWN;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {ps->dir = RIGHT;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {ps->dir = LEFT;}else if (KEY_PRESS(VK_SPACE)) {pause();}else if (KEY_PRESS(VK_ESCAPE)) {ps->status = ESC;}else if (KEY_PRESS(VK_F3)) {if (ps->SleepTime > 80) {ps->SleepTime -= 30;ps->foodWeight += 2;}}else if (KEY_PRESS(VK_F4)) {if (ps->foodWeight > 2) {ps->SleepTime += 30;ps->foodWeight -= 2;}}
}
2.3.3 蛇的移动
蛇的移动部分属于整个贪吃蛇中最为关键也是最为复杂的一个部分。
首先我们需要考虑在当前情况下,如果进行转向刚好吃到食物的情况,以及转向之后,不是食物的情况,以及在贪吃蛇每走一步都需要判断是否撞墙或者是否咬到自己。
转向遇到食物:创建一个结点在蛇要走的下一个位置,若这个结点刚好与食物重叠,那么将创建的结点头插到蛇中,然后将食物的空间删除。然后将整条蛇打印一遍。最后在创建出一个食物。
转向没有遇到食物:创建一个结点在蛇要走的下一个位置,下一个位置不是食物,将这个结点插入到蛇中,然后将蛇打印出来,但是最后一个结点不能打印,最后一个结点的位置打印两个空格,然后将最后一个结点释放掉。
撞墙检测:每走完一步都需要将蛇的头结点与墙的位置进行判断,如果出现在墙的位置,那么就撞墙了,将蛇的状态改为KILL_BY_WALL。
咬到自己检测:每走完一步,都需要将蛇的头结点与身上的各个结点进行对比,若出现横纵坐标都相等的情况,那么说明已经咬到自己了,将蛇的状态改为KILL_BY_SELF。
实现的代码如下:
//下一个结点是食物
int NextIsFood(SnakeNode* nextNode, Snake* ps) {if (nextNode->x == ps->pFood->x && nextNode->y == ps->pFood->y) {return 1;}else {return 0;}
}//下一个结点是食物,吃掉食物
void EatFood(SnakeNode* nextNode, Snake* ps) {nextNode->next = ps->SnakeHead;ps->SnakeHead = nextNode;SnakeNode* cur = ps->SnakeHead;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->score += ps->foodWeight;free(ps->pFood);//创建新食物CreateFood(ps);
}//下一个结点不是食物,不吃食物
void NotEatFood(SnakeNode* nextNode, Snake* ps) {nextNode->next = ps->SnakeHead;ps->SnakeHead = nextNode;SnakeNode* cur = ps->SnakeHead;while (cur->next->next) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}SetPos(cur->next->x, cur->next->y);printf(" ");free(cur->next);cur->next = NULL;
}//检测是否被墙杀死
void KillByWall(Snake* ps) {if (ps->SnakeHead->x == 0 ||ps->SnakeHead->x == 56 ||ps->SnakeHead->y == 0 ||ps->SnakeHead->y == 26) {ps->status = KILL_BY_WALL;}
}//检测是否咬到自己
void KillBySelf(Snake* ps) {SnakeNode* cur = ps->SnakeHead->next;while (cur) {if (ps->SnakeHead->x == cur->x && ps->SnakeHead->y == cur->y) {ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}void SnakeMove(Snake* ps) {//创建下一个结点SnakeNode* nextNode = (SnakeNode*)malloc(sizeof(SnakeNode));if (nextNode == NULL) {perror("SnakeMove malloc:");exit(1);}//确定下一个节点的位置switch (ps->dir){case UP:{nextNode->x = ps->SnakeHead->x;nextNode->y = ps->SnakeHead->y - 1;}break;case DOWN: {nextNode->x = ps->SnakeHead->x;nextNode->y = ps->SnakeHead->y + 1;}break;case RIGHT: {nextNode->x = ps->SnakeHead->x + 2;nextNode->y = ps->SnakeHead->y;}break;case LEFT: {nextNode->x = ps->SnakeHead->x - 2;nextNode->y = ps->SnakeHead->y;}break;}if (NextIsFood(nextNode, ps)) {EatFood(nextNode, ps);}else {NotEatFood(nextNode,ps);}KillByWall(ps);KillBySelf(ps);
}void GameRun(Snake* ps) {ps->status = OK;//打印右侧帮助信息PrintHelpInfo();do {GainKeyInfo(ps);if (ps->status == ESC) {break;}SnakeMove(ps);Sleep(ps->SleepTime);} while (ps->status == OK);
}
实现完以上的步骤之后,贪吃蛇代码就已经可以运行起来了,但是我们还是需要对后续进行处理。
2.4 GameEnd
接下来就是对游戏的善后工作了,我们需要检测游戏是因为什么而结束的,然后在屏幕中间打印出结束的原因,最后我们将蛇的各个结点进行释放。
void GameEnd(Snake* ps) {SnakeNode* cur = ps->SnakeHead;SetPos(15, 12);switch (ps->status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_SELF:printf("很遗憾,你咬到了你自己\n");break;case KILL_BY_WALL:printf("很遗憾,你撞到墙了\n");break;}while (cur) {SnakeNode* next = cur->next;free(cur);cur = next;}free(ps->pFood);ps->pFood = NULL;ps->SnakeHead = NULL;ps = NULL;
}
3.总代码
3.1 snake.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <stdbool.h>
#include <locale.h>//枚举当前的蛇的方向
enum DIRECTION { UP, DOWN, LEFT, RIGHT };
//当前蛇的状态,OK=正常允许,ESC=主动退出,KILL_BY_WALL=被墙杀死,KILL_BY_SELF=咬到自己死亡
enum STATUS { OK, ESC, KILL_BY_WALL, KILL_BY_SELF };#define WALL L'□' //打印墙
#define BODY L'●' //打印蛇身
#define FOOD L'★' //打印食物
#define POS_X 24 //蛇的初始位置横坐标
#define POS_y 5 //蛇的初始位置纵坐标//读取键盘信息的宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)typedef struct SnakeNode {int x; //x轴坐标int y; //y轴坐标struct SnakeNode* next;
}SnakeNode;typedef struct Snake {SnakeNode* SnakeHead; //蛇头,用来维护整个蛇身SnakeNode* pFood; //食物,维护食物的指针int score; //总分,用来计算当前得分int foodWeight; //食物分数,默认食物的分数int SleepTime; //每一步的休眠时间enum DIRECTION dir; //蛇头的方向,默认向右enum STATUS status; //蛇的状态,当前蛇的状态
}Snake;//设置光标位置
void SetPos(short x, short y);//游戏开始
void GameStart(Snake* ps);//欢迎界面
void WelcomeToGame();//打印地图
void CreateMap();//初始化蛇
void InitSnake(Snake* ps);//创建食物
void CreateFood(Snake* ps);//运行游戏
void GameRun(Snake* ps);//打印右侧帮助信息
void PrintHelpInfo();//获取键盘信息
void GainKeyInfo(Snake* ps);//暂停游戏
void pause();//蛇移动
void SnakeMove(Snake* ps);//如果下一个位置就是食物
int NextIsFood(SnakeNode* nextNode, Snake* ps);//吃掉食物
void EatFood(SnakeNode* nextNode, Snake* ps);//不是食物
void NotEatFood(SnakeNode* nextNode,Snake* ps);//被墙杀死
void KillByWall(Snake* ps);//被自己杀死
void KillBySelf(Snake* ps);//游戏结束。处理
void GameEnd(Snake* ps);
3.2 snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"void SetPos(short x, short y) {COORD pos = { x,y };HANDLE hOutput = NULL;//获取标准输出句柄hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上的光标的位置SetConsoleCursorPosition(hOutput, pos);
}void WelcomeToGame() {SetPos(40, 15); //大概设置在中间位置,具体可看自己的偏好printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25); //在下端设置“请按任意位置继续. . .”system("pause");system("cls"); //清屏SetPos(25, 12); printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(25, 13);printf("加速将得到更高的分数.\n");SetPos(40,25);system("pause");system("cls");
}void CreateMap() {int i = 0;SetPos(0, 0);//打印上面for (i = 0; i < 58; i+=2) {wprintf(L"%lc", WALL);}//打印下面SetPos(0, 26);for (i = 0; i < 58; i += 2) {wprintf(L"%lc", WALL);}//打印左边for (i = 1; i < 26; i++) {SetPos(0, i);wprintf(L"%lc", WALL);}//打印右边for (i = 1; i < 26; i++) {SetPos(56, i);wprintf(L"%lc", WALL);}
}void InitSnake(Snake* ps) {SnakeNode* cur = NULL;for (int i = 0; i < 5; i++) {cur = (SnakeNode*)malloc(sizeof(SnakeNode));if (cur == NULL) {perror("InitSnake malloc:");exit(1);}cur->x = POS_X + 2 * i;cur->y = POS_y;cur->next = NULL;if (ps->SnakeHead == NULL) {ps->SnakeHead = cur;}else {cur->next = ps->SnakeHead;ps->SnakeHead = cur;}}//打印蛇身cur = ps->SnakeHead;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//将其他信息完善ps->dir = RIGHT;ps->foodWeight = 10;ps->pFood = NULL;ps->score = 0;ps->SleepTime = 200;ps->status = OK;}void CreateFood(Snake* ps) {SnakeNode* cur = ps->SnakeHead;int x = 0;int y = 0;do {x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);while (cur) {if (cur->x == x && cur->y == y) {do {x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);}cur = cur->next;}//创建食物SnakeNode* Food = (SnakeNode*)malloc(sizeof(SnakeNode));if (Food == NULL) {perror("CreateFood malloc:");exit(1);}Food->x = x;Food->y = y;Food->next = NULL;ps->pFood = Food;SetPos(x, y);wprintf(L"%lc", FOOD);
}void GameStart(Snake* ps) {//先设置当前窗口大小system("mode con cols=100 lines=30");//设置窗口名system("title Snake");//获取标准输出的句柄HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//隐藏当前光标CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态//打印欢迎界面WelcomeToGame();//打印地图CreateMap();//初始化蛇InitSnake(ps);//创建一个食物CreateFood(ps);
}void PrintHelpInfo() {SetPos(64, 15);printf("不能穿墙,不能咬到自己\n");SetPos(64, 16);printf("用↑.↓.←.→分别控制蛇的移动.");SetPos(64, 17);printf("F3:为加速,F4:为减速");SetPos(64, 18);printf("ESC: 退出游戏、space:暂停游戏.");SetPos(64, 20);printf("版权@桀桀桀桀桀桀");
}void GainKeyInfo(Snake* ps) {SetPos(64, 10);printf("得分:%5d", ps->score);SetPos(64, 11);printf("每个食物得分:%2d", ps->foodWeight);if (KEY_PRESS(VK_UP) && ps->dir != DOWN) {ps->dir = UP;}else if (KEY_PRESS(VK_DOWN) && ps->dir != UP) {ps->dir = DOWN;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {ps->dir = RIGHT;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {ps->dir = LEFT;}else if (KEY_PRESS(VK_SPACE)) {pause();}else if (KEY_PRESS(VK_ESCAPE)) {ps->status = ESC;}else if (KEY_PRESS(VK_F3)) {if (ps->SleepTime > 80) {ps->SleepTime -= 30;ps->foodWeight += 2;}}else if (KEY_PRESS(VK_F4)) {if (ps->foodWeight > 2) {ps->SleepTime += 30;ps->foodWeight -= 2;}}
}void pause() {while (1) {Sleep(100);if (KEY_PRESS(VK_SPACE)) {break;}}
}int NextIsFood(SnakeNode* nextNode, Snake* ps) {if (nextNode->x == ps->pFood->x && nextNode->y == ps->pFood->y) {return 1;}else {return 0;}
}void EatFood(SnakeNode* nextNode, Snake* ps) {nextNode->next = ps->SnakeHead;ps->SnakeHead = nextNode;SnakeNode* cur = ps->SnakeHead;while (cur) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->score += ps->foodWeight;free(ps->pFood);//创建新食物CreateFood(ps);
}void NotEatFood(SnakeNode* nextNode, Snake* ps) {nextNode->next = ps->SnakeHead;ps->SnakeHead = nextNode;SnakeNode* cur = ps->SnakeHead;while (cur->next->next) {SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}SetPos(cur->next->x, cur->next->y);printf(" ");free(cur->next);cur->next = NULL;
}void KillByWall(Snake* ps) {if (ps->SnakeHead->x == 0 ||ps->SnakeHead->x == 56 ||ps->SnakeHead->y == 0 ||ps->SnakeHead->y == 26) {ps->status = KILL_BY_WALL;}
}void KillBySelf(Snake* ps) {SnakeNode* cur = ps->SnakeHead->next;while (cur) {if (ps->SnakeHead->x == cur->x && ps->SnakeHead->y == cur->y) {ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}void SnakeMove(Snake* ps) {//创建下一个结点SnakeNode* nextNode = (SnakeNode*)malloc(sizeof(SnakeNode));if (nextNode == NULL) {perror("SnakeMove malloc:");exit(1);}//确定下一个节点的位置switch (ps->dir){case UP:{nextNode->x = ps->SnakeHead->x;nextNode->y = ps->SnakeHead->y - 1;}break;case DOWN: {nextNode->x = ps->SnakeHead->x;nextNode->y = ps->SnakeHead->y + 1;}break;case RIGHT: {nextNode->x = ps->SnakeHead->x + 2;nextNode->y = ps->SnakeHead->y;}break;case LEFT: {nextNode->x = ps->SnakeHead->x - 2;nextNode->y = ps->SnakeHead->y;}break;}if (NextIsFood(nextNode, ps)) {EatFood(nextNode, ps);}else {NotEatFood(nextNode,ps);}KillByWall(ps);KillBySelf(ps);
}void GameRun(Snake* ps) {ps->status = OK;//打印右侧帮助信息PrintHelpInfo();do {GainKeyInfo(ps);if (ps->status == ESC) {break;}SnakeMove(ps);Sleep(ps->SleepTime);} while (ps->status == OK);
}void GameEnd(Snake* ps) {SnakeNode* cur = ps->SnakeHead;SetPos(15, 12);switch (ps->status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_SELF:printf("很遗憾,你咬到了你自己\n");break;case KILL_BY_WALL:printf("很遗憾,你撞到墙了\n");break;}while (cur) {SnakeNode* next = cur->next;free(cur);cur = next;}free(ps->pFood);ps->pFood = NULL;ps->SnakeHead = NULL;ps = NULL;
}
3.3 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"void Test01() {int ch = 0;do {Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N)");ch = getchar();getchar();} while (ch == 'Y' || ch == 'y');
}int main() {//修改适配本地中文环境setlocale(LC_ALL, "");Test01();SetPos(0, 27);return 0;
}
至此,整个游戏的编写就结束了,读者还可以对这个代码进行升级,比如:
1.将地图的进行升级,不在仅仅只是一个方框,加大难度;
2.写一个文件操作,记录历史最高得分记录;
3.将运行出来的效果加上各种动画。等等。
4.运行结果
相关文章:

贪吃蛇/链表实现(C/C++)
本篇使用C语言实现贪吃蛇小游戏,我们将其分为了三个大部分,第一个部分游戏开始GameStart,游戏运行GameRun,以及游戏结束GameRun。对于整体游戏主要思想是基于链表实现,但若仅仅只有C语言的知识还不够,我们还…...

Qlik Sense : IntervalMatch(离散匹配)
什么是IntervalMatch IntervalMatch 前缀用于创建表格以便将离散数值与一个或多个数值间隔进行匹配,并且任选匹配一个或多个额外关键值。 语法: IntervalMatch (matchfield)(loadstatement | selectstatement ) IntervalMatch (matchfield,keyfield…...

MySql45讲-08.事务到底是隔离的还是不隔离的?(结合MVCC视频)
命令的启动时机 begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令。 事务的版本…...

备战蓝桥杯----数据结构及STL应用(基础2)
上次我们讲了vector的大致内容,接下来让我们讲一下栈,队列吧! 什么是栈呢? 很简单,我们用的羽毛球桶就是,我们取的球,是最后放的,栈是一种先进后出的数据结构。 方法函数 s.push(…...

日常学习之:vue + django + docker + heroku 对后端项目 / 前后端整体项目进行部署
文章目录 使用 docker 在 heroku 上单独部署 vue 前端使用 docker 在 heroku 上单独部署 django 后端创建 heroku 项目构建 Dockerfile设置 settings.pydatabase静态文件管理安全设置applicaiton & 中间件配置 设置 requirements.txtheroku container 部署应用 前后端分别部…...

LangGraph:一个基于LangChain构建的AI库,用于创建具有状态、多参与者的应用程序
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...

04-Nacos-服务注册基于spring boot实现
官方参考 在不依赖spring cloud 组件基础上,单独的微服务项目,实现nacos接入 1、依赖文件pom.xml <dependency><groupId>com.alibaba.boot</groupId><artifactId>nacos-discovery-spring-boot-starter</artifactId><…...

iOS 闭包和Block的区别
iOS 闭包和Block的区别 原文地址: mob64ca12eb7baf 引言 在iOS开发中,闭包和Block是两个常用的概念。它们都是将一段代码作为变量传递和使用的方式。尽管它们在实现上有一些相似之处,但它们之间还是存在一些重要的区别。本文将会详细介绍闭包和Block的…...

后端学习笔记——后端细碎知识点(每天更新......)
细碎知识点 主要是go后端,也会设计到python、java的知识,懒得分类整理,所以都写在一篇文章里面了,方便自己查看笔记。 context.BindJSON获取POST请求中的json数据gin.H封装了生成json的方式 common.ReturnJSONSuccess(c, gin.H{&…...

二进制中1的个数
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO 联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬 学习必须往深处挖&…...

python+matlab text(按图的相对位置显示)
python 用 python 画图时,如果想采用归一化的坐标来指定文本框的位置,则需要用到 transform ax.transAxes 参数,如 ax plt.gca() plt.text(0.1,0.2, "text", fontsize 20, transform ax.transAxes)matlab 方法1 text(___,Name…...

rust 引用/mut 的所有权
在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。 不可变引用(shared reference)实现了Copy trait,不会发生所有权转移可变引用(mutable reference)未实现,会发…...

油烟净化器科技改革,清新用餐生活
我最近分析了餐饮市场的油烟净化器等产品报告,解决了餐饮业厨房油腻的难题,更加方便了在餐饮业和商业场所有需求的小伙伴们。 随着餐饮业蓬勃发展,人们对用餐环境的要求也与日俱增。本文将深入研讨餐饮油烟净化器技术的改革方向,…...

[足式机器人]Part3 机构运动学与动力学分析与建模 Ch01-1 刚体系统的运动学约束
本文仅供学习使用,总结很多本现有讲述运动学或动力学书籍后的总结,从矢量的角度进行分析,方法比较传统,但更易理解,并且现有的看似抽象方法,两者本质上并无不同。 2024年底本人学位论文发表后方可摘抄 若有帮助请引用 本文参考: 《空间机构的分析与综合(上册)》-张启先…...

51单片机智能小车
51单片机智能小车 delay.c #include "intrins.h"void Delay2000ms() //11.0592MHz {unsigned char i, j, k;i 15;j 2;k 235;do{do{while (--k);} while (--j);} while (--i); }void Delay10us() //11.0592MHz {unsigned char i;i 2;while (--i); }void Delay…...

9. 嵌入式系统开发:安全性与可靠性设计模式---引言
在复杂的嵌入式系统设计中,为了提高嵌入式系统安全性并保护嵌入式系统免受各种潜在故障的影响,可以采用不同的设计模式。这些模式各自有优势和适用的场景: 1. 受保护的单通道模式(Protected Single Channel Pattern) …...

内网安全:Exchange服务
目录 Exchange服务 实验环境 域横向移动-内网服务-Exchange探针 一. 端口扫描 二. SPN扫描 三. 脚本探针(还可以探针是否有安全漏洞) 域横向移动-内网服务-Exchange爆破 一 .BurpSuite Intruder模块爆破 域横向移动-内网服务-Exchange漏洞 CVE-2020-17144 Exchange R…...

Flask介绍和优势
Flask诞生于2010年,是由Armin Ronacher用Python语言编写的一款轻量级Web开发框架。自发布以来,Flask逐渐成为开发人员喜爱的选择,并在2021年5月发布了Flask 2.0版本,引入了一些新增特性,如基本的异步支持。 使用Flask…...

喜报|「云原生数据库PolarDB」、「阿里云瑶池一站式数据管理平台」揽获“2023技术卓越奖”
日前,国内知名IT垂直媒体&技术社区IT168公布2023年“技术卓越奖”评选结果,经由行业CIO/CTO大咖、技术专家及IT媒体三方的联合严格评审,阿里云瑶池数据库揽获两项大奖:云原生数据库PolarDB荣获“2023年度技术卓越奖”…...

【动态规划】【字符串】【行程码】1531. 压缩字符串
作者推荐 视频算法专题 本文涉及知识点 动态规划汇总 LeetCode 1531. 压缩字符串 II 行程长度编码 是一种常用的字符串压缩方法,它将连续的相同字符(重复 2 次或更多次)替换为字符和表示字符计数的数字(行程长度)…...

检测头篇 | 原创自研 | YOLOv8 更换 SEResNeXtBottleneck 头 | 附详细结构图
左图:ResNet 的一个模块。右图:复杂度大致相同的 ResNeXt 模块,基数(cardinality)为32。图中的一层表示为(输入通道数,滤波器大小,输出通道数)。 1. 思路 ResNeXt是微软研究院在2017年发表的成果。它的设计灵感来自于经典的ResNet模型,但ResNeXt有个特别之处:它采用…...

PHP语法
#本来是在学命令执行,所以学了学,后来发现,PHP语法和命令执行的关系好像没有那么大,不如直接学php的一些命令执行函数了。# #但是还是更一下,毕竟还是很多地方都要求掌握php作为脚本语言,所以就学了前面的…...

MySQL:三大日志(binlog、redolog、undolog)
再了解三个日志前我们先了解一下MySQL的两层架构: Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数和所有跨…...

【QT+QGIS跨平台编译】之十二:【libpng+Qt跨平台编译】(一套代码、一套框架,跨平台编译)
文件目录 一、libpng介绍二、文件下载三、文件分析四、pro文件五、编译实践一、libpng介绍 PNG(Portable Network Graphics,便携式网络图形),是一种采用无损压缩算法的位图格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。 PNG使用从LZ77派生的无损数据压缩算…...

Windows 和 Anolis 通过 Docker 安装 Milvus 2.3.4
Windows 10 通过 Docker 安装 Milvus 2.3.4 一.Windows 安装 Docker二.Milvus 下载1.下载2.安装1.Windows 下安装(指定好Docker文件目录)2.Anolis下安装 三.数据库访问1.ATTU 客户端下载 一.Windows 安装 Docker Docker 下载 双击安装即可,安…...

JUC并发编程与源码分析学习笔记(三)
目录 五十六、JMM之入门简介 五十七、JMM之学术定义和作用 五十八、JMM之三大特性 五十九、JMM之多线程对变量的读写过程 六十、JMM之happens-before-上集 六十一、JMM之happens-before-下集 五十六、JMM之入门简介 Java内存模型之JMM 1、先从大厂面试题开始 ①、你知道…...

力扣日记1.28-【回溯算法篇】93. 复原 IP 地址
力扣日记:【回溯算法篇】93. 复原 IP 地址 日期:2023.1.28 参考:代码随想录、力扣 93. 复原 IP 地址 题目描述 难度:中等 有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0&…...

Java 的反射学习总结
目录 一、什么是反射? 二、如何获取类对象? 三、如何通过类对象来创建类的对象? 四、类对象获取类构造器的方式 五、通过类对象获取类的属性 六、通过类对象获取类的方法 一、什么是反射? 反射是指在运行时动态地获取、检查…...

图论第二天|695. 岛屿的最大面积 1020. 飞地的数量 130. 被围绕的区域 417. 太平洋大西洋水流问题 827.最大人工岛
目录 Leetcode695. 岛屿的最大面积Leetcode1020. 飞地的数量Leetcode130. 被围绕的区域Leetcode417. 太平洋大西洋水流问题Leetcode827.最大人工岛 Leetcode695. 岛屿的最大面积 文章链接:代码随想录 题目链接:695. 岛屿的最大面积 思路:dfs …...

【JavaScript 基础入门】02 JavaScrip 详细介绍
JavaScrip 详细介绍 目录 JavaScrip 详细介绍1. JavaScript 是什么2. JavaScript的作用3. HTML/CSS/JS 的关系4. 浏览器执行 JS 简介5. JavaScript 的组成6. JavaScript 的特点 1. JavaScript 是什么 JavaScript,通常缩写为 JS,是一种高级的,…...