贪吃蛇---C语言---详解
引言
C语言已经学了不短的时间的,这期间已经开始C++和Python的学习,想给我的C语言收个尾,想起了小时候见过别人的老人机上的贪吃蛇游戏,自己父母的手机又没有这个游戏,当时成为了我的一大遗憾,这两天发现C语言实现这个项目似乎并不难,于是查了一些WindowsAPI的控制台函数,实现了这一游戏。如果你觉得你的C语言基础语法学的差不多了,又想实现贪吃蛇这样一个小游戏,那么就跟我一起来实现它吧。下面是最终成品的样子:

本贪吃蛇是用控制台实现,其中¥是贪吃蛇的食物,⚪是贪吃蛇,■是墙体。
Win32 API
在开始我们的代码之前,像讲一下关于Win32 API的相关知识,Windows这个多作业系统除了协调应用程序的执行,分配内存,管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗,绘制图形,使用周边设备等目的,由于这些函数的服务对象是应用程序(Application),所以便称之为Application Programming Interface,简称API函数。Win32 API也就是Microsoft Windows32位平台的应用程序编程接口。
控制台程序
平常我们运行起来的黑框其实是就是控制台程序
我们可以用cmd命令来控制控制台窗口的长宽:比如设置窗口大小,30行,100列
mode con cols=100 lines=30

同时也可以通过命令修改窗口的名字:
title 贪吃蛇

这里注意一下,在改名字之后加一个getchar()保证程序处在运行状态,这样才能正确观察到要改后的名字。
这些能在控制台窗口执行的命令,像我上方图片中的代码一样,可以用C语言函数system来执行。
代码放在下面:
#include<stdlib.h>
int main()
{system("mode con cols=100 lines=30");//设置窗口大小system("title 贪吃蛇");//改窗口标题getchar();return 0;
}
这里注意一下system的头文件是
#include <stdlib.h>
控制台上的坐标COORD
COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕上的坐标,下面是关于对COORD的定义:
typedef struct _COORD{SHORT x;SHORT y;
}COORD, *PCOORD;
其中x轴和y轴如图

同时可以给上方结构体(坐标)赋值:
COORD pos = {10,15};
GetStdHandle
GetStdHandle是一个Windows API函数。它用于一个特定的标准设备(标准输入,标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);//函数的参数为标准设备
句柄是什么?
句柄相当于一个操作工具,你可以通过操作某设备的句柄去获得和修改某标准设备的信息
实例(获得句柄)
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性信息
BOOL WINAPI GetConsoleInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
//通过传CursorInfo的地址并通过函数将当前光标信息传给CursorInfo
CONSOLE_CURSOR_INFO
在上一份代码中CursorInfo,里面存的是光标信息,类型是CONSOLE_CURSOR_INFO,我们可以来看看这个类型是如何定义的
typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;//这个变量是表示光标所占一个格的百分比BOOL bVisible;//这个变量是决定光标是否可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完 全填充单元格到单元底部的水平线条。
- bVisible,游标的可见性。如果光标可见,则此成员为TRUE
我们在运行打印贪吃蛇的过程中将光标设置为不可见,就不会影响到整个游戏的美观
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo
上方的GetConsoleCursorInfo是通过函数获取光标信息,这次的函数是通过函数实在改变控制台光标信息,下面是本函数声明
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
下面看一组改变控制台光标信息的实例:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
SetConsoleCursorPosition
设置指定控制台缓冲区的光标位置,我们可以将坐标信息放到COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的控制台位置
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD pos
);
实例:
COORD pos = { 10, 5};HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos);
//通过以上代码可以将光标设置到10 5 位置上
看到这里,我们是否可以考虑封装一个函数,可以专门通过传入坐标来控制光标位置,于是封装了一个这样的函数Setpos
//设置光标的坐标
void SetPos(short x, short y)
{COORD pos = { x, y };HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos);
}
GetAsyncKeyState
这个函数用于获取按键情况,原型如下:
SHORT GetAsyncKeyState(int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位short数据中,最高位是1,说明按键的状态是按下,如果是0,说明最高位的状态是抬起;如果最低为被设置为1则说明,该按键被按过,否则为0。
如果我们要判断一个按键是否被按过,可以检测GetAsyncKeyState的返回值最低为的值是否为1,可据此写出一个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
这样就可以通过向KEY_PRESS传入键值直接监测按键是否被按过了。
下面是关于不同键值介绍的链接
Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn
不过目前我们知道:
- VK_UP 向上箭头键
- VK_DOWN 向下箭头键
- VK_LEFT 向左箭头键
- VK_RIGHT 向右箭头键
- VK_ESCAPE ESC按键
- VK_F3 F3按键
- VK_F4 F4按键
这些VK_XXX已经是头文件中用宏定义好的常量,直接用就行,不需要知道具体的值
就足够用了
贪吃蛇地图设计与分析
地图
如果想用控制台窗口打印地图,就需要了解一下控制台窗口坐标的知识
如下图所示,横向是X轴,从左向右增长,纵向是Y轴,从上到下依次增长

在地图上,我们打印墙体用宽字符■,打印蛇用宽字符●,打印食物我这里用的是宽字符¥(因为我个人比较喜欢)如果你在字符表里如果有别的喜欢的字符,也当然可以灵活的根据个人爱好改变
刚刚我介绍的时候介绍的字符是宽字符,意思是占两个字节的字符,普通的字符占一个字节
可以看看占两个字节字符和占一个字节字符的区别:

由观察可以发现,一个占两字节的字符在控制台打印的时候也是占两个一字节字符所占的位置的
这里还需要引入一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。 C语言最初假定字符都是但自己的。但是这些假定并不是在世界的任何地方都适用。
下面引用一段介绍:
C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字⺟上⽅有注⾳符 号,它就⽆法⽤ASCII码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符 号。⽐如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体 系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪 怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。⽐如,130在法语编码中代表了é,在希 伯来语编码中却代表了字⺟Gimel,在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这 些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。 ⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是GB2312,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入宽字符的类型wchar_t和宽字符的输入,输出函数,加入<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
刚才打印方框的过程提到了本地化,如果不进行本地化,■将无法被程序编译识别,最终只会打印问号,所以接下来我们讲讲如何运用<locale.h>以及其函数对编译环境进行本地化。
<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样的行为的部分。
标准中,依赖地区的部分有以下几项:
- 数字的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏, 指定⼀个类项:
- LC_COLLATE
- LC_CTYPE
- LC_MONETARY
- LC_NUMERIC
- LC_TIME
- LC_ALL---针对所有类项修改
关于每个类项的详细说明,可参考
setlocale 函数
char* setlocale (int category, const char* locale);
setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale的第一个参数可以是前面说明的类项中的一个,那么只会影响一个类项,如果第一个参数是LC_ALL,那么就直接影响所有类项。
C标准给第二个参数仅定义了2种可能的取值:"C"和""。
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL,"C");
当地区设为"C"时,库函数按正常方式执行。
如果想在程序运行时改变地区,就只能显示调用setlocale函数。用""作为第二个参数,调用setlocale函数就可以切换到本地模式,这种模式会适应本地环境。
当切换到我们本地模式后,就可以支持一些宽字符(如汉字)的占位输出了。
setlocale(LC_ALL, " ");//切换到本地环境
宽字符的打印
#include <stdio.h>
#include<locale.h>
int main() {setlocale(LC_ALL, "");wchar_t ch1 = L'●'; wchar_t ch2 = L'你';wchar_t ch3 = L'好';wchar_t ch4 = L'¥';printf("%c%c\n", 'a', 'b');wprintf(L"%c\n", ch1);wprintf(L"%c\n", ch2);wprintf(L"%c\n", ch3);wprintf(L"%c\n", ch4);return 0;
}

这里比对的更加清晰一些,⼀个普通字符占⼀个字符的位置 但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果 要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。
关于普通字符和宽字符的处理展示大概是这个样子:

我们可以假设实现一个地图,27行,58列,围绕周围画出地图:

蛇和食物
初始化的时候,假设蛇长为5,蛇的每个节点宽字符●。这里要注意的是,蛇的每个节点和食物出现的X轴位置都要保证是二的倍数,不然会出现蛇和食物无法对齐或者蛇一半卡在墙体中的情况。
代码环节
数据的结构设计
上面说了这么多,到现在终于可以讲代码了,在学了这些控制台操作和地图分析之后,相信其实聪明的你已经基本能大概想出来如何去实现贪吃蛇的逻辑了,在开始代码之前,来介绍一下我们对我们对贪吃蛇数据的维护和设计
这里讲一下定义的每个节点的结构体:
typedef struct SnakeNode
{int x;//节点横坐标int y;//节点纵坐标struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode, * pSnakeNode;//重命名结构体类型
如果要管理整条蛇,还需要我们封装一个Snake来维护整条蛇🐍:
typedef struct Snake
{pSnakeNode pSnake;//指向蛇头节点的指针pSnakeNode pFood;//指向食物的食物指针int Score;//当前分数int FoodWeight;//食物比重int SleepTime;//休眠时间enum GAME_STATUES status;//游戏当前状态enum DIRECTION dir;//蛇当前方向
}Snake, * pSnake;
在维护整条蛇的结构体类型中,定义了两个枚举类型,分别用来表示
游戏当前的状态:
enum GAME_STATUES {OK = 1,//游戏正常运行ESC, //点击ESC主动退出KILL_BY_WALL,//撞到墙游戏结束KILL_BY_SELF //咬到自己游戏结束
};
蛇当前的前进方向:
enum DIRECTION {UP = 1,//上DOWN, //下LEFT, //左RIGHT //右
};
贪吃蛇项目流程设计
这里介绍整个游戏过程中的运行逻辑,我们基本也是这个顺序展开代码

游戏主函数:运行逻辑
#include"greedy_snake.h"
int main()
{srand((unsigned int)time(NULL));//随机初始化种子,相关内容可以参考我之前的扫雷博客int ch;do {Snake snake = { 0 };//创建一个维护整个贪吃蛇的数据类型snake.pSnake = NULL;GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("想要再来一局吗?Y/N:");ch = getchar();} while (ch == 'Y' || ch == 'y');SetPos(0, 26);return 0;
}
GameStart-游戏开始的数据初始化和维护
void GameStart(pSnake ps)
{//下面五行使光标不可见HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO rem;GetConsoleCursorInfo(houtput, &rem);rem.bVisible = 0;SetConsoleCursorInfo(houtput, &rem);setlocale(LC_ALL, "");//设置为本地类项//初始化界面system("mode con cols=100 lines=30");//设置窗口大小system("title 贪吃蛇");//改窗口标题//下面是打印欢迎和介绍信息SetPos(32, 10);printf("欢迎来到贪吃蛇小游戏!\n");SetPos(33, 15);system("pause");system("cls");SetPos(29, 9);printf("游戏介绍:");SetPos(33, 11);printf("通过↑ ← ↓ →控制蛇的移动");SetPos(33, 13);printf("可以通过F3加速,F4减速");SetPos(33, 15);printf("更高的速度下可以获得更高的分数");SetPos(33, 17);printf("可以使用空格暂停");SetPos(33, 19);system("pause");//这个命令可以使游戏暂停,按任意键继续//绘制地图CreateMap();//初始化创建蛇,传psInitSnake(ps);//初始化创建食物,传psCreateFood(ps);
}
SetPos-设置光标位置
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}
CreateMap-绘制地图
void CreateMap()
{system("cls");SetPos(0, 0);//这里的WALL在头文件中用宏定义:#define WALL L'■'//上for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//左和右for (int i = 1; i <= 25; i++) {SetPos(0, i);wprintf(L"%lc", WALL);SetPos(56, i);wprintf(L"%lc", WALL);}//打印右侧边框的提示介绍信息SetPos(62, 15);printf("通过↑←↓→控制蛇的移动");SetPos(62, 16);printf("可以通过F3加速,F4减速");SetPos(62, 17);printf("更高的速度下可以获得更高的分数");SetPos(62, 18);printf("可以使用空格暂停");
}
CreateFood-初始化创建食物
void CreateFood(pSnake ps)
{int xx = 0;int yy = 0;//生成的地址不能在地图外,不能在蛇身上do{xx = rand() % 53 + 2;yy = rand() % 25 + 1;if (xx % 2 == 0) {pSnakeNode pcur = ps->pSnake;while (pcur) {if (xx == pcur->x && yy == pcur->y)goto again;pcur = pcur->next;}break;}again:;//循环直到生成正确的地址} while (1);pSnakeNode PFood = (SnakeNode*)malloc(sizeof(SnakeNode));if (PFood == NULL) {perror("malloc food fail:");exit(1);}PFood->x = xx;PFood->y = yy;ps->pFood = PFood;SetPos(xx, yy);//食物在宏中定义为:#define FOOD L'¥'wprintf(L"%lc", FOOD);
}
GameRun-游戏运行维护函数
void GameRun(pSnake ps)
{do {//打印游戏帮助信息SetPos(62, 10);printf("总分:%d\n", ps->Score);SetPos(62, 11);printf("食物分值:%2d\n", 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_LEFT) && ps->dir != RIGHT)ps->dir = LEFT;else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)ps->dir = RIGHT;else if (KEY_PRESS(VK_ESCAPE)) {ps->status = ESC;break;}else if (KEY_PRESS(VK_F3)) {//F3设置加速if (ps->SleepTime >= 80) {ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)) {//F4设置减速if (ps->FoodWeight > 2) {ps->SleepTime += 30;ps->FoodWeight -= 2;}}else if (KEY_PRESS(VK_SPACE))//空格设置暂停{while (1) {Sleep(100);if (KEY_PRESS(VK_SPACE)) {break;}}}//睡一下Sleep(ps->SleepTime);//根据按键控制蛇的运动和吃食物,并打印SnakeMove(ps);} while (ps->status == OK);
}
SnakeMove-蛇移动
void SnakeMove(pSnake ps)
{//根据在GameRun中获得的方向设置生成蛇的下一个节点pSnakeNode pNext = (SnakeNode*)malloc(sizeof(SnakeNode));if (pNext == NULL) {perror("malloc pNext fail:");exit(1);}pNext->next = NULL;switch (ps->dir) {case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break; case LEFT:pNext->x = ps->pSnake->x - 2;pNext->y = ps->pSnake->y;break; case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//判断下一个节点的位置并操控蛇的状态if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {//如果吃上食物EatFood(ps, pNext);}else {//如果没吃上食物NotEatFood(ps, pNext);KillByWall(ps);//判断是否撞墙KillBySelf(ps);//判断是否咬到自己}
}
EatFood-吃到食物后蛇增长
void EatFood(pSnake ps,pSnakeNode pNext)
{//将新节点赋给蛇pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode pcur = ps->pSnake;while (pcur) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}ps->Score += ps->FoodWeight;//释放并创建新食物free(ps->pFood);CreateFood(ps);
}
NotEatFood-没有吃到食物向后移动
void NotEatFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode pcur = ps->pSnake;while (pcur->next->next) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}SetPos(pcur->next->x, pcur->next->y);printf(" ");//将最后一个节点置空free(pcur->next);pcur->next = NULL;SetPos(pcur->x, pcur->y);//蛇的身体在头文件中用宏定义为:#define BODY L'●'wprintf(L"%lc", BODY);
}
KillByWall-撞墙判定
void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;//如果撞墙改变游戏状态}
}
KillBySelf-咬到自己判定
void KillBySelf(pSnake ps)
{pSnakeNode pcur = ps->pSnake->next;while (pcur) {if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;//如果要到自己改变游戏状态return;}pcur = pcur->next;}
}
GameEnd-游戏善后,释放蛇
void GameEnd(pSnake ps)
{//打印结束信息SetPos(20, 11);switch (ps->status){case ESC:printf("正常退出游戏\n");SetPos(20, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_WALL:printf("撞墙了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_SELF:printf("咬到自己了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;}//释放蛇pSnakeNode pcur = ps->pSnake;pSnakeNode del = ps->pSnake;while (pcur) {del = pcur;pcur = pcur->next;free(del);}ps->pSnake = NULL;SetPos(0, 26);free(ps->pFood);ps = NULL;
}
代码汇总
写了这么多,大概就介绍完了所有函数,现在将它们放到三个文件中,相应创建文件CV一下应该就能在你们的VS跑了
头文件-greedy_snake.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<windows.h>
#include<locale.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'■'
#define BODY L'●'
#define FOOD L'¥'
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)enum GAME_STATUES {OK = 1,ESC,KILL_BY_WALL,KILL_BY_SELF
};enum DIRECTION {UP = 1,DOWN,LEFT,RIGHT
};typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode, * pSnakeNode;typedef struct Snake
{pSnakeNode pSnake;//指向蛇头节点的指针pSnakeNode pFood;//指向食物的食物指针int Score;//当前分数int FoodWeight;//食物比重int SleepTime;//休眠时间enum GAME_STATUES status;//游戏当前状态enum DIRECTION dir;//蛇当前方向
}Snake, * pSnake;//游戏开始的维护
void GameStart(pSnake ps);//绘制地图
void CreateMap();//初始化蛇
void InitSnake(pSnake ps);//初始化食物
void CreateFood(pSnake ps);//设置光标位置
void SetPos(int x, int y);//游戏运行维护函数
void GameRun(pSnake ps);//游戏结束善后
void GameEnd(pSnake ps);//蛇移动
void SnakeMove(pSnake ps);
源文件-greedy_snake.c
#include"greedy_snake.h"//设置光标位置
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}
void CreateMap()
{system("cls");SetPos(0, 0);//上for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//左for (int i = 1; i <= 25; i++) {SetPos(0, i);wprintf(L"%lc", WALL);SetPos(56, i);wprintf(L"%lc", WALL);}SetPos(62, 15);printf("通过↑←↓→控制蛇的移动");SetPos(62, 16);printf("可以通过F3加速,F4减速");SetPos(62, 17);printf("更高的速度下可以获得更高的分数");SetPos(62, 18);printf("可以使用空格暂停");
}//初始化蛇
void InitSnake(pSnake ps)
{//创建五个蛇身节点pSnakeNode pcur = NULL;for (int i = 0; i < 5; i++) {pcur = (SnakeNode*)malloc(sizeof(SnakeNode));if (pcur == NULL) {perror("malloc 节点 fail:");exit(1);}pcur->x = POS_X + 2 * i;pcur->y = POS_Y;pcur->next = NULL;if (ps->pSnake == NULL) {ps->pSnake = pcur;}else {pcur->next = ps->pSnake;ps->pSnake = pcur;}}//打印蛇身pcur = ps->pSnake;while (pcur) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//贪吃蛇信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;ps->status = OK;
}void CreateFood(pSnake ps)
{int xx = 0;int yy = 0;do{xx = rand() % 53 + 2;yy = rand() % 25 + 1;if (xx % 2 == 0) {pSnakeNode pcur = ps->pSnake;while (pcur) {if (xx == pcur->x && yy == pcur->y)goto again;pcur = pcur->next;}break;}again:;} while (1);pSnakeNode PFood = (SnakeNode*)malloc(sizeof(SnakeNode));if (PFood == NULL) {perror("malloc food fail:");exit(1);}PFood->x = xx;PFood->y = yy;ps->pFood = PFood;SetPos(xx, yy);wprintf(L"%lc", FOOD);
}void GameStart(pSnake ps)
{//下面五行使光标不可见HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO rem;GetConsoleCursorInfo(houtput, &rem);rem.bVisible = 0;SetConsoleCursorInfo(houtput, &rem);setlocale(LC_ALL, "");//设置为本地类项//初始化界面system("mode con cols=100 lines=30");//设置窗口大小system("title 贪吃蛇");//改窗口标题SetPos(32, 10);printf("欢迎来到贪吃蛇小游戏!\n");SetPos(33, 15);system("pause");system("cls");SetPos(29, 9);printf("游戏介绍:");SetPos(33, 11);printf("通过↑ ← ↓ →控制蛇的移动");SetPos(33, 13);printf("可以通过F3加速,F4减速");SetPos(33, 15);printf("更高的速度下可以获得更高的分数");SetPos(33, 17);printf("可以使用空格暂停");SetPos(33, 19);system("pause");//绘制地图CreateMap();//初始化创建蛇,传psInitSnake(ps);//初始化创建食物,传psCreateFood(ps);
}void EatFood(pSnake ps,pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode pcur = ps->pSnake;while (pcur) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}ps->Score += ps->FoodWeight;free(ps->pFood);CreateFood(ps);
}void NotEatFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode pcur = ps->pSnake;while (pcur->next->next) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}SetPos(pcur->next->x, pcur->next->y);printf(" ");free(pcur->next);pcur->next = NULL;SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);
}void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}void KillBySelf(pSnake ps)
{pSnakeNode pcur = ps->pSnake->next;while (pcur) {if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}pcur = pcur->next;}
}void SnakeMove(pSnake ps)
{pSnakeNode pNext = (SnakeNode*)malloc(sizeof(SnakeNode));if (pNext == NULL) {perror("malloc pNext fail:");exit(1);}pNext->next = NULL;switch (ps->dir) {case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break; case LEFT:pNext->x = ps->pSnake->x - 2;pNext->y = ps->pSnake->y;break; case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {EatFood(ps, pNext);}else {NotEatFood(ps, pNext);KillByWall(ps);KillBySelf(ps);}
}void GameRun(pSnake ps)
{do {//打印游戏帮助信息SetPos(62, 10);printf("总分:%d\n", ps->Score);SetPos(62, 11);printf("食物分值:%2d\n", 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_LEFT) && ps->dir != RIGHT)ps->dir = LEFT;else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)ps->dir = RIGHT;else if (KEY_PRESS(VK_ESCAPE)) {ps->status = ESC;break;}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;}}else if (KEY_PRESS(VK_SPACE)){while (1) {Sleep(100);if (KEY_PRESS(VK_SPACE)) {break;}}}//睡一下Sleep(ps->SleepTime);//根据按键控制蛇的运动和吃食物,并打印SnakeMove(ps);} while (ps->status == OK);
}void GameEnd(pSnake ps)
{SetPos(20, 11);switch (ps->status){case ESC:printf("正常退出游戏\n");SetPos(20, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_WALL:printf("撞墙了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_SELF:printf("咬到自己了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;}pSnakeNode pcur = ps->pSnake;pSnakeNode del = ps->pSnake;while (pcur) {del = pcur;pcur = pcur->next;free(del);}ps->pSnake = NULL;SetPos(0, 26);free(ps->pFood);ps = NULL;
}
运行文件-snake_run.c
#include"greedy_snake.h"
int main()
{srand((unsigned int)time(NULL));int ch;do {Snake snake = { 0 };snake.pSnake = NULL;GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("想要再来一局吗?Y/N:");ch = getchar();} while (ch == 'Y' || ch == 'y');SetPos(0, 26);return 0;
}
运行截图





结尾
到这里,本篇博客的内容基本上就结束了,写博客不易,如果感觉对你有帮助的话,还请留个赞留个关注再走啊。博主的C语言语法学习之路到现在也算是真正结束,统计下来C语言将近学了三四遍了,在后面的时间里,我准备好好开始过数据结构的内容,这些时日是没有特别多的时间去写题攻算法了,给自己报了一堆比赛还需要去准备,还是要先把C++和Python在假期赶快速成一下,数据结构系统仔细的过上一遍,给未来打好基础。后期我还会继续产出有意思的内容,请大家多多关注我吧!
在这里记录一下,今天是2024.1.31,大一的寒假♥
相关文章:
贪吃蛇---C语言---详解
引言 C语言已经学了不短的时间的,这期间已经开始C和Python的学习,想给我的C语言收个尾,想起了小时候见过别人的老人机上的贪吃蛇游戏,自己父母的手机又没有这个游戏,当时成为了我的一大遗憾,这两天发现C语…...
Airflow原理浅析
⭐️ airflow基本原理 Apache Airflow 是一个开源的工作流自动化工具,它用于调度和管理复杂的数据工作流。Airflow 的原理基于有向无环图(DAG)的概念,它通过编写和组织任务的有向图来描述工作流程。 以下是 Apache Airflow 的一…...
uniapp 使用canvas 画海报,有手粘贴即可用
html部分 <view click"doposter">下载海报</view> <canvas canvas-id"myCanvas" type2d style"width: 370px; height: 550px;opcity:0;position: fixed;z-index:-1;" id"myCanvas" />js 部分 drawBackground() {c…...
Vite+Vue3+TS 引入使用Cesium.js
申请 Cesium Token 进入Cesium 注册账号 cesium 离谱的是 E宝 (Epic) 居然可以快捷登录?! 登录后点击导航栏的 Access Token 再右侧即可看到默认Token 安装&引入 # Cesium pnpm pnpm install cesium# 如果项目同时存在Three.js 需避免使用pnpm T…...
Cocos creator 动作系统
动作系统简介 是用于控制物体运动的一套系统,完全依赖代码进行实现,动态调节节点的移动。 移动 cc.moveTo 移动到某个坐标(x,y) //1秒时间内,移动到0,0let action1 cc.moveTo(1,0,0)this.node.runAction(action1)c…...
对Spring当中AOP的理解
AOP(面向切面编程)全称Aspect Oriented Programminge AOP就是把系统中重复的代码抽取出来,单独开发,在系统需要时,使用动态代理技术,在不修改源码的基础上,将单独开发的功能通知织入(应用)到系统中的过程,完…...
【Vue】2-8、Axios 网络请求
cdn:<script src"https://unpkg.com/axios/dist/axios.min.js"></script> 注:使用 CDN 链接就可以不需要去下载对应的 js 文件到本地,只需要联网即可使用,可以减少项目的体积 <!DOCTYPE html> <…...
Vue中嵌入原生HTML页面
Vue中嵌入html页面并相互通信 需求:b2b支付需要从后获取到数据放到form表单提交跳转,如下: 但是vue目前暂时没找到有类似功能相关文档,所以我采用iframe嵌套的方式 1. Vue中嵌入Html <iframe src"/static/gateway.htm…...
streampark+flink一键整库或多表同步mysql到doris实战
streamparkflink一键整库或多表同步mysql到doris实战,此应用一旦推广起来,那么数据实时异构时,不仅可以减少对数据库的查询压力,还可以减少数据同步时的至少50%的成本,还可以减少30%的存储成本; streampar…...
Vim实战:使用 Vim实现图像分类任务(二)
文章目录 训练部分导入项目使用的库设置随机因子设置全局参数图像预处理与增强读取数据设置Loss设置模型设置优化器和学习率调整策略设置混合精度,DP多卡,EMA定义训练和验证函数训练函数验证函数调用训练和验证方法 运行以及结果查看测试完整的代码 在上…...
学习MySQL ENUM数据类型
学习MySQL ENUM数据类型 ENUM是MySQL中的一个字符串对象,它允许从预定义的值列表中选择一个值。这种数据类型特别适用于值的数量有限且不太可能变化的情况。 定义ENUM类型 在定义ENUM类型时,你需要明确列出所有可能的字符串值。例如: CRE…...
88.合并两个有序数组
88.合并两个有序数组 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。 请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。 **注意:**最…...
python查询xml类别
第一章 导包 import os from xml.etree.ElementTree import ElementTree第二章 存储类别 # 定义一个空集合用于存储类别 classes set()第三章 遍历所有XML文件 # 遍历指定目录下的所有XML文件 for filename in os.listdir(/home/li/PycharmProjects/Annotations):if filena…...
nginx配置及性能优化
1. 请简述nginx的工作原理? Nginx的工作原理基于事件驱动模型和异步非阻塞I/O处理机制。 具体来说,Nginx接收到客户端的请求后,会将该请求映射到配置文件中指定的location block。这个过程中,Nginx本身并不执行实际的工作&#…...
阿里云如何找回域名,进行添加或删除?
权威域名管理介绍说明,包含添加域名、删除域名、找回域名、域名分组等操作介绍。 一、添加域名 非阿里云注册域名或子域名如需使用云解析DNS,需要通过添加域名功能,将主域名或子域名添加到云解析控制台,才可以启用域名解析服务。…...
机器学习 低代码 ML:PyCaret 的使用
✅作者简介:人工智能专业本科在读,喜欢计算机与编程,写博客记录自己的学习历程。 🍎个人主页:小嗷犬的个人主页 🍊个人网站:小嗷犬的技术小站 🥭个人信条:为天地立心&…...
前端入门第二天
目录 一、列表、表格、表单 二、列表(布局内容排列整齐的区域) 1.无序列表(不规定顺序) 2.有序列表(规定顺序) 3.定义列表(一个标题多个分类) 三、表格 1.表格结构标签 2.合并…...
Django实现富文本编辑器Ckeditor5图片上传功能
上一章我们已经为我们的博客继承了富文本编辑器Ckeditor5,虽然已经可以对文字进行排版处理,虽然已经可以通过插入图片的url地址来插入图片,但还无法通过本地上传图片,那么我们这个富文本编辑器就是不完整的,这一章我们将实现上传图片功能! Ckeditor5图片上传采用的是…...
【C语言】epoll_wait / select
一、epoll_wait和select对比 1. 阻塞和非阻塞 在Linux C语言中进行socket编程时,epoll_wait 和 select 都是用于多路I/O复用的系统调用,但是它们的行为可以设置为阻塞和非阻塞模式,这取决于调用它们时所使用的参数。 让我们分别看看 epoll…...
Java 数据抓取
大家好我是苏麟 , 今天聊聊数据抓取 . 大家合理使用 注意,爬虫技术不能滥用,干万不要给别人的系统造成压力、不要侵犯他人权益! 数据抓取 实质上就是java程序模拟浏览器进行目标网站的访问,无论是请求目标服务器的接口还是请求目标网页内容…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
C#中的CLR属性、依赖属性与附加属性
CLR属性的主要特征 封装性: 隐藏字段的实现细节 提供对字段的受控访问 访问控制: 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性: 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑: 可以…...
stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
数据结构:递归的种类(Types of Recursion)
目录 尾递归(Tail Recursion) 什么是 Loop(循环)? 复杂度分析 头递归(Head Recursion) 树形递归(Tree Recursion) 线性递归(Linear Recursion)…...
java 局域网 rtsp 取流 WebSocket 推送到前端显示 低延迟
众所周知 摄像头取流推流显示前端延迟大 传统方法是服务器取摄像头的rtsp流 然后客户端连服务器 中转多了,延迟一定不小。 假设相机没有专网 公网 1相机自带推流 直接推送到云服务器 然后客户端拉去 2相机只有rtsp ,边缘服务器拉流推送到云服务器 …...
[学习笔记]使用git rebase做分支差异化同步
在一个.NET 项目中,使用了Volo.Abp库,但出于某种原因,需要源码调试,因此,使用源码方式集成的项目做了一个分支archive-abp-source 其中引用方式变更操作的提交为:7de53907 后续,在master分支中…...
【汇编逆向系列】四、函数调用包含单个参数之Double类型-mmword,movsd,mulsd,addsd指令,总结汇编的数据类型
一、汇编代码 上一节开始,讲到了很多debug编译独有的汇编方式,为了更好的区分release的编译器优化和debug的区别,从本章节开始将会提供debug和release的汇编用作对比 Debugb编译 single_double_param:00000000000000A0: F2 0F 11 44 24 08…...
