1. 前言
我们要用C语言来实现贪吃蛇游戏之前,得了解C语言函数、枚举、结构体、动态内存管理、预处理指令、链表和Win32 API等等的一些相关知识。
关于链表和函数,在之前的文章中都有写过了,友友们可以自行查看。剩下知识会在之后的博客中所提及,请大家多多关注。
这里主要介绍Win32 API,及如何一步一步实现贪吃蛇游戏。
2. Win32 API 介绍
下面介绍的结构体和函数在Win32 API 都是现成的,我们就了解一下如何使用就行。
2.1 Win32 API
Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调⽤这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使⽤周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之称ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows32位平台的应用程序编程接口。
2.2 控制台程序
不知道大家知不知道cmd的程序。
平常我们运行起来的黑框程序其实就是控制台程序
来看看它是什么样的。
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小。像30行,100列,这些都是可以的。
也可以调用C语言函数system来执行不过在使用时要包含#include <windows.h>
。
system("mode con cols=20 lines=20");
我们也可以通过命令设置控制台窗⼝的名字:
system("title 贪吃蛇");
看看效果
2.3 控制台屏幕上的坐标COORD
COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
就是所示这样
2.4 GetStdHandle
GetStdHandle是一个WindowsAPI函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
就是GetStdHandle来获得某一种设备的控制权限。
HANDLE GetStdHandle(DWORD nStdHandle);
举个例子:如果我们想获得输出程序的句柄
那我们得调用GetStdHandle这个函数。
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE)
2.5 GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息。
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机游标(光标)的信息。
2.5.1 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息。
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible,游标的可见性。如果光标可见,则此成员为TRUE。
bVisible = false就是为了隐藏控制台光标。
CursorInfo.bVisible = false;
int main()
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(handle, &cursor_info);
cursor_info.dwSize = 100;
cursor_info.bVisible = true;
SetConsoleCursorInfo(handle, &cursor_info);
return 0;
}
把dwSize试着设置为dwSize = 100,把bVisible设置我bVisible = true。我们来看看效果:
2.6 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
就是在程序运行起来的时候,这个光标是有长度和宽度的。而这些属性是可以设置的,就是放在
CONSOLE_CURSOR_INFO结构体变量中的。
int main()
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(handle, &cursor_info);
return 0;
}
就是把控制台里面的光标信息放在 cursor_info放在里面,所以GetConsoleCursorInfo(handle, &cursor_info),里就传了地址。
2.7 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
我们来看一个示例:
假设把光标定位到pos = { 5, 20 },输入1后,打印是在下一行打印。
int main()
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 5, 20 };
SetConsoleCursorPosition(handle, pos);
int ch = getchar();
putchar(ch);
return 0;
}
用上面这种方式设置一个坐标比较麻烦,那我们直接封装一个函数来确定一个坐标。
代码来实现一下
void SetPos(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(handle, pos);
}
那以后需要设置坐标直接调用就行。
我们来试一下在(10,10)处打印一个hi
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
void SetPos(int x, int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(handle, pos);
}
int main()
{
SetPos(10, 10);
printf("hi\n");
return 0;
}
显然是可以的。
2.8 GetAsyncKeyState
因为我们需要知道玩家在键盘上按键,那我们怎么知道呢?
就是用GetAsyncKeyState获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
int vKey
);
这个函数就一个参数,这个参数指的是键盘上每一个键的虚拟值。
在Win32 API中给键盘上的每一个键都编了号。
我们来看看一部分:
这个函数将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
那我们如何检测呢?
不管这个数字是几,只要按位与1,结果得到1的话,说明被按过,返回的是0,说明没有被按过。
为了方便,我们封装一个PRESS_KET 来检测vk这虚拟键值对应的按键是否被按过
如果按过返回1,未按过返回0
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1) ? 1:0)
3. 贪吃蛇游戏设计与分析
3.1 地图
我们最终的贪吃蛇大概要是这个样子,那我们的地图如何布置呢?
这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍⼀下控制台窗口的坐标知识。
控制台窗口的坐标如图所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t
和宽字符的输入和输出函数,加入了<locale.h>
头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
3.1.1 <locale.h>
本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
就像货币的格式:
在标准中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
3.1.2 类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个个宏,指定一个类项:
-
LC_COLLATE:影响字符串比较较函数
strcoll()
和strxfrm()
。 -
LC_CTYPE:影响字符处理函数的⾏为。
-
LC_MONETARY:影响货币格式。
-
LC_NUMERIC:影响
printf()
的数字格式。 -
LC_TIME:影响时间格式
strftime()
和wcsftime()
。 -
LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
3.1.3 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应wprintf()
的占位符为 %lc
;在双引号前面,表示宽字符串,wprintf()
的占位符为%ls
。
我们用代码来看看:
#include <locale.h>
#include <stdio.h>
#include <windows.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;
}
3.1.4 地图坐标
而我们想得到这样的:
我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,
如下:
3.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24,5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外⼀半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
3.3 数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
而我们要管理整条贪吃蛇,我们再封装一个Snake的结构来维护整条贪吃蛇:
typedef struct Snake
{
pSnakeNode _pSnake;//指向贪吃蛇头结点的指针
pSnakeNode _pFood;//指向食物结点的指针
int _Score;//贪吃蛇累计的总分
int _FoodWeight;//一个食物的分数
int _SleepTime;//每走一步休息的时间,时间越短,速度越快,时间越长,速度越慢
enum DIRECTION _Dir;//描述蛇的方向
enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
}Snake, * pSnake;
3.3.1 蛇的方向
蛇就只有四个方向,向上,向下,向左和向右,,可以一一列举出来,所以使用枚举来描述蛇前进方向。
代码实现:
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
3.3.2 游戏状态
就像我们玩的一些游戏一样,要能够知道游戏运行的状态,像正常运行、撞到墙、蛇撞到自己和正常游戏结束,我们同样可以一一例举出来,也使用枚举来描述。
来看看代码实现:
enum GAME_STATUS
{
OK,//正常运行
END_NORMAL,//按ESC退出
KILL_BY_WALL,//撞到自己
KILL_BY_SELF//正常结束
};
3.4 游戏流程设计
同样设置三个文件,一个test.c用来测试代码,一个snake.h用来放函数声明,最后一个snake.c用来放相关函数的实现。
4.核心逻辑实现分析
4.1 游戏主逻辑
主逻辑分为3个过程:
- 游戏开始(GameStart)完成游戏的初始化
- 游戏运行(GameRun)完成游戏运行逻辑的实现
- 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
4.2 游戏开始(GameStart)
在snake.c中来开始对游戏初始化:
- 控制台窗口大小的设置
- 控制台窗口名字的设置
- 鼠标光标的隐藏
- 打印欢迎界⾯
- 创建地图
- 初始化第蛇
- 创建第⼀个食物
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);
}
4.2.1 打印欢迎界面
在游戏开始之前,我们需要对玩家有一些提示:
就是像这样的
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");
}
当做好这些后,就要创建地图了。
4.2.2 创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf
函数,打印格式串前使用L"%c"
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
先在屏幕上打印上和下,它们相差的是两个字符。
而左和右的墙,在循环时,就只相差1就行。
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);
}
}
最终实现的时候是这样的。
4.2.3 初始化身
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身体后,将蛇的每一节打印在屏幕上。
在创建蛇身时,我们使用头插,将蛇的身体节点一个一个插入,但最终还要返回蛇头节点,方便之后进行操作。
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc()");
return;
}
cur->x = POS_X + 2 * i;
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;
}
4.2.4 创建第一个食物
要生成食物,先随机生成食物的坐标,而x坐标必须是2的倍数,而且食物物的坐标不能和蛇身体每个节点的坐标重复,也不能和墙的坐标重复。
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);
}
4.3 游戏运行(GameRun)
游戏运行期间,右侧打印帮助信息,提示玩家:
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
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);
}
4.3.1 蛇身移动
先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
确定了下一个位置后,需要看下一个位置是否是食物(NextIsFood),是食物就做吃食物物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
我们来看看代码实现:
void SnakeMove(pSnake ps)
{
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
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);
}
4.3.1.1 判断蛇头到达的坐标处是否是食物
不管是不是食物,节点都直接插入,而后再做下一步的处理。
判断蛇头到达的坐标处是否是食物,如果是那就吃掉,并创建新的食物。
代码实现
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;
}
4.3.1.2 判断蛇头的坐标是否和墙的坐标冲突
需要判断蛇头的坐标是否和墙的坐标冲突,如果冲突,那游戏就结束,不冲突,就继续前进。
就只需要判断蛇头节点的x是不是0或者56,或者是蛇头节点的y是不是0或26,就行。
相关代码实现:
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;
}
4.3.1.3 判断蛇头的坐标是否和蛇身体的坐标冲突
需要判断蛇头节点的坐标是不是与身体的坐标重合,重合则返回的状态为KILL_BY_SEL。
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;
}
}
4.4 游戏结束
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,是主动退出,还是撞到墙了,还是撞到蛇自己了,最后要释放蛇身节点。
释放节点时,采用头删的方式,不要忘记把头节点置为NULL。文章来源:https://www.toymoban.com/news/detail-779361.html
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;
}
5. 附代码
5.1 snake.h
#include <locale.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#include <stdio.h>
#include <time.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);
5.2 snake.c
#include "snake.h"
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(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 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)
{
pSnakeNode cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake()::malloc()");
return;
}
cur->x = POS_X + 2 * i;
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);
}
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("zxctsclrjjjcph@版权");
}
void Pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
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()");
return;
}
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 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 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;
}
5.3 test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void test()
{
Snake snake = { 0 };//创建了贪吃蛇
//1. 游戏开始 - 初始化游戏
GameStart(&snake);
//2. 游戏运行 - 游戏的正常运行过程
//GameRun(&snake);
//3. 游戏结束 - 游戏善后(释放资源)
//GameEnd(&snake);
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;
}
有错误欢迎指出,大家一起进步。
如有转载请标注。文章来源地址https://www.toymoban.com/news/detail-779361.html
到了这里,关于【C语言】实现贪吃蛇游戏的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!