找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:C语言
游戏演示:
贪吃蛇游戏演示
目录
游戏前期准备:
设置控制台相关的信息
GetStdHandle
GetConsoleCursorInfo
SetConsoleCursorInfo
SetConsoleCursorPosition
GetAsyncKeyState
贪吃蛇游戏设计与分析
本地化
地图,食物和蛇身的设计
GameStart()—— 游戏的初始化
打印欢迎界面
绘制贪吃蛇地图
初始化贪吃蛇
初始化食物
GameRun()——游戏的运行
打印右侧的帮助信息
贪吃蛇的相关运行信息
GameOver()——游戏的结束(善后工作)
贪吃蛇源码
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
游戏前期准备:
本次实现贪吃蛇会使用到的一些Win32 API知识,接下来我们就学习一下。
背景介绍:
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
设置控制台相关的信息
平常我们运行起来的黑框程序其实就是控制台程序(如下图所示)。
我们可以使用cmd命令来设置控制台窗口的长宽。例如:设置控制台窗口的大小,30行,100列。
//格式: 列 行mode con cols=100 lines=30
注意:
1. 列和行在赋值时,不能带有空格。例如:cols = 100,这就是不行的,没有影响到控制台的大小。
2. 使用这个命令之前,需要把这个控制台改为让Windows决定或者Windows 控制台主机
演示:
改变VS编译器的控制台
3. 使用system函数所需要包含的头文件既可以是stdlib.h,也可以是Windows.h(不分大小写的,因此可以使用"windows.h"、"WINDOWS.H"或者"Windows.h"等形式来引用该头文件。不过,一般约定使用"Windows.h"的形式来引用该头文件,以保持代码的一致性和可读性。)
下面就来使用这个来改变控制台的大小。
从上面的结果来看:行列对应不一致。没错,一行的宽度是一列的宽度的二倍。
也可以通过命令设置控制台窗口的名字:
//格式:title 要修改的名字
注意:在更改之后要观察到的话,就不能让程序运行结束,也就是说只能在程序运行期间才能够观察的到。
控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。
COORD类型的声明:
typedef struct _COORD { SHORT X;//短整型 SHORT Y;} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };
GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
标准输入是指键盘,标准输出和标准错误是指屏幕。
这个句柄就类似一个遥控器,可以通过句柄来操作标准设备。而我们想要操作标准设备也得通过GetStdHandle这个函数来获得句柄。再通过句柄来操作。
HANDLE GetStdHandle(DWORD nStdHandle);
例如:
HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
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;
举例:
HANDLE hOutput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo = {0};//获取控制台光标信息GetConsoleCursorInfo(hOutput, &CursorInfo);
SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);
举例:
//获取标准输出的句柄HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//定义一个存放光标信息的结构体CONSOLE_CURSOR_INFO CursorInfo = {0};//获取控制台光标信息存放到这个结构体中GetConsoleCursorInfo(hOutput, &CursorInfo);//隐藏控制台光标CursorInfo.bVisible = false; //把控制台光标大小调到最大CursorInfo.dwSize = 100;//设置控制台光标状态(按照上面的设置调)SetConsoleCursorInfo(hOutput, &CursorInfo);
SetConsoleCursorPosition
设置控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的变量中,调 用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD pos);
举例:
//改变光标的位置COORD pos = { 10, 5};HANDLE hOutput = NULL;//获取标准输出的句柄hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
注意:这个pos的位置设置有可能不一定会成功。
GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1 。下面是虚拟键代码虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn虚拟键码
举例:检测数字键是否被摁过。(字母键上面的数字键)
//判断一个键是否被摁过//如果被摁过结果就是1,否则就是0#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)int main(){while (1){if (KEY_PRESS(0x30)){printf("%d\n", 0);}else if (KEY_PRESS(0x31)){printf("%d\n", 1);}else if (KEY_PRESS(0x32)){printf("%d\n", 2);}else if (KEY_PRESS(0x33)){printf("%d\n", 3);}else if (KEY_PRESS(0x34)){printf("%d\n", 4);}else if (KEY_PRESS(0x35)){printf("%d\n", 5);}else if (KEY_PRESS(0x36)){printf("%d\n", 6);}else if (KEY_PRESS(0x37)){printf("%d\n", 7);}else if (KEY_PRESS(0x38)){printf("%d\n", 8);}else if (KEY_PRESS(0x39)){printf("%d\n", 9);}}}
贪吃蛇游戏设计与分析
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞自身死亡
• 计算得分
• 蛇身加速、减速
• 暂停游戏
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
加入了宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
<locale.h>本地化
setlocale函数原型:
char* setlocale (int category, const char* locale);
类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏, 指定一个类项:
• LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的行为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境。
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。 setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。 C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。 在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。 当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
setlocale(LC_ALL, "");//切换到本地环境(注意这里双引号里不能有空格)
那如果想在屏幕上打印宽字符,怎么打印呢? 宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应 wprintf() 的占位符为 %ls 。
举例:
#include <stdio.h>#include <locale.h>int main(){ //切换到本地环境 setlocale(LC_ALL, ""); wprintf(L"%s\n", L"我要学编程"); wchar_t wc1 = L'我'; wchar_t wc2 = L'要'; wchar_t wc3 = L'学'; wchar_t wc4 = L'编'; wchar_t wc5 = L'程'; wprintf(L"%lc", wc1); wprintf(L"%lc", wc2); wprintf(L"%lc", wc3); wprintf(L"%lc", wc4); wprintf(L"%lc", wc5); return 0;}
地图,食物和蛇身的设计
我们假设实现一个棋盘27行,58列的棋盘,再围绕地图画出墙。
由于1行的宽度是一列宽度的二倍,就可以按照上面的样式绘制地图。这些都用宽字符来打印。
初始化状态:假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。 注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中, 另外一半在墙外的现象。 关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。食物的x坐标也要是2的倍数。
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信 息,那么蛇的每一节其实就是链表的一个节点。每个节点只要记录好蛇身节点在地图上的坐标就行, 所以蛇节点结构如下:
//蛇身的节点类型typedef struct SnakeNode{//坐标int x;int y;//指向下一个节点的指针struct SnakeNode* next;}SnakeNode, * pSnakeNode;
还得创建一些变量:指向蛇头的指针,初始时蛇的速度,蛇的方向,食物,食物分数,总分,贪吃蛇的状态。但是这些都比较麻烦,我们就创建一个贪吃蛇的结构体来管理这些变量。
//贪吃蛇typedef struct Snake{pSnakeNode _pSnake;//指向蛇头的指针pSnakeNode _pFood;//指向食物的指针DIRECTION _dir;//蛇的方向GAME_STATE _state;//游戏的运行状态int _FoodWeight;//一个食物的分数int _score;//总分数int _SleepTime;//休眠时间,时间越短,速度越快,时间越长,速度越慢}Snake, * pSnake;
蛇的方向有四种:上,下,左,右。我们就可以枚举出来。
//蛇的方向typedef enum DIRECTION{UP = 1,//上DOWN,//下LEFT,//左RIGHT//右}DIRECTION;
游戏的运行状态:正常运行,正常退出,撞墙死亡,撞到自己死亡。
//游戏的状态typedef enum GAME_STATE{OK,//正常运行KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己END_NORMAL//正常退出}GAME_STATE;
接下来就是正式的游戏设计。
首先分装三个大的函数。
GameStart()—— 游戏的初始化
初始化的内容:1,打印欢迎界面 2,绘制贪吃蛇地图 3,初始化贪吃蛇和食物 。
打印欢迎界面
首先得分装一个函数用来定位坐标。
//定位光标void SetPos(short x, short y){HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄COORD pos = { x, y };SetConsoleCursorPosition(houtput, pos);//设置光标的位置}
system("pause")是一个用于暂停控制台的函数。它会在控制台输出一个提示信息,等待用户按下任意键后才会继续执行程序。
system("pause");
由上面的界面切换到下面这个界面,就需要用到一个清理控制台界面的函数。
system("cls");//用于清理当前控制台的界面所有信息
打印欢迎界面:
//打印欢迎界面void WelcomToGame(){//首先得定位光标SetPos(39, 10);printf("欢迎来到贪吃蛇小游戏\n");SetPos(40, 15);system("pause");//暂停system("cls");//清理屏幕SetPos(28, 10);printf("用↑.↓.←.→ 来控制蛇的移动!摁F3加速!摁F4减速!\n");SetPos(38, 11);printf("游戏即将开始,请做好准备!\n");SetPos(40, 15);system("pause");system("cls");}
绘制贪吃蛇地图
上面这个就是我们要绘制的地图。值得一提的是:这个方块是宽字符,而使用wprintf来打印宽字符就得先将C语言环境转化到本地环境。至于后面的打印,就是通过定位来循环打印。
先打印上下两行,再打印左右两列。
上一行的坐标是(2*i,0),i 的范围是0~28。 下一行的坐标是(2*i,25),i 的范围是0~28。
左一列的坐标是(0,i),i 的范围是1~25。 右一列的坐标是(56,i),i 的范围是1~25。
注意:
1. 一行的打印就相当于是打印了一列中的一个,因此用总列数-2就是我们要打印的列坐标。
2. 列在打印时,需要先定位好坐标。因为打印的顺序是默认从左到右的;而我们是要实现从上到下的打印。
#define WALL L'□'//绘制地图void CreatMap(){//打印上体墙for (int i = 0; i <= 28; i++){wprintf(L"%lc", WALL);}SetPos(0, 26);//定位到下体墙的位置//打印下体墙for (int i = 0; i <= 28; i++){wprintf(L"%lc", WALL);}//打印左体墙for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);//用换行不行,因此加不加换行无所谓}//打印右体墙for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);//用换行不行,因此加不加换行无所谓}}
初始化贪吃蛇
既然要初始化蛇,首先就得有蛇,而蛇是用节点串起来的。因此我们只要创建5个节点,并且传入我们想要的坐标(我们想要蛇出现在哪个位置,这个位置最好是固定的,这里也是采用固定的位置。),最后把这些节点串起来就行了。
//创建一条蛇pSnakeNode pcur = NULL;pSnakeNode prev = NULL;for (int i = 0; i < 5; i++){pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));if (tmp == NULL){perror("InitSnake():malloc:");return;}pcur = tmp;pcur->x = POS_X + 2 * i;pcur->y = POS_Y;pcur->next = NULL;if (ps->_pSnake == NULL){//直接插入即可ps->_pSnake = pcur;}else{//尾插if (prev != NULL)prev->next = pcur;}prev = pcur;}
打印蛇(打印整个链表)
#define BODY L'●'#define HEAD L'◆'//开始在控制台上打印蛇int count = 0;while (pcur){SetPos(pcur->x, pcur->y);//定位if (count == 0){count++;wprintf(L"%lc", HEAD);//打印蛇头}else{wprintf(L"%lc", BODY);//打印蛇身}pcur = pcur->next;}
设置贪吃蛇的属性。
//设置贪吃蛇的属性ps->_dir = LEFT;//初始时蛇的方向向左ps->_FoodWeight = 50;//一个食物50分ps->_score = 0;//总分为0ps->_SleepTime = 200;//单位是毫秒ps->_state = OK;//正常运行
因为这里的蛇头是在最左边,所以这个蛇的初始方向不能是向右走,除此之外都可以。
初始化食物
初始化食物其实就是创建一个食物并且打印出来。
这个食物为了能够被蛇给吃掉,x坐标也必须是2的倍数,并且这个食物应该是要随机生成的,还要在这个墙体中。
int x = 0;int y = 0;//随机创建食物(食物的x坐标必须是2的倍数,因此要判断)again:do{ //为了食物出现在墙内x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2);//食物的坐标不能和蛇身冲突pSnakeNode pcur = ps->_pSnake;//开始寻找看看是否与蛇身冲突while (pcur){if (pcur->x == x && pcur->y == y){goto again;//如果冲突了,就要回炉重造}pcur = pcur->next;}//开始创建食物的节点pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));if (tmp == NULL){perror("CreatFood():malloc:");return;}tmp->x = x;tmp->y = y;tmp->next = NULL;ps->_pFood = tmp;
打印食物
//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);
GameRun()——游戏的运行
1,打印帮助手册 2,贪吃蛇的相关运行信息 3,判断贪吃蛇是否死亡
上图就是游戏运行时的界面。
打印右侧的帮助信息
//打印帮助信息void PrintHelpInfo(){SetPos(68, 10);printf("小提示:");SetPos(68, 13);printf("不能穿墙,不能咬到自己!");SetPos(68, 14);printf("用↑.↓.←.→ 来控制蛇的移动!");SetPos(68, 15);printf("摁F3加速!摁F4减速!");SetPos(68, 16);printf("加速将增加单个食物的分数!");SetPos(68, 17);printf("减速将减少单个食物的分数!");SetPos(68, 18);printf("摁Esc退出游戏!摁空格暂停游戏!");}
贪吃蛇的相关运行信息
贪吃蛇要运行起来,就得需要我们摁键来实现贪吃蛇的走动。所以接下来就是判断哪个键是否摁过来判断蛇的走向。而只要是蛇的状态不等于OK时,此时就不需要再走了。
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//接下来就是通过按键来判断贪吃蛇的运行状态do{//打印分数显示SetPos(68, 7);printf("当前总分数:%08d", ps->_score);SetPos(68, 8);printf("当前食物分数:%02d", 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_SPACE)){Pause();//暂停}else if (KEY_PRESS(VK_ESCAPE)){//退出游戏ps->_state = END_NORMAL;}else if (KEY_PRESS(VK_F3)){//加速(减少休眠时间)if (ps->_SleepTime > 80)//设置为4档速度{ps->_SleepTime -= 30;ps->_FoodWeight += 10;}}else if (KEY_PRESS(VK_F4)){//减速if (ps->_FoodWeight > 10)//设置为4档速度{ps->_SleepTime += 20;ps->_FoodWeight -= 10;}}//蛇开始走//走一步,就休息一下SnakeMove(ps);//蛇走一步的过程Sleep(ps->_SleepTime);//检测是否撞墙KillByWall(ps);//检测是否撞到自己KillBySelf(ps); //通过总分数来判断游戏是否结束 if (ps->_score > 30000) { SetPos(20, 13); printf("恭喜你!成功通关!"); }} while (ps->_state == OK);//只有蛇的状态正常才走
暂停的实现就只需要系统一直处于休眠状态。
void Pause(){while (1){//休眠200毫秒Sleep(200);//这个只能放到休眠的后面if (KEY_PRESS(VK_SPACE)){break;}}}
当进入这个暂停函数就需要休眠,即使再次摁了空格键,也得先休眠一下。
蛇在走的时候,就是根据我们摁的键位来判断蛇头应该出现在哪个地方。再通过链接蛇头的下一个位置和蛇身以及释放蛇身的尾节点。还有一个小细节:如果蛇头下一个位置是食物的话,就要吃掉食物,并且再创建一个食物,而如果不是食物的话,就只需要按照上面的步骤走就行了。因此就得先判断是否为食物。
//蛇走一步的过程void SnakeMove(pSnake ps){//创建一个节点来存放蛇要走的下一个节点pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));if (next == NULL){perror("SnakeMove():malloc:");return;}//根据方向来判断蛇是怎么走的switch (ps->_dir){case UP:next->x = ps->_pSnake->x;next->y = ps->_pSnake->y - 1;break;case DOWN:next->x = ps->_pSnake->x;next->y = ps->_pSnake->y + 1;break;case LEFT:next->x = ps->_pSnake->x - 2;next->y = ps->_pSnake->y;break;case RIGHT:next->x = ps->_pSnake->x + 2;next->y = ps->_pSnake->y;break;}//判断蛇走的下一个节点是不是食物if (NextIsFood(next, ps)){ //是食物就吃掉食物EatFood(next, ps);}else{ //不是就不吃NoFood(next, ps);}}
下一个位置是食物
//下一个位置是食物,就可以吃掉void EatFood(pSnakeNode next, pSnake ps){//把这个节点(就是食物节点)头插到蛇身就行ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;//释放掉这个节点(因为创建了两个节点:一个食物节点,一个蛇头的下一个节点)free(next);next = NULL;//打印蛇身pSnakeNode pcur = ps->_pSnake;int count = 0;while (pcur){SetPos(pcur->x, pcur->y);//定位if (count == 0){count++;wprintf(L"%lc", HEAD);//打印蛇头}else{wprintf(L"%lc", BODY);//打印蛇身}pcur = pcur->next;} //分数的增加ps->_score += ps->_FoodWeight;//重新创建食物CreatFood(ps);}
下一个位置不是食物
//下一个位置不是食物void NoFood(pSnakeNode next, pSnake ps){//把下一个位置的节点头插到蛇身next->next = ps->_pSnake;ps->_pSnake = next;//把蛇身最后一个节点的空间释放掉,顺便打印蛇身pSnakeNode pcur = ps->_pSnake;int count = 0;while (pcur->next->next != NULL){//遍历时,可以直接打印SetPos(pcur->x, pcur->y);if (count == 0){count++;wprintf(L"%lc", HEAD);}else{wprintf(L"%lc", BODY);}pcur = pcur->next;}//把最后一个节点的位置打印成空格SetPos(pcur->next->x, pcur->next->y);printf(" ");free(pcur->next);pcur->next = NULL;}
如果不把蛇身的尾节点的位置打印成空格,那么上一次的痕迹就不会被消除。 会导致蛇身一直变长。 走一步,再休息两百毫秒,可以让我们有时间来判断贪吃蛇下一步需怎么走,如果不休息就会直接撞墙。
检测是否撞墙只需要检测蛇头是否撞墙,因为蛇身的每一个节点是重复执行蛇头的操作。蛇头不撞墙那么蛇身就没有机会撞墙,如果蛇头撞墙,那么就说明这个蛇撞墙了。
//检测是否撞墙void KillByWall(pSnake ps){//只要判断蛇头是否碰到墙就可以了if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56|| ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_state = 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->_state = KILL_BY_SELF;//改变蛇的状态即可break;}pcur = pcur->next;}}
GameOver()——游戏的结束(善后工作)
善后也就是把蛇身的节点释放掉,并告诉玩家游戏结束的原因。
//结束游戏(善后工作)void GameOver(pSnake ps){SetPos(20, 13);switch (ps->_state){case KILL_BY_WALL:printf("很遗憾!撞墙死亡!");break;case KILL_BY_SELF:printf("很遗憾!撞到自己死亡!");break;case END_NORMAL:printf("玩家主动结束游戏!");break;}//释放蛇身链表pSnakeNode prev = ps->_pSnake;pSnakeNode pcur = ps->_pSnake;while (pcur){prev = pcur->next;free(pcur);pcur = prev;}}
贪吃蛇源码
Snake.h
#include <stdio.h>#include <stdlib.h>#include <Windows.h>#include <locale.h>#include <stdbool.h>#include <time.h>#define POS_X 24#define POS_Y 5#define WALL L'□'#define BODY L'●'#define FOOD L'★'#define HEAD L'◆'#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//游戏的状态typedef enum GAME_STATE{OK,//正常运行KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞到自己END_NORMAL//正常退出}GAME_STATE;//蛇的方向typedef enum DIRECTION{UP = 1,//上DOWN,//下LEFT,//左RIGHT//右}DIRECTION;//蛇身的节点类型typedef struct SnakeNode{//坐标int x;int y;//指向下一个节点的指针struct SnakeNode* next;}SnakeNode, * pSnakeNode;//贪吃蛇typedef struct Snake{pSnakeNode _pSnake;//指向蛇头的指针pSnakeNode _pFood;//指向食物的指针DIRECTION _dir;//蛇的方向GAME_STATE _state;//游戏的运行状态int _FoodWeight;//一个食物的分数int _score;//总分数int _SleepTime;//休眠时间,时间越短,速度越快,时间越长,速度越慢}Snake, * pSnake;//定位光标void SetPos(short x, short y);//初始化游戏void GameStart(pSnake ps);//打印欢迎界面void WelcomToGame();//绘制地图void CreatMap();//初始化蛇void InitSnake(pSnake ps);//创建食物void CreatFood(pSnake ps);//游戏运行void GameRun(pSnake ps);//蛇走一步的过程void SnakeMove(pSnake ps);//判断蛇要走的下一个节点是否为食物int NextIsFood(pSnakeNode next, pSnake ps);//下一个位置是食物,就可以吃掉void EatFood(pSnakeNode next, pSnake ps);//下一个位置不是食物void NoFood(pSnakeNode next, pSnake ps);//检测是否撞墙void KillByWall(pSnake ps);//检测是否撞到自己void KillBySelf(pSnake ps); //结束游戏(善后工作)void GameOver(pSnake ps);
Snake.c
#include "Snake.h"//定位光标void SetPos(short x, short y){HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄COORD pos = { x, y };SetConsoleCursorPosition(houtput, pos);//设置光标的位置}//打印欢迎界面void WelcomToGame(){//首先得定位光标SetPos(39, 10);printf("欢迎来到贪吃蛇小游戏\n");SetPos(40, 15);system("pause");//暂停system("cls");//清理屏幕SetPos(28, 10);printf("用↑.↓.←.→ 来控制蛇的移动!摁F3加速!摁F4减速!\n");SetPos(38, 11);printf("游戏即将开始,请做好准备!\n");SetPos(40, 15);system("pause");system("cls");}//绘制地图void CreatMap(){//打印上体墙for (int i = 0; i <= 28; i++){wprintf(L"%lc", WALL);}SetPos(0, 26);//定位到下体墙的位置//打印下体墙for (int i = 0; i <= 28; i++){wprintf(L"%lc", WALL);}//打印左体墙for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//打印右体墙for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}}//初始化蛇void InitSnake(pSnake ps){//创建一条蛇pSnakeNode pcur = NULL;pSnakeNode prev = NULL;for (int i = 0; i < 5; i++){pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));if (tmp == NULL){perror("InitSnake():malloc:");return;}pcur = tmp;pcur->x = POS_X + 2 * i;pcur->y = POS_Y;pcur->next = NULL;if (ps->_pSnake == NULL){//直接插入即可ps->_pSnake = pcur;}else{//尾插if (prev != NULL)prev->next = pcur;}prev = pcur;}pcur = ps->_pSnake;//开始在控制台上打印蛇int count = 0;while (pcur){SetPos(pcur->x, pcur->y);//定位if (count == 0){count++;wprintf(L"%lc", HEAD);//打印蛇头}else{wprintf(L"%lc", BODY);//打印蛇身}pcur = pcur->next;}//设置贪吃蛇的属性ps->_dir = LEFT;//初始时蛇的方向向左ps->_FoodWeight = 50;//一个食物50分ps->_score = 0;//总分为0ps->_SleepTime = 200;//单位是毫秒ps->_state = OK;//正常运行}//创建食物void CreatFood(pSnake ps){int x = 0;int y = 0;//随机创建食物(食物的x坐标必须是2的倍数,因此要判断)again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2);//食物的坐标不能和蛇身冲突pSnakeNode pcur = ps->_pSnake;//开始寻找看看是否与蛇身冲突while (pcur){if (pcur->x == x && pcur->y == y){goto again;}pcur = pcur->next;}//开始创建食物的节点pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));if (tmp == NULL){perror("CreatFood():malloc:");return;}tmp->x = x;tmp->y = y;tmp->next = NULL;ps->_pFood = tmp;//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);}//初始化游戏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);//设置光标状态//打印欢迎界面和功能介绍WelcomToGame();//绘制地图CreatMap();//初始化蛇InitSnake(ps);//创建食物CreatFood(ps);}//打印帮助信息void PrintHelpInfo(){SetPos(68, 10);printf("小提示:");SetPos(68, 13);printf("不能穿墙,不能咬到自己!");SetPos(68, 14);printf("用↑.↓.←.→ 来控制蛇的移动!");SetPos(68, 15);printf("摁F3加速!摁F4减速!");SetPos(68, 16);printf("加速将增加单个食物的分数!");SetPos(68, 17);printf("减速将减少单个食物的分数!");SetPos(68, 18);printf("摁Esc退出游戏!摁空格暂停游戏!");}void Pause(){while (1){//休眠200毫秒Sleep(200);//这个只能放到休眠的后面if (KEY_PRESS(VK_SPACE)){break;}}}//判断蛇要走的下一个节点是否为食物int NextIsFood(pSnakeNode next, pSnake ps){return ((next->x == ps->_pFood->x) && (next->y == ps->_pFood->y));}//下一个位置是食物,就可以吃掉void EatFood(pSnakeNode next, pSnake ps){//把这个节点(就是食物节点)头插到蛇身就行ps->_pFood->next = ps->_pSnake;ps->_pSnake = ps->_pFood;//释放掉这个节点(因为有两个节点)free(next);next = NULL;//打印蛇身pSnakeNode pcur = ps->_pSnake;int count = 0;while (pcur){SetPos(pcur->x, pcur->y);//定位if (count == 0){count++;wprintf(L"%lc", HEAD);//打印蛇头}else{wprintf(L"%lc", BODY);//打印蛇身}pcur = pcur->next;}ps->_score += ps->_FoodWeight;//重新创建食物CreatFood(ps);}//下一个位置不是食物void NoFood(pSnakeNode next, pSnake ps){//把下一个位置的节点头插到蛇身next->next = ps->_pSnake;ps->_pSnake = next;//把蛇身最后一个节点的空间释放掉,顺便打印蛇身pSnakeNode pcur = ps->_pSnake;int count = 0;while (pcur->next->next != NULL){//遍历时,可以直接打印SetPos(pcur->x, pcur->y);if (count == 0){count++;wprintf(L"%lc", HEAD);}else{wprintf(L"%lc", BODY);}pcur = pcur->next;}//把最后一个节点的位置打印成空格SetPos(pcur->next->x, pcur->next->y);printf(" ");free(pcur->next);pcur->next = NULL;}//蛇走一步的过程void SnakeMove(pSnake ps){//创建一个节点来存放蛇要走的下一个节点pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));if (next == NULL){perror("SnakeMove():malloc:");return;}//根据方向来判断蛇是怎么走的switch (ps->_dir){case UP:next->x = ps->_pSnake->x;next->y = ps->_pSnake->y - 1;break;case DOWN:next->x = ps->_pSnake->x;next->y = ps->_pSnake->y + 1;break;case LEFT:next->x = ps->_pSnake->x - 2;next->y = ps->_pSnake->y;break;case RIGHT:next->x = ps->_pSnake->x + 2;next->y = ps->_pSnake->y;break;}//判断蛇走的下一个节点是不是食物if (NextIsFood(next, ps)){EatFood(next, ps);}else{NoFood(next, ps);}}//检测是否撞墙void KillByWall(pSnake ps){//只要判断蛇头是否碰到墙就可以了if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56|| ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_state = 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->_state = KILL_BY_SELF;break;}pcur = pcur->next;}}//游戏运行void GameRun(pSnake ps){//先打印帮助信息PrintHelpInfo();//接下来就是通过按键来判断贪吃蛇的运行状态do{//打印分数显示SetPos(68, 7);printf("当前总分数:%05d", ps->_score);SetPos(68, 8);printf("当前食物分数:%02d", 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_SPACE)){Pause();//暂停}else if (KEY_PRESS(VK_ESCAPE)){//退出游戏ps->_state = END_NORMAL;}else if (KEY_PRESS(VK_F3)){//加速(减少休眠时间)if (ps->_SleepTime > 80)//设置为4档速度{ps->_SleepTime -= 30;ps->_FoodWeight += 10;}}else if (KEY_PRESS(VK_F4)){//减速if (ps->_FoodWeight > 10){ps->_SleepTime += 20;ps->_FoodWeight -= 10;}}//蛇开始走//走一步,就休息一下SnakeMove(ps);//蛇走一步的过程Sleep(ps->_SleepTime);//检测是否撞墙KillByWall(ps);//检测是否撞到自己KillBySelf(ps);if (ps->_score > 30000){SetPos(20, 13);printf("恭喜你!成功通关!");break;}} while (ps->_state == OK);}//结束游戏(善后工作)void GameOver(pSnake ps){SetPos(20, 13);switch (ps->_state){case KILL_BY_WALL:printf("很遗憾!撞墙死亡!");break;case KILL_BY_SELF:printf("很遗憾!撞到自己死亡!");break;case END_NORMAL:printf("玩家主动结束游戏!");break;}//释放蛇身链表pSnakeNode prev = ps->_pSnake;pSnakeNode pcur = ps->_pSnake;while (pcur){prev = pcur->next;free(pcur);pcur = prev;}}
test.c
#include "Snake.h"void test(){//创建贪吃蛇Snake snake = { 0 };//初始化游戏//1.游戏相关界面的打印//2.贪吃蛇与食物的打印GameStart(&snake);//游戏运行GameRun(&snake);//结束游戏(善后工作)GameOver(&snake);}int main(){//适配本地化环境setlocale(LC_ALL, ""); //生成随机种子srand((unsigned int)time(NULL));//测试贪吃蛇test(); //为了格式更好看SetPos(0, 27);return 0;}
好啦!本期贪吃蛇游戏的学习之旅到此结束了!我们下一期再一起学习吧!