手把手教你实现贪吃蛇

 > 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:实现贪吃蛇

> 毒鸡汤:时间并不可真的帮我们去解决哪些问题,它只不过是会把原来怎么也想不通的问题,变得不再重要了。
> 望小伙伴们点赞👍收藏✨加关注哟💕💕 

🌟前言

        作为零零后的我们想必贪吃蛇都玩过吧,博主记得当时还在小霸王里面玩的这款小游戏,十分怀念,记忆深刻,每次都是偷偷躲在被窝里面玩,为了防止别抓,基本上是躲在厕所里面玩,上个厕所上个十年,丑事不必再提。作为一个程序员捏,当然要简单的实现它啦,也是为曾经的自己画上一个圆满的句号吧。那咱们闲话少谈,直接手撕贪吃蛇。

⭐游戏背景

        我们采用的是VS2019编译环境,所以家人们准备好编译环境,当然咱们上手不能直接上代码,可能一开始上代码就是从入门到放弃,我懂大家的捏。咱们先介绍游戏,到时候反手就说,这个博主教我们怎么玩游戏,取关,必须取关,家人们不先知道怎么玩你怎么知道游戏的逻辑,给这些人拖出去斩了。

        不知道大家还记得在C语言中我们实现了两款游戏,一个是三子棋,另一个是扫雷,这些游戏和贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。



博主后面这些经典小游戏都会一一实现,刺激

游戏演示

看的是不是很神奇,一点也不神奇,不信你继续向下看。

贪吃蛇

⭐实现目标

使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏

⭐技术要点

        C语⾔、数据结构(链表)、枚举、结构体、动态内存管理、预处理指令、win32 API(这个咱们后面讲解),这些基本上都学过哟,像链表不知道大家能不能手撕链表,不会的拖出去打30大板。

win32 API介绍

        使用Win32 API,应用程序可以充分挖掘Windows的32位操作系统的潜力。 Microsoft的所有32位平台都支持统一的API,包括函数、结构、消息、宏及接口。使用 Win32 API不但可以开发出在各种平台上都能成功运行的应用程序,而且也可以充分利用每个平台特有的功能和属性。

        在具体编程时,程序实现方式的差异依赖于相应平台的底层功能的不同。最显著的差异是某些函数只能在更强大的平台上实现其功能。例如,安全函数只能在Windows NT操作系统下使用。另外一些主要差别就是系统限制,比如值的范围约束,或函数可管理的项目个数等等。

当然啦,咱们不用学习这里面全部知识,不然又要被黑子喷咯。

🌙Win32 API

        有一些函数,并不用于交互,比如管理当前系统正在运行的进程、硬件系统状态的监视等等……这些函数只有一套,但是可以被所有的Windows程序调用(只要这个程序的权限足够高),简而言之,API是为程序所共享的。

        为了达到所有程序能共享一套API的目的,Windows采用了“动态链接库”的办法。之所以叫“动态链接库”,是因为这样的函数库的调用方式是“随用随取”而不是像静态链接库那样“用不用都要带上”。

🌙控制台程序

        像我们运行一个代码,就有一个黑框框,没错它就是控制台程序,Win32 API我们是可以控制它滴,终于有人可以管得住它了,不然它还是一个野孩子。

我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列

1 mode con cols= 100 lines= 30
也可以通过命令设置控制台窗⼝的名字:
1 title 贪吃蛇
耶嘿这孩子有名字咯。

🌙控制台屏幕上的坐标COORD

COORD 是Windows API中定义的⼀种结构,表示⼀个字符在控制台屏幕上的坐标
typedef struct _COORD {SHORT X;SHORT Y;
} COORD, * PCOORD;
给坐标赋值:
COORD pos = { 10 , 15 };

🌙GetStdHandle

GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。简单的来讲就是可以用键盘控制控制台。
HANDLE GetStdHandle(DWORD nStdHandle);
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)

hOutput = GetStdHandle(STD _OUTPUT_HANDLE);

🌙GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

实例:
HANDLE hOutput = NULL ;
// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo (hOutput, &CursorInfo); // 获取控制台光标信息

🌙SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。
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);
SetPos:封装⼀个设置光标位置的函数
// 设置光标的坐标
void SetPos ( short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL ;
// 获取标准输出的句柄 ( ⽤来标识不同设备的数值 )
hOutput = GetStdHandle (STD_OUTPUT_HANDLE);
// 设置标准输出上光标的位置为 pos
SetConsoleCursorPosition (hOutput, pos);
}

🌙GetAsyncKeyState

获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState ( int vKey );
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。 是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
# define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

贪吃蛇游戏设计与分析

这里我们得了解游戏的逻辑和框架,别慌,一步一步慢慢来!!!

🌙地图设计

咱们看看地图是如何设计的:

既然要设计地图,必然需要在VS2019下控制台的坐标问题。

        这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍一下控制台窗⼝的坐标知识。控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★普通的字符是占⼀个字节的,这类宽字符是占用2个字节。这里再简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是但自己的。但是这些假定并不是在世界的任何地方都适用。
        后来为了使C语言适应国家化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加入和<locale.h>头文件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
💫setlocale函数

char* setlocale (int category, const char* locale);

  1. setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
  2. setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。

C标准给第⼆个参数仅定义了2种可能取值:"C"和" "。在任意程序执⾏开始,都会隐藏式执⾏调⽤:

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显⽰调⽤setlocale函数。用" "作为第2个参数,调setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
setlocale (LC_ALL, " " ); // 切换到本地环境
我们发现⼀个普通字符占⼀个字符的位置但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。
💫地图坐标
        我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙, 如下:

🌙蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。 注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。 关于食物,就是在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

🌙游戏流程设计

核心逻辑实现分析(重点)

        咱们从三个方面来讲解贪吃蛇的核心逻辑分析:test.c主函数,snake.h包含头文件,snake.c主函数的实现,跟我们玩链表差不多,都是老套路了那贪吃蛇的实现正式上路咯,启程。

🌙snake.h头文件的实现

        像头文件这个东西必然是要包含一些头文件的,当然这只是它的一部分作用,像我们定义的链表,为了在各个源文件都能使用,我们就在头文件实现链表。如果函数很多,就把函数头包函数在头文件中,方便调用,那咱们看看在snake.h中有啥?



//包含头文件
#include <locale.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<windows.h>
#include<time.h>
#include<stdbool.h>//定义图案
#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 )//枚举键位
enum  DIRECTION
{UP = 1,//上DOWN,//下LEFT,//左RIGHT//右
};//枚举游戏状态
enum GAME_STATUS
{OK,//正常运行END_NORMAL,//按ESC退出KILL_BY_WALL,KILL_BY_SELF
};//贪吃蛇每个节点的描述
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 DIRECTION _Dir;//描述蛇的方向enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;//游戏开始 - 完成游戏的初始化动作
void GameStart(pSnake ps);//定位坐标
void SetPos(short x, short y);//游戏开始的欢迎界面
void WelComeToGame();//打印地图
void CreateMap();//初始化贪吃蛇
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//游戏的正常运行
void GameRun(pSnake ps);//打印帮助信息
void PrintHelpInfo();//游戏暂定和恢复
void Pause();//蛇的移动
void SnakeMove(pSnake ps);//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext);//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext);//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext);//蛇是否撞墙
void KillByWall(pSnake ps);//蛇是否自杀
void KillBySelf(pSnake ps);//游戏结束后的善后处理
void GameEnd(pSnake ps);

🌙snake.c源文件的实现

        在这个文件中咱们实现贪吃蛇的函数:实现时当然需要包含snake.h的文件啦,需要用头文件里面的链表等.....

🌠游戏开始 - 初始化游戏


//1. 游戏开始 - 初始化游戏
void GameStart(pSnake ps)
{//设置控制台的大小system("mode con cols=100 lines=30");system("title 贪吃蛇");//光标影藏掉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);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
}
💫打印欢迎界面

其实这个函数大家可以自己设置,把欢迎界面整的好看些,像博主就比较懒就简单设计一下咯。

//打印欢迎界面
void WelComeToGame()
{//定位光标SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25);system("pause");//pause是暂停system("cls");SetPos(20, 14);printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");SetPos(40, 25);system("pause");system("cls");
}
💫创建地图/初始化贪食蛇

大家眼睛擦亮点,计算坐标时不要计算错了,可以根据自己设计自己喜欢的图:

        切记一定要把蛇初始状态需要是偶数倍:初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有一半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。

//创建地图
void CreateMap()
{//上SetPos(0, 0);int i = 0;for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}//初始化贪食蛇
void InitSnake(pSnake ps)
{//初始化蛇身 0 0 0 0 0 pSnakeNode cur = NULL;//循环5个节点for (int i = 0; i < 5; i++){//开辟空间cur = (pSnakeNode)malloc(sizeof(SnakeNode));//判断是否开辟成功if (cur == NULL){perror("InitSnake:malloc");exit(-1);}//初始化蛇的位置在(24,5)cur->x = POS_X + i * 2;cur->y = POS_Y;cur->next = NULL;//开始链接//头插if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇身cur = ps->_pSnake;//指向贪吃蛇头结点的指针while (cur){//定位坐标SetPos(cur->x, cur->y);//打印wprintf(L"%lc", BODY);//指向下一个节点cur = cur->next;}//对贪吃蛇的每个组成都初始化ps->_Status = OK;//游戏的状态ps->_Score = 0;//贪吃蛇累计的总分ps->_pFood = NULL;//指向食物结点的指针ps->_SleepTime = 200;//每走一步休息的时间ps->_FoodWeight = 10;//一个食物的分数//(我们默认蛇开始向右走)ps->_Dir = RIGHT;//描述蛇的方向
}
💫创建食物

        关于食物,就是在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

在创建食物时我们需要注意一些事项:小本本拿好咯

  • 食物可能与创建的蛇相同:就必须从新创建食物。(这里采用again)
  • 食物需要开辟空间的:方便和蛇链接嘛,毕竟是单链表
  • 食物的一些初始化
//创建食物
void CreateFood(pSnake ps)
{//初始化int x = 0;int y = 0;
again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x坐标必须是2的倍数//坐标不能和蛇的身体冲突pSnakeNode cur = ps->_pSnake;while (cur){//比较坐标if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//给食物开辟空间pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//判断if (pFood == NULL){perror("CreateFood()::malloc()");return;}//初始化pFood->x = x;pFood->y = y;ps->_pFood = pFood;//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);}
🌠游戏运行 - 游戏的正常运行过程


//2. 游戏运行 - 游戏的正常运行过程
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();//循环方向,直到方向正确do{//打印得分SetPos(64, 10);printf("得分:%05d", 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_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 = END_NORMAL;break;}else if (KEY_PRESS(VK_SPACE)){//暂停蛇移动Pause();}else if (KEY_PRESS(VK_F3))//加速{if (ps->_SleepTime >= 80){ps->_SleepTime -= 30;ps->_FoodWeight += 2;}}else if (KEY_PRESS(VK_F4))//减速{if (ps->_SleepTime < 320){ps->_SleepTime += 30;ps->_FoodWeight -= 2;}}//蛇动的时间Sleep(ps->_SleepTime);//蛇的移动SnakeMove(ps);} while (ps->_Status == OK);
}
💫打印帮助信息

这个函数根据自己的需要而写,这个就是看大家的创意咯

//打印帮助信息
void PrintHelpInfo()
{SetPos(64, 15);printf("1.不能撞墙,不能咬到自己");SetPos(64, 16);printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");SetPos(64, 17);printf("3.F3加速,F4减速");SetPos(64, 18);printf("4.ESC-退出, 空格-暂停游戏");SetPos(64, 20);printf("旧言专用版权");}
💫判断蛇头到达的坐标处是否是食物

这个函数其实很挫,判断一下x和y坐标就行。

//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y){return 1;}else{return 0;}
}
💫吃掉食物

直接就是头插法:不会的打板子

吃完食物记得创建食物

//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{//头插(把食物当做蛇头)pnext->next = ps->_pSnake;ps->_pSnake = pnext;//打印蛇pSnakeNode cur = ps->_pSnake;while (cur){//找坐标SetPos(cur->x, cur->y);//打印wprintf(L"%lc", BODY);//找下一个节点cur = cur->next;}//释放食物空间free(ps->_pFood);//加分数ps->_Score += ps->_FoodWeight;//创建食物CreateFood(ps);//新创建食物
}
💫不吃食物

运转到下一个坐标,开始覆盖,用头插,把前面的蛇尾置为空。

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{//头插pnext->next = ps->_pSnake;ps->_pSnake = pnext;//打印蛇身pSnakeNode cur = ps->_pSnake;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 KillBySelf(pSnake ps)
{//遍历一遍蛇的坐标就行pSnakeNode cur = ps->_pSnake->next;while (cur){if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y){//游戏状态为自杀ps->_Status = KILL_BY_SELF;}//找下一个节点cur = cur->next;}
}
💫蛇的移动

蛇既然要移动到下一个坐标,所以需要开辟下一个位置的空间,再判断蛇是否到食物。

等到蛇移动到下一个坐标再判断蛇是否撞墙自杀。

//蛇的移动
void SnakeMove(pSnake ps)
{//开辟蛇移动到下一个格子的空间pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));//判断if (pNext == NULL){perror("SnakeMove()::malloc()");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 (NextIsFood(ps, pNext)){//吃掉食物EatFood(ps, pNext);}else{//不吃食物NoFood(ps, pNext);}//蛇是否撞墙KillByWall(ps);//蛇是否自杀KillBySelf(ps);}
💫暂停蛇移动

让蛇可以有视觉效果,打印时,休息一会儿。

//暂停蛇移动
void Pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}
 🌠游戏结束 - 游戏善后(释放资源)

等于释放内存,这个表示我们熟的来:

//3. 游戏结束 - 游戏善后(释放资源)
void GameEnd(pSnake ps)
{//判断游戏状态SetPos(20, 12);switch (ps->_Status){case END_NORMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("自杀了,游戏结束\n");break;case KILL_BY_WALL:printf("撞墙了,游戏结束\n");break;}//释放蛇身的结点pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}ps->_pSnake = NULL;}

🌙test.c源文件的实现

直接上代码好叭

//包含头文件
#include"snake.h"void test()
{int ch = 0;do{Snake snake = { 0 };//创建了贪吃蛇//1. 游戏开始 - 初始化游戏GameStart(&snake);//2. 游戏运行 - 游戏的正常运行过程GameRun(&snake);//3. 游戏结束 - 游戏善后(释放资源)GameEnd(&snake);SetPos(20, 18);printf("再来一局吗?(Y/N):");ch = getchar();getchar();// 清理掉\n} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}int main()
{//设置程序适应本地环境setlocale(LC_ALL, "");//设置语言环境srand((unsigned int)time(NULL));//游戏睡眠状态//调用函数test();return 0;
}

🌞总代码

💧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);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
}//打印欢迎界面
void WelComeToGame()
{//定位光标SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25);system("pause");//pause是暂停system("cls");SetPos(20, 14);printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");SetPos(40, 25);system("pause");system("cls");
}//创建地图
void CreateMap()
{//上SetPos(0, 0);int i = 0;for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}//初始化贪食蛇
void InitSnake(pSnake ps)
{//初始化蛇身 0 0 0 0 0 pSnakeNode cur = NULL;//循环5个节点for (int i = 0; i < 5; i++){//开辟空间cur = (pSnakeNode)malloc(sizeof(SnakeNode));//判断是否开辟成功if (cur == NULL){perror("InitSnake:malloc");exit(-1);}//初始化蛇的位置在(24,5)cur->x = POS_X + i * 2;cur->y = POS_Y;cur->next = NULL;//开始链接//头插if (ps->_pSnake == NULL){ps->_pSnake = cur;}else{cur->next = ps->_pSnake;ps->_pSnake = cur;}}//打印蛇身cur = ps->_pSnake;//指向贪吃蛇头结点的指针while (cur){//定位坐标SetPos(cur->x, cur->y);//打印wprintf(L"%lc", BODY);//指向下一个节点cur = cur->next;}//对贪吃蛇的每个组成都初始化ps->_Status = OK;//游戏的状态ps->_Score = 0;//贪吃蛇累计的总分ps->_pFood = NULL;//指向食物结点的指针ps->_SleepTime = 200;//每走一步休息的时间ps->_FoodWeight = 10;//一个食物的分数//(我们默认蛇开始向右走)ps->_Dir = RIGHT;//描述蛇的方向
}//创建食物
void CreateFood(pSnake ps)
{//初始化int x = 0;int y = 0;
again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x坐标必须是2的倍数//坐标不能和蛇的身体冲突pSnakeNode cur = ps->_pSnake;while (cur){//比较坐标if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//给食物开辟空间pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));//判断if (pFood == NULL){perror("CreateFood()::malloc()");return;}//初始化pFood->x = x;pFood->y = y;ps->_pFood = pFood;//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);}//1. 游戏开始 - 初始化游戏
void GameStart(pSnake ps)
{//设置控制台的大小system("mode con cols=100 lines=30");system("title 贪吃蛇");//光标影藏掉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("1.不能撞墙,不能咬到自己");SetPos(64, 16);printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");SetPos(64, 17);printf("3.F3加速,F4减速");SetPos(64, 18);printf("4.ESC-退出, 空格-暂停游戏");SetPos(64, 20);printf("旧言专用版权");//getchar();
}//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y){return 1;}else{return 0;}
}//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{//头插(把食物当做蛇头)pnext->next = ps->_pSnake;ps->_pSnake = pnext;//打印蛇pSnakeNode cur = ps->_pSnake;while (cur){//找坐标SetPos(cur->x, cur->y);//打印wprintf(L"%lc", BODY);//找下一个节点cur = cur->next;}//释放食物空间free(ps->_pFood);//加分数ps->_Score += ps->_FoodWeight;//创建食物CreateFood(ps);//新创建食物
}//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{//头插pnext->next = ps->_pSnake;ps->_pSnake = pnext;//打印蛇身pSnakeNode cur = ps->_pSnake;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(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 cur = ps->_pSnake->next;while (cur){if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y){//游戏状态为自杀ps->_Status = KILL_BY_SELF;}//找下一个节点cur = cur->next;}
}//蛇的移动
void SnakeMove(pSnake ps)
{//开辟蛇移动到下一个格子的空间pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));//判断if (pNext == NULL){perror("SnakeMove()::malloc()");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 (NextIsFood(ps, pNext)){//吃掉食物EatFood(ps, pNext);}else{//不吃食物NoFood(ps, pNext);}//蛇是否撞墙KillByWall(ps);//蛇是否自杀KillBySelf(ps);}//暂停蛇移动
void Pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}//2. 游戏运行 - 游戏的正常运行过程
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();//循环方向,直到方向正确do{//打印得分SetPos(64, 10);printf("得分:%05d", 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_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 = END_NORMAL;break;}else if (KEY_PRESS(VK_SPACE)){//暂停蛇移动Pause();}else if (KEY_PRESS(VK_F3))//加速{if (ps->_SleepTime >= 80){ps->_SleepTime -= 30;ps->_FoodWeight += 2;}}else if (KEY_PRESS(VK_F4))//减速{if (ps->_SleepTime < 320){ps->_SleepTime += 30;ps->_FoodWeight -= 2;}}//蛇动的时间Sleep(ps->_SleepTime);//蛇的移动SnakeMove(ps);} while (ps->_Status == OK);
}//3. 游戏结束 - 游戏善后(释放资源)
void GameEnd(pSnake ps)
{//判断游戏状态SetPos(20, 12);switch (ps->_Status){case END_NORMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("自杀了,游戏结束\n");break;case KILL_BY_WALL:printf("撞墙了,游戏结束\n");break;}//释放蛇身的结点pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}ps->_pSnake = NULL;}
💧snake.h
#pragma once//包含头文件
#include <locale.h>
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<windows.h>
#include<time.h>
#include<stdbool.h>//定义图案
#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 )//枚举键位
enum  DIRECTION
{UP = 1,//上DOWN,//下LEFT,//左RIGHT//右
};//枚举游戏状态
enum GAME_STATUS
{OK,//正常运行END_NORMAL,//按ESC退出KILL_BY_WALL,KILL_BY_SELF
};//贪吃蛇每个节点的描述
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 DIRECTION _Dir;//描述蛇的方向enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;//游戏开始 - 完成游戏的初始化动作
void GameStart(pSnake ps);//定位坐标
void SetPos(short x, short y);//游戏开始的欢迎界面
void WelComeToGame();//打印地图
void CreateMap();//初始化贪吃蛇
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//游戏的正常运行
void GameRun(pSnake ps);//打印帮助信息
void PrintHelpInfo();//游戏暂定和恢复
void Pause();//蛇的移动
void SnakeMove(pSnake ps);//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext);//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext);//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext);//蛇是否撞墙
void KillByWall(pSnake ps);//蛇是否自杀
void KillBySelf(pSnake ps);//游戏结束后的善后处理
void GameEnd(pSnake ps);
💧test.c
#define _CRT_SECURE_NO_WARNINGS 1//包含头文件
#include"snake.h"void test()
{int ch = 0;do{Snake snake = { 0 };//创建了贪吃蛇//1. 游戏开始 - 初始化游戏GameStart(&snake);//2. 游戏运行 - 游戏的正常运行过程GameRun(&snake);//3. 游戏结束 - 游戏善后(释放资源)GameEnd(&snake);SetPos(20, 18);printf("再来一局吗?(Y/N):");ch = getchar();getchar();// 清理掉\n} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}int main()
{//设置程序适应本地环境setlocale(LC_ALL, "");//设置语言环境srand((unsigned int)time(NULL));//游戏睡眠状态//调用函数test();return 0;
}

🌟结束语

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小说手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/176522.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flutter笔记:绘图示例 - 一个简单的(Canvas )时钟应用

Flutter笔记 绘图示例 - 一个简单的&#xff08;Canvas &#xff09;时钟应用 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_2855…

SOME/IP学习笔记2

1. SOME/IP 协议 SOME/IP目前支持UDP&#xff08;用户传输协议&#xff09;和TCP&#xff08;传输控制协议&#xff09;&#xff0c; PS:UDP和TCP区别如下 TCP面向连接的&#xff0c;可靠的数据传输服务&#xff1b;UDP面向无连接的&#xff0c;尽最大努力的数据传输服务&…

Windows使用ssh远程连接(虚拟机)Linux(Ubuntu)的方法

步骤 1.Windows下载一个SSH客户端软件 要使用SSH连接&#xff0c;当然得先有一个好用的客户端软件才方便。 我这里使用的是WindTerm&#xff0c;一个开源免费的SSH连接工具&#xff0c;用什么软件不是重点。 这里默认你已经生成过SSH的密钥了&#xff0c;如果没有&#xff0c…

OpenHarmony 开发者论坛正式上线,盖楼赢惊喜好礼~

你&#xff0c;是否曾遇到 OpenHarmony 开发难题&#xff0c;却不知找谁解答&#xff1f; 你&#xff0c;是否曾想分享 OpenHarmony 技术&#xff0c;但没有一个官方投稿平台&#xff1f; 你&#xff0c;是否想加入火热的 OpenHarmony 开源项目&#xff0c;却不知如何参与和贡…

【Python大数据笔记_day07_hive中的分区表、分桶表以及一些特殊类型】

分区表 分区表的特点/好处:需要产生分区目录,查询的时候使用分区字段筛选数据,避免全表扫描从而提升查询效率 效率上注意:如果分区表在查询的时候呀没有使用分区字段去筛选数据,效率不变 分区字段名注意:分区字段名不能和原有的字段名重复,因为分区字段名要作为字段拼接到表后…

新版软考高项试题分析精选(三)

请点击↑关注、收藏&#xff0c;本博客免费为你获取精彩知识分享&#xff01;有惊喜哟&#xff01;&#xff01; 1、项目整体管理要综合考虑项目各个相关过程&#xff0c;围绕整体管理特点&#xff0c;以下说法中&#xff0c;&#xff08; &#xff09;是不正确的。 A.项目的…

MDM如何配置数据审批功能

MDM基础数据平台是进行清洗和治理企业的主数据&#xff0c;使企业的主数据具有唯一性、准确性、一致性、及时性&#xff0c;通过主数据数据清洗功能将错误数据和重复数据进行清洗&#xff0c;从而保证企业数据能够做到以上那几点&#xff0c;然后再分发至下游系统&#xff0c;使…

ebSocket connection to ‘wss://xxx.xxxxxxx.xxx/‘ failed:

目录 1&#xff1a;网络连接问题&#xff1a;检查您是否已连接到互联网&#xff0c;您的网络是否稳定。您还可以尝试重置您的Internet连接或切换到另一个网络。 排除方法&#xff1a;直接打开个网址就知道了&#xff0c;这应该不用教了吧 2&#xff1a;防火墙或代理设置&…

【MySQL】对表结构进行增删查改的操作

表的操作 前言正式开始建表查看表show tables;desc xxx;show create table xxx; 修改表修改表名 rename to对表结构进行修改新增一个列 add 对指定列的属性做修改 modify修改列名 change 删除某列 drop 删除表 drop 前言 前一篇讲了库相关的操作&#xff0c;如果你不太懂&…

前端JS解构数组对象

// 3. 对象数组解构const arr [{username: 小明,age: 18,agw:19},{username: 小ha,age: 18,agw:19}]arr.map(item>item.age)//js结构数组对象console.log( arr.map(item>{return {aaa:item.age,bbb:item.username}}))

一家3代37台!海尔智家凭啥赢得一家人信赖?

“我&#xff0c;我母亲、我弟弟、我儿子&#xff0c;这么多年都一直在用海尔家电……”这是上海用户王晓华口中&#xff0c;自己一家3代人与37台海尔家电的故事。 一个家庭几代人始终如一的选择背后&#xff0c;实则是一个企业一如既往将用户需求放在第一位的体现。我们将视野…

098001190、098002190、098101190、098102190比例线圈放大器

0343100001、0344100101、0523100150、0353450101、035303001、0354050101、0473200050、0052010100、0063030100、0172200410、0363150101、0364100601、1027001020、1044112011、1043112010、1128022100、098001190、098002190、098101190、098102190、098201190、098202190、…