【C语言】基于C语言实现的贪吃蛇游戏
🔥个人主页:大白的编程日记
🔥专栏:C语言学习之路
文章目录
- 【C语言】基于C语言实现的贪吃蛇游戏
- 前言
- 一.最终实现效果
- 一.Win32 API介绍
- 1.1Win32 API
- 1.2控制台程序
- 1.3控制台屏幕上的坐标COORD
- 1.4GetStdHandle
- 1.5GetConsoleCursorInfo
- 1.6SetConsoleCursorInfo
- 1.7SetConsoleCursorPosition
- 1.8GetAsyncKeyState
- 二.C语言的国际化
- 2.1国际化
- 2.2<locale.h>本地化
- 2.3类项
- 2.4setlocale函数
- 2.5宽字符的打印
- 三.思路分析
- 四.GameStar函数
- 4.1设置控制台信息
- 4.2欢迎界面的打印
- 4.3地图的绘制
- 4.4初始化贪吃蛇
- 五.GameRun函数
- 5.1提示信息函数
- 5.2打印分数
- 5.3检测键值
- 5.4蛇的移动
- 5.5检测是否撞墙
- 六.GameEnd函数
- 七.游戏主体设计
- 八.源码
- 后言
前言
哈喽,各位小伙伴大家好!今天给大家带来的是使用C语言实现的贪吃蛇小游戏。也是检验C语言是否学好的试金石。话不多说,咱们进入正题!向大厂冲锋!
一.最终实现效果
贪吃蛇实现视频
一.Win32 API介绍
本次实现贪吃蛇会使用到的⼀些Win32 API知识,接下来我们就学习⼀下什么是Win32 API。
1.1Win32 API
Windows 这个多作业系统除了协调应⽤程序的执行、分配内存、管理资源之外,
它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使⽤周边设备等目的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。
WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。
1.2控制台程序
平常我们运行起来的黑框程序其实就是控制台程序。
那这个控制台的大小我们可不可设置呢?其实是能的。
- 设置大小
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列。
mode con cols=100 lines=30
我们要设置控制台的话就需要使用system函数,system函数可以用来执行系统命令。
int main()
{
system("mode con cols=40 lines=40");
return 0;
}
- 设置名字
控制台的名字也能修改,使用title命令即可
title 贪吃蛇
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
getchar();
return 0;
}
- 控制台设置
我们平时的默认是控制台终端,但是它实现不了我们想要的效果,所以我们要修改一下。
先点箭头后出现的设置
之后找到默认终端程序应用,改为控制台主机即可。
1.3控制台屏幕上的坐标COORD
我们的贪吃蛇游戏里面有蛇,食物,墙等等。他们涉及到的位置是我们游戏中的关键信息,那我们再屏幕上也要定位他们的位置,那怎么定位呢?这就涉及到COORD了。
COORD是WindowsAPI中定义的⼀个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
COORD是一种结构体类型
- 结构体类型
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
- 头文件
使用COORD结构体需要包含windows.h的头文件
那现在我们定义一个坐标就可以这样写
COORD pos = { 2,3 };
COORD pos1 = { 5,6 };
1.4GetStdHandle
GetStdHandle是⼀个WindowsAPI函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
我们在控制台窗口中进行光标隐藏等操作时,都需要先获得这个窗口。而GetStdHandle就可以获得一个对窗口操作的把手,也就是句柄。通过这个把手我们就可以对控制台窗口进行操作。
HANDLE GetStdHandle(DWORD nStdHandle);
- 参数
标准设备。 此参数的取值可为下列值之一。
那我们贪吃蛇进行蛇的移动,食物的绘制等都是在屏幕上输出信息。所以我们需要或许标准输出设备的句柄
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);
1.5GetConsoleCursorInfo
大家可以发现我们运行时窗口会有一个光标闪烁,那等下贪吃蛇运行时我们是不希望它闪烁的,有没有办法把它隐藏掉呢?那我们得先获取光标信息。
GetConsoleCursorInfo是检索有关指定控制台屏幕缓冲区的光标大小可见性的信息的函数
它的参数有两个:
- 光标控制台句柄
第一个参数是一个跟光标关联的控制台窗口的句柄
_In_ HANDLE hConsoleOutput,
- 光标信息
第二个参数是指向光标信息结构体CONSOLE_CURSOR_INFO的指针
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
- 光标信息结构体
dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条
bVisible,游标的可见性。如果光标可见,则此成员为TRUE。
那我们现在获取光标就可以这样写
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo={0};//光标信息结构体
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
现在我们输出一下光标的信息看一下
//获取标准输出的句柄(⽤来标识不同设备的数值)
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo = { 0 };//光标信息结构体
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
printf("%d", CursorInfo.dwSize);
现在我们把光标大小设为50
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo = { 0 };//光标信息结构体
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.dwSize = 50;
可是为什么光标大小没变呢?
这是因为我们只修改了光标的信息,我们还需要设置修改后光标信息。
1.6SetConsoleCursorInfo
SetConsoleCursorInfo是用来设置指定控制台屏幕缓冲区的光标的大小和可见性。
- 参数
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
它的参数和GetConsoleCursorInfo一样。
现在我们来设置光标信息
- 修改光标大小
- 隐藏光标
1.7SetConsoleCursorPosition
那我们如何让光标移动到指定位置呢?
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
- 参数
一个是控制台窗口的句柄 一个是光标位置的结构体
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
那我们现在想定位光标到第10行,第5列就可以这样写。
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
为了方便我们后面游戏打印食物和移动蛇的位置,我们封装⼀个设置光标位置的函数
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
1.8GetAsyncKeyState
我们贪吃蛇游戏需要根据键盘的的按键进行上下左右的移动。 那如何让程序获取我们的按键信息呢?
这时就需要用到GetAsyncKeyState函数
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState的返回值是short类型,在上⼀次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高 位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
- 参数
函数的参数是一个int类型,代表要检测按键的虚拟键值
SHORT GetAsyncKeyState(
[in] int vKey
);
- 虚拟键值表
这样我们就可以定义一个宏来检测按键是否被按过,只需检测函数返回值的最低位即可。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
二.C语言的国际化
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印食物使用宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占用2个字节。
这⾥再简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。
2.1国际化
C语言字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使用了单字节中的低7 位,最高位是没有使用的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。
比如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表示最多256个符号。但是,这⾥又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码⽅式,代表的字母却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字⺟Gimel在俄语编码中又会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是⼀样的,不⼀样的只是128–255的这一段。
至于亚洲国家的文字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示256x25=65536个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序行为的函数。
2.2<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。
在标准中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
2.3类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏,指定⼀个类项:
- LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm()
- LC_CTYPE:影响字符处理函数的行为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响 printf() 的数字格式。
- LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
- LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
2.4setlocale函数
setlocale函数⽤于修改当前地区,可以针对一个类项修改,也可以针对所有类项。setlocale的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和""(本地模式)。
- 正常模式
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点
- 本地模式
当程序运行起来后想改变地区,就只能显示调用setlocale函数。⽤"作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
setlocale(LC_ALL, " ");//切换到本地环境
2.5宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引
号前面,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表示宽字符串,对应
wprintf() 的占位符为 %ls 。
#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"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
return 0;
}
三.思路分析
我们先把游戏分为三个大的模块。每个模块分别负责不同的功能。 每个模块具体实现又由多个小的函数模块合理设计拼接后组成。大体的思路如下:
-
GameStar函数
GameStar函数负责进入窗口大小和名字的设置,光标的隐藏。
欢迎界面的打印,地图的绘制。
贪吃蛇的创建和初始化,食物的创建。 -
GameRun函数
右侧打印帮助信息-PrintHelpInfo
打印当前已获得分数和每个食物的分数以提示用户
根据按键情况移动蛇-SnakeMove直到游戏是结束状态 -
SnakeMove
根据蛇头的坐标和方向,计算下⼀个节点的坐标
判断下⼀个节点是否是食物-NextIsFood
是食物就吃掉-EatFood
不是食物,吃掉食物,尾巴删除⼀节-NoFood
判断是否撞墙-KillByWall
判断是否撞上自己-KillBySelf -
GameEnd
告知游戏结束的原因
释放蛇身节点
四.GameStar函数
为了方便阅读代码,这里先把我们定义好的宏给大家看
- 实现效果
4.1设置控制台信息
首先我们需要设置控制台大小,名字,还要隐藏光标。
这些操作我们前面讲过的了。不再过多赘述了。
system("mode con cols=100 lines=30");//设置控制台窗口大小
system("title 贪吃蛇");//设置控制台名字
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info ;
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
//修改光标(隐藏光标)
cursor_info.bVisible= false;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
4.2欢迎界面的打印
这里我们把欢迎界面的打印封装为WelcomeToSnake函数
我们只需要将光标定位到指定位置,然后打印文字即可。
注意这里涉及到程序的暂停我们用system函数即可实现。
打印完切换下一个界面时,我们清理屏幕。这个用system函数也能实现
再定位打印文字。
void WelcomeToSnake()
{
setpos(40, 14);//定位光标
wprintf(L"欢迎来到贪吃蛇小游戏~\n");
setpos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//清理屏幕
setpos(25, 14);//定位光标
wprintf(L"分别用↑.↓.←.→控制移动方向,F3加速,F4减速\n");
setpos(25, 15);//定位光标
wprintf(L"加速能够获得更高的分数\n");
setpos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//清理屏幕
}
4.3地图的绘制
地图的绘制我们也单独封装为一个Greatmap()函数
假设我们要绘制一个27行 58列的地图,我们只需要关注好四个角落点的坐标,
然后用四层for循环打印即可。
void Greatmap()
{
setpos(0, 0);//定位原点
for (int i = 0; i < 29; i++)//打印第一行墙体
{
wprintf(L"%lc", WALL);
}
setpos(0, 26);//定位第二行起点
for (int i = 0; i < 29; 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);
}
}
这里大家要注意行的打印光标不需要移动,但是列的打印光标需要移动。
4.4初始化贪吃蛇
游戏中食物的信息,贪吃蛇的信息,蛇的移动方向, 游戏状态,食物分数,总得分,蛇的移动快慢(休眠时间), 食物信息。都是我们游戏过程中需要维护的信息。我们维护游戏其实就是维护这些信息。我们把他们用结构体包含,作为贪吃蛇,我们只需要维护这贪吃蛇即可。
- 游戏状态
四种游戏状态:正常游戏 撞墙 咬到自己 玩家主动结束。
用枚举类型即可。
enum STATUS//游戏的状态
{
ok = 1,
kill_by_wall,
kill_by_self,
end
};
- 蛇的移动方向
四种移动状态:上 下 左 右。
用枚举类型即可。
enum DIRECTION//枚举蛇的方向
{
up = 1,
down,
left ,
right
};
- 蛇头
蛇的维护我们只需要一个指向蛇头的指针即可。食物我们也可以看成蛇的节点。蛇的节点我们用链表来表示。
typedef struct SnakeNode //蛇身
{
int x;
int y;
struct SnakeNode* next;
}Snakenode,* pSnakenode;
- 其他信息
食物分数 总分 休眠时间
用int类型即可。
int fool_score;
int score;
int sleep_time;
- 贪吃蛇信息
typedef struct Snake//游戏中贪吃蛇的信息
{
pSnakenode shead;
pSnakenode snakefool;
enum DRIECTION dir;
enum STATUS statu;
int fool_score;
int score;
int sleep_time;
}Snake,*psnake;
- 创建食物函数
我们创建一个蛇的节点作为食物节点。再用rand函数生成横纵坐标。
然后循环遍历区判断食物节点和和蛇身的每一个节点坐标是否有重合。
重合就用goto语句继续生成坐标知道不重合为止。接着打印食物即可。
void GreatFool(psnake ps)
{
pSnakenode pcur = ps->shead;
pSnakenode fool = (pSnakenode)malloc(sizeof(Snakenode));//创建蛇的节点作为食物节点
if (fool == NULL)//判空
{
perror("GreatFool:malloc");
return;
}
int x, y;
again:
do
{
x = rand() % 53 + 2;//生成横坐标
y = rand() % 25 + 1;//纵坐标
} while (x % 2 != 0);//保证横坐标为偶数
while (pcur)//遍历蛇身
{
if (pcur->x == x && pcur->y == y)
goto again;//食物和蛇身重叠重新生成
pcur = pcur->next;
}
fool->x = x;//赋值横坐标
fool->y = y;//赋值纵坐标
fool->next = NULL;//置空
setpos(fool->x, fool->y);//定位光标
wprintf(L"%lc", FOOD);//打印食物
ps->snakefool= fool;//存放食物节点
}
最后我们再循环创建五个初始的蛇身节点,初始化贪吃蛇信息,再创建食物即可。
void Initsnake(psnake ps)
{
psnake pcur = ps;
for (int i = 0; i < 5; i++)//创建五个蛇的节点
{
pSnakenode node = (pSnakenode)malloc(sizeof(Snakenode));//创建蛇的节点
if (node == NULL)//判空
{
perror("Initsnake:malloc");
return;
}
node->next = NULL;
node->x = POS_X + 2 * i;//横坐标
node->y = POS_Y;//纵坐标
if (pcur->shead == NULL)
{
pcur->shead = node;//蛇头节点(第一个节点)
}
else
{
node->next = pcur->shead;//头插法
pcur->shead = node;
}
while (node)//遍历蛇身打印
{
setpos(node->x, node->y);//定位蛇身坐标
wprintf(L"%lc", BODY);//打印
node = node->next;//遍历下一个蛇身节点
}
}
ps->dir = right;//默认移动方向为右
ps->statu = ok;//游戏状态正常
ps->fool_score = 10;//默认食物分数为10
ps->score = 0;//总分为0
ps->sleep_time = 200;//休眠时间默认为0
GreatFool(ps);//创建食物
}
五.GameRun函数
5.1提示信息函数
在游戏时我们打印一下提示信息给玩家。封装PrintHelpInfo()
void PrintHelpInfo()//打印提示信息
{
setpos(64, 15);
wprintf(L"不能穿墙,不能咬到⾃⼰\n");
setpos(64, 16);
printf("分别用↑.↓.←.→分别控制蛇的移动\n");
setpos(64, 17);
wprintf(L"F3 为加速,F4 为减速\n");
setpos(64, 18);
wprintf(L"ESC :退出游戏.space:暂停游戏.");
setpos(64, 19);
wprintf(L"成杰MAKE");
}
5.2打印分数
游戏过程食物分数和总得分都需要更新
我们也要打印出来。
setpos(64, 10);//定位
printf("总分:%2d", ps->score);//打印信息
setpos(64, 11);//定位
printf("当前食物分数:%2d", ps->fool_score);//打印信息
5.3检测键值
游戏过程我们需要根据键盘输入进行相对的相应
蛇的移动,暂停 加速减速等。
我们前面已经定义了一个宏,直接使用即可
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//检测虚拟键值
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))//检测ESC按键
{
ps->statu = end;//设置退出游戏状态
}
else if (KEY_PRESS(VK_SPACE) )//检测space按键
{
Pause();//暂停游戏
}
else if (KEY_PRESS(VK_F3))//检车f3加速按键
{
if (ps->sleep_time > 120)//设置加速上限
{
ps->sleep_time -= 20;//减少休眠加快速度
ps->fool_score += 2;//分数增加
}
}
else if (KEY_PRESS(VK_F4))//检测f4减速按键
{
if (ps->fool_score> 2)//设置减速上限
{
ps->sleep_time += 20;//增加休眠时间减慢速度
ps->fool_score -= 2;//分数减少
}
}
注意我们移动时不能与当前移动方向相反。
加速减速我们加减休眠时间即可。
暂停我们用pause函数即可,检测space按键,按过直接break跳出即可。
否则一直休眠。
void Pause()
{
while (1)
{
Sleep(200);//休眠
if (KEY_PRESS(VK_SPACE))//检测是否再次按space按键
{
break;//继续游戏
}
}
}
检测完后我们再根据按键输入进行蛇的移动即可。
我们封装为Snakemove函数。
我们先创建一个节点作为下一个蛇头节点。
然后根据移动方向给节点横纵坐标赋值即可。
pSnakenode pcur = (pSnakenode)malloc(sizeof(Snakenode));//生成蛇头下一个位置的节点
if (pcur == NULL)//判空
{
perror("Snakemove:malloc");
return;
}
switch (ps->dir)//检测蛇移动方向
{
case up:
pcur->y=ps->shead->y -1;//赋值横坐标
pcur->x = ps->shead->x;//赋值纵坐标
break;
case down:
pcur->y = ps->shead->y +1;//赋值横坐
pcur->x = ps->shead->x; // 赋值纵坐标
break;
case right:
pcur->x = ps->shead->x +2;//赋值横坐
pcur->y = ps->shead->y; // 赋值纵坐标
break;
case left:
pcur->x = ps->shead->x -2;//赋值横坐
pcur->y = ps->shead->y; // 赋值纵坐标
break;
}
5.4蛇的移动
蛇的移动分两种情况:
- 吃食物移动
若下一个位置是食物。
我们释放开辟的节点,然后让食物节点成为新的蛇头节点
然后遍历打印蛇身,再创建新的食物即可。同时更新得分。
void EatFood(pSnakenode pn, psnake ps)//吃食物
{
ps->snakefool->next = ps->shead;//食物节点链接蛇头
ps->shead = ps->snakefool;//食物节点成为蛇头
free(pn);//释放新开辟的蛇头节点
pn = NULL;
pSnakenode cur = ps->shead;
while (cur)//遍历打印蛇身
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->score += ps->fool_score;//总分增加
GreatFool(ps);//生成新的食物
}
- 不吃食物移动
让新开辟节点链接原来的蛇头,新开辟的节点成为新的蛇头。
再遍历找到倒数第二个尾节点,释放尾巴节点,再让倒数第二个节点指向空封尾。同时记得打印空白覆盖掉蛇尾节点打印的信息。再遍历打印蛇身即可。
void NotFood(pSnakenode pn, psnake ps)
{
pn->next = ps->shead;//让新的蛇头连接原来的蛇头
ps->shead = pn;//让蛇下一个位置的节点成为新的蛇头
pSnakenode pcur = ps->shead;
while (pcur->next->next != NULL)//找到蛇尾倒数第二个节点
{
pcur = pcur->next;//移动
}
pSnakenode dle = pcur;
setpos(pcur->next->x, pcur->next->y);//定位光标到最后的蛇尾
printf(" ");//覆盖原来蛇尾打印的信息
free(dle->next);//释放最后的蛇尾节点
dle->next = NULL;
pSnakenode cur = pn;
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
5.5检测是否撞墙
我们只需要检测横坐标是否为0或56
纵坐标是否为0或26。
再根据穿墙的横坐标或纵坐标不变,修改蛇头另一变化坐标即可。
最后即可设置游戏状态即可。
要实现穿墙我们再让蛇每次移动都刷新城墙即可。
void KillByWall(psnake ps)
{
if (ps->shead->x == 56)//检测是否撞到第二列城墙
{
ps->shead->x = 0;//重新定位穿墙后的横坐标位置(第一列出)
}
else if (ps->shead->x == 0)//检测是否撞到第一列城墙
{
ps->shead->x = 56;//重新定位穿墙后的横坐标位置(第二列出)
}
else if (ps->shead->y == 0)//检测是否撞到第一行城墙
{
ps->shead->y = 26;//重新定位穿墙后的纵坐标位置(第一行出)
}
else if (ps->shead->y == 26)//检测是否撞到第二行城墙
{
ps->shead->y = 0;//重新定位穿墙后的纵坐标位置(第二行出)
}
}
- 检测是否咬到自己
我们只需要判断当前蛇头坐标是否与其他蛇身坐标重合即可。
用循环遍历蛇身在判断即可。最后即可设置游戏状态即可
void KillBySelf( psnake ps)
{
pSnakenode pcur = ps->shead->next;//保存蛇头下一个节点
while (pcur)//遍历蛇身
{
if (ps->shead->x == pcur->x && ps->shead->y == pcur->y)//蛇身与蛇头重合
{
ps->statu = kill_by_self;//设置游戏状态
break;//结束循环
}
pcur = pcur->next;//移动
}
}
每次移动后都检测撞墙和咬到自己。
判断游戏状态,正常则继续游戏。
六.GameEnd函数
游戏结束我们需要检测游戏状态再打印对应提示信息。
最后循环遍历销毁蛇身节点和食物节点即可
void GemaEnd(psnake ps)
{
pSnakenode pcur = ps->shead;
setpos(20, 12);//定位光标
switch (ps->statu)//游戏状态
{
case kill_by_wall://撞墙
wprintf(L"您撞到墙上,游戏结束!");
break;
case kill_by_self://咬到自己
wprintf(L"您咬到自己,游戏结束!");
break;
case end://自己结束
wprintf(L"您主动结束了游戏!");
break;
}
setpos(0, 27);//定位光标
while (pcur)//遍历蛇身销毁链表
{
pSnakenode del = pcur;//保存销毁节点
pcur = pcur->next;//移动下一个销毁节点
free(del);//销毁
del = NULL;
}
free(ps->snakefool);//销毁食物
ps->snakefool = NULL;
}
七.游戏主体设计
最后我们调用三大模块函数即可,再用do_while循环给用户选择是否继续再来一局游戏的选择。注意使用宽字符要记得本地化。
setlocale(LC_ALL, "");
void test()
{
char ch = 0;
do
{
Snake snake = { 0 };
GemeStar(&snake);
GameRun(&snake);
GemaEnd(&snake);
setpos(20, 15);
wprintf(L"再来一局吗?(Y/N):");
ch = getchar();
setpos(0, 27);
} while (ch == 'Y' || ch == 'y');
}
八.源码
- Snake.h
#pragma once
#include<Windows.h>
#include<stdlib.h>
#include<stdio.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
#define WALL L'□'
#define FOOD L'★'
#define BODY 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 STATUS//游戏的状态
{
ok = 1,
kill_by_wall,
kill_by_self,
end
};
typedef struct SnakeNode //蛇身
{
int x;
int y;
struct SnakeNode* next;
}Snakenode,* pSnakenode;
typedef struct Snake//游戏中贪吃蛇的信息
{
pSnakenode shead;
pSnakenode snakefool;
enum DRIECTION dir;
enum STATUS statu;
int fool_score;
int score;
int sleep_time;
}Snake,*psnake;
void setpos(short x,short y);
void GemeStar(psnake ps);
void Greatmap();
void Initsnake(psnake ps);
void GreatFool(psnake ps);
void GameRun(psnake ps);
void PrintHelpInfo();
void Pause();
void Snakemove(psnake ps);
int NextIsFood(pSnakenode pn, psnake ps);
void EatFood(pSnakenode pn, psnake ps);
void NotFood(pSnakenode pn, psnake ps);
void KillByWall(psnake ps);
void KillBySelf(psnake ps);
void GemaEnd(psnake ps);
- Snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
void WelcomeToSnake()
{
setpos(40, 14);//定位光标
wprintf(L"欢迎来到贪吃蛇小游戏~\n");
setpos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//清理屏幕
setpos(25, 14);//定位光标
wprintf(L"分别用↑.↓.←.→控制移动方向,F3加速,F4减速\n");
setpos(25, 15);//定位光标
wprintf(L"加速能够获得更高的分数\n");
setpos(42, 20);//定位光标
system("pause");//暂停程序
system("cls");//清理屏幕
}
void GemeStar(psnake ps)
{
system("mode con cols=100 lines=30");//设置控制台窗口大小
system("title 贪吃蛇");//设置控制台名字
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info ;
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
//修改光标(隐藏光标)
cursor_info.bVisible= false;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
WelcomeToSnake();//欢迎界面打印
Greatmap();//地图打印
Initsnake(ps);//初始化贪吃蛇的信息
}
void Greatmap()
{
setpos(0, 0);//定位原点
for (int i = 0; i < 29; i++)//打印第一行墙体
{
wprintf(L"%lc", WALL);
}
setpos(0, 26);//定位第二行起点
for (int i = 0; i < 29; 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 setpos(short x, short y)//定位坐标函数
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);//创建句柄
COORD pos = {x,y};//坐标的结构体
SetConsoleCursorPosition(houtput, pos);//设置光标位置
}
void Initsnake(psnake ps)
{
psnake pcur = ps;
for (int i = 0; i < 5; i++)//创建五个蛇的节点
{
pSnakenode node = (pSnakenode)malloc(sizeof(Snakenode));//创建蛇的节点
if (node == NULL)//判空
{
perror("Initsnake:malloc");
return;
}
node->next = NULL;
node->x = POS_X + 2 * i;//横坐标
node->y = POS_Y;//纵坐标
if (pcur->shead == NULL)
{
pcur->shead = node;//蛇头节点(第一个节点)
}
else
{
node->next = pcur->shead;//头插法
pcur->shead = node;
}
while (node)//遍历蛇身打印
{
setpos(node->x, node->y);//定位蛇身坐标
wprintf(L"%lc", BODY);//打印
node = node->next;//遍历下一个蛇身节点
}
}
ps->dir = right;//默认移动方向为右
ps->statu = ok;//游戏状态正常
ps->fool_score = 10;//默认食物分数为10
ps->score = 0;//总分为0
ps->sleep_time = 200;//休眠时间默认为0
GreatFool(ps);//创建食物
}
void GreatFool(psnake ps)
{
pSnakenode pcur = ps->shead;
pSnakenode fool = (pSnakenode)malloc(sizeof(Snakenode));//创建蛇的节点作为食物节点
if (fool == NULL)//判空
{
perror("GreatFool:malloc");
return;
}
int x, y;
again:
do
{
x = rand() % 53 + 2;//生成横坐标
y = rand() % 25 + 1;//纵坐标
} while (x % 2 != 0);//保证横坐标为偶数
while (pcur)//遍历蛇身
{
if (pcur->x == x && pcur->y == y)
goto again;//食物和蛇身重叠重新生成
pcur = pcur->next;
}
fool->x = x;//赋值横坐标
fool->y = y;//赋值纵坐标
fool->next = NULL;//置空
setpos(fool->x, fool->y);//定位光标
wprintf(L"%lc", FOOD);//打印食物
ps->snakefool= fool;//存放食物节点
}
void PrintHelpInfo()//打印提示信息
{
setpos(64, 15);
wprintf(L"不能穿墙,不能咬到⾃⼰\n");
setpos(64, 16);
printf("分别用↑.↓.←.→分别控制蛇的移动\n");
setpos(64, 17);
wprintf(L"F3 为加速,F4 为减速\n");
setpos(64, 18);
wprintf(L"ESC :退出游戏.space:暂停游戏.");
setpos(64, 19);
wprintf(L"成杰MAKE");
}
void Pause()
{
while (1)
{
Sleep(200);//休眠
if (KEY_PRESS(VK_SPACE))//检测是否再次按space按键
{
break;//继续游戏
}
}
}
int NextIsFood(pSnakenode pn, psnake ps)
{
return (pn->x == ps->snakefool->x &&
pn->y == ps->snakefool->y);//判断下一个节点是否是食物
}
void Snakemove(psnake ps)
{
pSnakenode pcur = (pSnakenode)malloc(sizeof(Snakenode));//生成蛇头下一个位置的节点
if (pcur == NULL)//判空
{
perror("Snakemove:malloc");
return;
}
switch (ps->dir)//检测蛇移动方向
{
case up:
pcur->y=ps->shead->y -1;//赋值横坐标
pcur->x = ps->shead->x;//赋值纵坐标
break;
case down:
pcur->y = ps->shead->y +1;//赋值横坐
pcur->x = ps->shead->x; // 赋值纵坐标
break;
case right:
pcur->x = ps->shead->x +2;//赋值横坐
pcur->y = ps->shead->y; // 赋值纵坐标
break;
case left:
pcur->x = ps->shead->x -2;//赋值横坐
pcur->y = ps->shead->y; // 赋值纵坐标
break;
}
if (NextIsFood(pcur, ps))//判断下一个节点是否为食物
{
EatFood(pcur, ps);//吃食物的移动
}
else
{
NotFood(pcur,ps);//不吃食物的移动
}
}
void GameRun(psnake ps)
{
PrintHelpInfo();//打印提示信息
do
{
setpos(64, 10);//定位
printf("总分:%2d", ps->score);//打印信息
setpos(64, 11);//定位
printf("当前食物分数:%2d", ps->fool_score);//打印信息
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))//检测ESC按键
{
ps->statu = end;//设置退出游戏状态
}
else if (KEY_PRESS(VK_SPACE) )//检测space按键
{
Pause();//暂停游戏
}
else if (KEY_PRESS(VK_F3))//检车f3加速按键
{
if (ps->sleep_time > 120)//设置加速上限
{
ps->sleep_time -= 20;//减少休眠加快速度
ps->fool_score += 2;//分数增加
}
}
else if (KEY_PRESS(VK_F4))//检测f4减速按键
{
if (ps->fool_score> 2)//设置减速上限
{
ps->sleep_time += 20;//增加休眠时间减慢速度
ps->fool_score -= 2;//分数减少
}
}
Snakemove(ps);//蛇移动
Greatmap();//每次移动刷新城墙实现穿墙;
Sleep(ps->sleep_time);//休眠
KillByWall(ps);//检测是否撞墙
KillBySelf(ps);//检测是否咬到自己
} while (ps->statu == ok);//检测游戏状态
}
void EatFood(pSnakenode pn, psnake ps)//吃食物
{
ps->snakefool->next = ps->shead;//食物节点链接蛇头
ps->shead = ps->snakefool;//食物节点成为蛇头
free(pn);//释放新开辟的蛇头节点
pn = NULL;
pSnakenode cur = ps->shead;
while (cur)//遍历打印蛇身
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->score += ps->fool_score;//总分增加
GreatFool(ps);//生成新的食物
}
void NotFood(pSnakenode pn, psnake ps)
{
pn->next = ps->shead;//让新的蛇头连接原来的蛇头
ps->shead = pn;//让蛇下一个位置的节点成为新的蛇头
pSnakenode pcur = ps->shead;
while (pcur->next->next != NULL)//找到蛇尾倒数第二个节点
{
pcur = pcur->next;//移动
}
pSnakenode dle = pcur;
setpos(pcur->next->x, pcur->next->y);//定位光标到最后的蛇尾
printf(" ");//覆盖原来蛇尾打印的信息
free(dle->next);//释放最后的蛇尾节点
dle->next = NULL;
pSnakenode cur = pn;
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
void KillByWall(psnake ps)
{
if (ps->shead->x == 56)//检测是否撞到第二列城墙
{
ps->shead->x = 0;//重新定位穿墙后的横坐标位置(第一列出)
}
else if (ps->shead->x == 0)//检测是否撞到第一列城墙
{
ps->shead->x = 56;//重新定位穿墙后的横坐标位置(第二列出)
}
else if (ps->shead->y == 0)//检测是否撞到第一行城墙
{
ps->shead->y = 26;//重新定位穿墙后的纵坐标位置(第一行出)
}
else if (ps->shead->y == 26)//检测是否撞到第二行城墙
{
ps->shead->y = 0;//重新定位穿墙后的纵坐标位置(第二行出)
}
}
void KillBySelf( psnake ps)
{
pSnakenode pcur = ps->shead->next;//保存蛇头下一个节点
while (pcur)//遍历蛇身
{
if (ps->shead->x == pcur->x && ps->shead->y == pcur->y)//蛇身与蛇头重合
{
ps->statu = kill_by_self;//设置游戏状态
break;//结束循环
}
pcur = pcur->next;//移动
}
}
void GemaEnd(psnake ps)
{
pSnakenode pcur = ps->shead;
setpos(20, 12);//定位光标
switch (ps->statu)//游戏状态
{
case kill_by_wall://撞墙
wprintf(L"您撞到墙上,游戏结束!");
break;
case kill_by_self://咬到自己
wprintf(L"您咬到自己,游戏结束!");
break;
case end://自己结束
wprintf(L"您主动结束了游戏!");
break;
}
setpos(0, 27);//定位光标
while (pcur)//遍历蛇身销毁链表
{
pSnakenode del = pcur;//保存销毁节点
pcur = pcur->next;//移动下一个销毁节点
free(del);//销毁
del = NULL;
}
free(ps->snakefool);//销毁食物
ps->snakefool = NULL;
}
- test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"snake.h"
void test()
{
char ch = 0;
do
{
Snake snake = { 0 };
GemeStar(&snake);
GameRun(&snake);
GemaEnd(&snake);
setpos(20, 15);
wprintf(L"再来一局吗?(Y/N):");
ch = getchar();
setpos(0, 27);
} while (ch == 'Y' || ch == 'y');
}
int main()
{
setlocale(LC_ALL, "");
test();
return 0;
}
后言
这就是用C语言写出来的贪吃蛇小游戏。这游戏基本使用到C语言所学的全部内容。也算是C语言的试金石吧!今天就分享到这里,感谢小伙伴的耐心垂阅!咱们下期见!拜拜~