小游戏贪吃蛇的实现之C语言版

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-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);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(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;//总分为0
ps->_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;}
}

 好啦!本期贪吃蛇游戏的学习之旅到此结束了!我们下一期再一起学习吧!

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

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

相关文章

单机三pxc节点集群,+docker-haproxy2.0负载均衡实现

一.下载 https://www.haproxy.org/download/2.0/src/haproxy-2.0.5.tar.gz 或者在这里下载&#xff08;下面需要的各个配置文件都有&#xff09;&#xff1a; https://download.csdn.net/download/cyw8998/89170129 二.编写文件&#xff0c;制作docker镜像 1.Dockerfile&a…

四款一键智能改写工具,为你轻松改出爆款文章

四款一键智能改写工具&#xff0c;为你轻松改出爆款文章&#xff01;当今&#xff0c;虽然内容创作变得非常重要。但是&#xff0c;有时候创作灵感可能枯竭&#xff0c;或者需要对已有内容进行改写以增加独特性。这时候&#xff0c;一键智能改写工具成为了创作中的一种强大的辅…

LeetCode 课程表二(拓扑排序+Python)

使用桶排序算法中的kahn&#xff08;卡恩&#xff09;算法&#xff0c;也可以使用dfs。 这里使用卡恩算法&#xff0c;主要维护一个列表cnt&#xff0c;cnt【i】表示能到达节点i的边&#xff0c;比如说&#xff1a;a到c有一条边&#xff0c;b到c有一条边&#xff0c;那么cnt【…

信息流广告大行其是,微博回望“原生”的初心

摘要&#xff1a;有流量的地方&#xff0c;就当有原生信息流广告 信息流广告&#xff0c;自2006年Facebook推出后就迅速火遍全球数字营销界&#xff0c;被誉为实现了广告主、用户、媒体平台三赢。特别是随着OCPM/OCPX大放异彩&#xff0c;信息流广告几乎成为广告主的必选项&…

Print Conductor 文档批量打印工具 v9.0.2312

网盘下载 Print Conductor 是 Windows 上一款功能强大的文档批量打印工具&#xff0c;通过该软件可以快速的帮用户批量处理打印PDF文件、协议、文档、图纸、演示文稿、文本文件等&#xff0c;完美的支持PDF、DOC、JPG、PNG、SNP、PSD、MSG、WRI、WPS、RTF、TXT、XLS、PPT、PPS、…

在Linux系统中,禁止有线以太网使用NTP服务器进行时间校准的几种方法

目录标题 方法 1&#xff1a;修改NTP配置以禁止所有同步方法 2&#xff1a;通过网络配置禁用NTP同步方法 3&#xff1a;禁用NTP服务 在Linux系统中&#xff0c;如果想要禁止有线以太网使用NTP服务器进行时间校准&#xff0c;可以通过以下几种方法之一来实现&#xff1a; 方法 …

Java中的对象

什么是类和对象 在Java中类是物以类聚&#xff0c;分类的思维模式&#xff0c;思考问题首先会解决问题需要哪些分类&#xff0c;然后对这些类进行单独思考&#xff0c;最后才是对某分类下的细节进行单独思考 面向对象适合处理复杂问题适合处理需要多人协作的问题 在Java中面向…

【状态机dp 动态规划】100290. 使矩阵满足条件的最少操作次数

本文涉及知识点 动态规划汇总 状态机dp LeetCode100290. 使矩阵满足条件的最少操作次数 给你一个大小为 m x n 的二维矩形 grid 。每次 操作 中&#xff0c;你可以将 任一 格子的值修改为 任意 非负整数。完成所有操作后&#xff0c;你需要确保每个格子 grid[i][j] 的值满足…

【Qt 学习笔记】Qt常用控件 | 显示类控件 | Label的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 显示类控件 | Label的使用及说明 文章编号&#xff1a;Q…

Opencv Python图像处理笔记一:图像、窗口基本操作

文章目录 前言一、输入输出1.1 图片读取显示保存1.2 视频读取保存1.3 文件读取保存 二、GUI2.1 窗口2.2 轨迹条2.3 画图2.4 鼠标回调 三、图像入门操作3.1 颜色空间转化3.2 通道分离合并3.3 添加边框3.4 算数操作 四、二值化4.1 普通4.2 自适应4.3 Otsu 参考 前言 随着人工智能…

mysql基础3——创建和修改数据表

创建数据表 创建一个表&#xff08;importtype有默认值1&#xff09;并插入一条数据&#xff08;importtype字段没有指定值&#xff09; 约束 默认约束&#xff08;把设置的默认值自动赋值给字段&#xff09; create table demo.importhead(listnum int,supplied int,stock…

C++进修——C++基础入门

初识C 书写HelloWorld #include <iostream> using namespace std;int main() {cout << "HelloWorldd" << endl;system("pause");return 0; }注释 作用&#xff1a;在代码中加一些说明和解释&#xff0c;方便自己或其他程序员阅读代码…

docker打包部署自己的应用

docker部署应用 当谈及使用 Docker 进行容器化部署应用时&#xff0c;可以想象它是一个能够将整个应用程序及其所有依赖项打包成一个独立、可移植的容器的工具。这个容器不仅包含了应用代码&#xff0c;还包括了操作系统、运行时环境以及所有依赖的库和组件。这样一来&#xf…

双工结构(duplex construction)

参考文献&#xff1a; [BDPV11] Bertoni G, Daemen J, Peeters M, et al. Duplexing the sponge: single-pass authenticated encryption and other applications[C]//Selected Areas in Cryptography: 18th International Workshop, SAC 2011, Toronto, ON, Canada, August 1…

mybatis创建入门流程体验

mysql数据库中建表 drop table if exists tb_user;create table tb_user(id int primary key auto_increment,username varchar(20),password varchar(20),gender char(1),addr varchar(30) );INSERT INTO tb_user VALUES (1, zhangsan, 123, 男, 北京); INSERT INTO tb_user …

星域社区原版APP源码/社区交友App源码/动态圈子群聊php源码

简介 初始版本是由RuleAPP规则之树开发的&#xff0c;而星域社区则是在此基础上进行了二次开发和美化。作者花了近一年的时间来打磨它&#xff0c;现在即将推出Pro版。如果你只想免费使用的话&#xff0c;可以使用原始的RuleAPP版本。但是&#xff0c;如果你想要获得更好的美观…

【YOLOv9】实战二:手把手教你使用TensorRT实现YOLOv9实时目标检测(含源码)

‍‍&#x1f3e1;博客主页&#xff1a; virobotics(仪酷智能)&#xff1a;LabVIEW深度学习、人工智能博主 &#x1f384;所属专栏&#xff1a;『LabVIEW深度学习实战』 &#x1f4d1;上期文章&#xff1a;『【YOLOv9】实战一&#xff1a;在 Windows 上使用LabVIEW OpenVINO工具…

CTF工具处理(2)--HackBar报错

来源&#xff1a;HackBar插件绕许可_hackbar许可证-CSDN博客 报错页面&#xff1a; 工具报错原因&#xff1a; 版本升级&#xff0c;需要收费。 破解&#xff1a; 1.下载文件&#xff08;去网上找破解HackBar&#xff09; 2.添加附件&#xff1a; 3.重启&#xff1a; 按F12就…

引领4G拾音新时代:DuDuTalk双定向拾音设备上市,助力现场管理步入智能化

近日&#xff0c;继DuDuTalk的4G智能拾音工牌&#xff08;挂牌和胸牌&#xff09;之后&#xff0c;赛思云科技在线下沟通场景智能语音采集方案领域的又一突破性产品4G双定向桌面拾音终端全新上市。 该产品是面向营业网点、市政大厅、医疗诊室、售票窗口、贵宾室等环境的柜台服…

【工具-pip】

工具-pip ■ pip-工具■ pip-安装■ pip-卸载■ pip-帮助■ pip- 批量安装库■ 批量卸载库■ 提高pip下载速度 ■ pip-工具 pip 是 Python 标准库中的一个包&#xff0c;这个包是用来管理 Python 第三方库。 通用的 Python 包管理工具。提供了对 Python 包的查找、下载、安装、…