介绍:
这张图展示的想必是我们再熟悉不过的一个小游戏,想来我们一定听说过贪吃蛇,吃掉食物,增加自身长度,这款游戏一直以来也有着不小的热度;
而我们在玩这款游戏的时候,是否有思考过,我们所操控的这个蛇初始是怎样生成的?又是如何运动、如何增加自身长度的?又是以一种怎样的方式宣告它的结束?
那今天这篇文章,就来像大家展示一个,用C语言实现一个简易版贪吃蛇的全过程,来向大家展示,贪吃蛇最初的长度是如何生成的,它如何移动以及如何吃掉食物等一系列问题,来帮大家更好的了解这个游戏的本质,相信大家在看完这篇文章后,会对贪吃蛇的代码实现有一个基础的认知,
下面先来向大家展示一下我们最终完工后这一简易贪吃蛇的成果:
那接下来我们就正式开始实现了。
1.部分API函数的简单介绍:
要实现贪吃蛇这个小游戏,那就不得不先提到API函数,对于刚刚接触计算机语言的朋友来说,这个函数可能略微有些陌生,那我们就先对其来个简单的介绍:
Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程(Application),所以便称之为Application Programming Interface,简称 API 函数。WIN32API也就是Microsoft Windows32位平台的应⽤程序编程接口。
1.1.控制台程序:
平时我们每一次运行代码,代码的结果都会显示在运行结束后弹出来的一个框框里,那这个框框,也就是我们的控制台,我们这次要执行的贪吃蛇项目,也是在我们的控制台上执行
因此,在执行贪吃蛇之前,我们可以将控制台的大小和标题都改至符合项目的情况:
我们可以使用cmd指令来操控控制台的大小:
mode con cols=100 lines=30
cols表示列,lines表示行,在控制台上,每一行的大小是两倍的列的大小,其具体位置和行列的大小关系对应情况如下:
当我们使用这个命令后,控制台的大小就变为了30行、100列;
Mode命令详细可参考https://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/modehttps://learn.microsoft.com/zh-cn/windows-server/administration/windows-commands/modetitle命令可修改控制台的名称
title 贪吃蛇
title命令详细可参考
1.2.控制台屏幕上的坐标COORD:
COORD是Windows API中定义的⼀个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
1.2.1.COORD类型的声明:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 30, 15 };
1.3.GetStdHandle
HANDLE GetStdHandle(DWORD nStdHandle);
该API函数,它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使用这个句柄可以操作设备。
当我们需要对控制台进行任何操作时,意味着我们要对控制台的输出信息进行调整,因此我们要获得控制台的输出句柄,该句柄就像我们日常生活中使用的遥控器,控制台就相当于一个电视机,有了输出句柄我们才能对控制台上的输出信息进行操作:
那如何获得输出句柄呢,下面是实例操作:
//获得输出句柄
HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
1.4.GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息:
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO //是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
//(光标)的信息
1.4.1.CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完
全填充单元格到单元底部的水平线条。
bVisible,游标的可见性。如果光标可见,则此成员为TRUE。
隐藏光标:
CursorInfo.bVisible = false; //隐藏控制台光标
1.5.SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性
前面我们所使用的GetConsoleCursorInfo是为了获得我们正在使用的控制台的光标的信息,而当我们修改了这个信息之后,我们还需要把他重新运用到我们的控制台上,那这个时候就需要使用到SetConsoleCursorInfo这个API函数:
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);//设置控制台光标状态
1.6.SetConsoleCursorPosition
设置光标的位置,该API函数的调用,可以直接使光标的位置移动到指定位置:
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
hConsoleOutput [in]
控制台屏幕缓冲区的句柄。 该句柄必须具有 GENERIC_READ 访问权限。 有关详细信息,请参阅控制台缓冲区安全性和访问权限https://learn.microsoft.com/zh-cn/windows/console/console-buffer-security-and-access-rightsdwCursorPosition [in]
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内。
1.7.GetAsyncKeyState
当我们操作贪吃蛇的时候,一定是通过按键盘上的某些按键来操控贪吃蛇,那这个时候就需要使用到API函数GetAsyncKeyState,这个函数会判断我们是否按到过这个按键,我们再来通过这个返回值来进行对应功能的实现,该函数的原型如下:
SHORT GetAsyncKeyState(
int vKey
);
2.贪吃蛇游戏的实现:
在介绍完一部分我们会使用到的API函数后,我们就可以正式开始实现代码了,事实上API函数很多很多,有着成千上万种不同的功能,我们所了解的只是冰山一角,如果大家有兴趣的话当然可以可以自行了解;
贪吃蛇游戏的实现总共分为三个部分:游戏开始(GameStart)、游戏运行(GameRun)、游戏结束(GameOver),我们来一一进行实现:
2.1.GameStart
在该功能中,我们主要要实现的有以下几点:
1.控制台大小和名称的调整;
2.光标的隐藏;
3.欢迎界面的打印;
4.贪吃蛇游戏地图的创建;
5.贪吃蛇的创建和初始化;
6.食物的创建和初始化;
在实现这些功能之前,我们需要先实现两个非常简单的操作,虽然很简单,但是他们非常重要,这其中的一个操作是实现一个函数,这个函数贯穿我们整个贪吃蛇游戏,我们无时无刻不在使用它,他就是我们控制台光标位置的指定函数SetPos();
2.1.01.光标位置的指定SetPos的实现:
为了实现这个函数,我们当然是要结合到我们前面所了解到的API函数GetStdHandle、和SetConsoleCursorPosition,将光标设置到我们指定的x、y位置,实现如下:
void SetPos(short x, short y)
{
//获得输出句柄
HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y }; //设置我们需要位置的坐标
SetConsoleCursorPosition(Houtput, pos);
}
在实现完这个函数后,我们就要开始实现第二个操作:
2.1.02.本地化:
这第二个非常简单的操作,当然就是本地化,我们在实现贪吃蛇这一游戏的时候,一定会使用到宽字符,为防止有些朋友对宽字符可能并无了解,这里简单介绍一下:
我们平常使用printf输出在控制台或者是终端上的字符,都是标准字符,站一个字节位,而我们的宽字符,会占据两个字节位,这里给出一张图便于大家直接感受:
a就是我们平常使用的标准字符,而 ● 就是我们在贪吃蛇游戏中要使用到的宽字符,我们可以明显发现,宽字符的站位大小是标准字符的站位大小的两倍,我们也可以发现,我们的中文汉字也是站两个字节位大小的;
但我们编译器本身提供的环境(也就是C环境)是不支持宽字符的(如果对宽字符不曾了解,可关注我后序的文章,会详细讲解,这里只需初步了解),也没有函数能将其输出在控制台上,但我们的当地的环境,也就是中国地区的环境下,是能支持宽字符的打印的,这个时候我们就要将环境调整为本地环境,这个时候,我们就要使用到locale.h头文件中的setlocale函数(其具体使用会和宽字符一同讲解,这里只需初步了解),将环境改为本地环境:
setlocale(LC_ALL, "");
在实现完这两个简单的操作后,我们就可以上手实现GameStart函数中的各种功能了;
2.1.1.控制台大小和名称的调整:
使用前面介绍过的mode和title命令,直接实现对控制台的调整:
void AdjustConsole()
{
//调整控制台的大小
system("mode con cols=100 lines=30");
//修改控制台的名称
system("title 贪吃蛇");
}
2.1.2.光标的隐藏
先创建出CONSOLE_CURSOR_INFO类型的结构体,利用句柄使其成为我们要使用的这个控制台的光标信息,将其中的是否可见变量设置为false(要包含头文件<stdbool.h>),再使用SetConsoleCursorInfo完成修改:
void HideCursorInfo()
{
//获得输出句柄
HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo = { 0 };
GetConsoleCursorInfo(Houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(Houtput, &CursorInfo);
}
2.1.3.欢迎界面的打印
void Welcome()
将光标调整到合适位置,打印欢迎界面的信息;
别忘了使用pause命令停顿,使我们贪吃蛇游戏的流程更加清晰明了:
void Welcome()
{
SetPos(38, 13);
wprintf(L"欢迎来到贪吃蛇小游戏");
SetPos(38, 20);
system("pause");
}
效果展示:
在实现完这个界面后,我们最好给出一个游戏说明的界面,能让玩家在游戏开始前提前了解到游戏的规则,如下:
void PrintHelp()
{
SetPos(30, 10);
wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
SetPos(30, 11);
wprintf(L"空格(space)表示暂停,Esc表示正常退出游戏");
SetPos(30, 13);
wprintf(L"争取获得更高的分数");
SetPos(30, 22);
system("pause");
}
效果展示:
2.1.4.贪吃蛇游戏地图的创建
void CreateMap(pSnack ps)
我们整个控制台的大小是100列,30行,在创建地图之前,我们要确定好地图的大小,这里我们以27*56的地图大小来做示范,大家也可根据自身习惯,来决定地图的大小;
地图的样貌(27 * 58)如下图所示:
我们需要用到宽字符 □ 来作为地图的边框,来分别打印出四条地图的边框:
以打印上图来做示范,以此类推,打印出左右下的地图边框:
27*56地图的上边框共有28个 □ ,所以共要循环28次,如下:
SetPos(0, 0);
for (int i = 0; i < 56; i += 2)
{
wprintf(L"□");
}
以次方法再打印出左右下的边框:
//下
SetPos(0, 26);
for (int i = 0; i < 56; i += 2)
{
wprintf(L"□");
}
//左
for (int i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"□");
}
//右
for (int i = 1; i < 26; i++)
{
SetPos(54, i);
wprintf(L"□");
}
效果展示:
为了便于视觉上的体验,我们还能在地图的附近选择合适的位置给出提示信息:
//打印出帮助信息
SetPos(60, 19);
wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
SetPos(60, 21);
wprintf(L"空格(space)表示暂停");
SetPos(60, 23);
wprintf(L"Esc表示正常退出游戏");
2.1.5.贪吃蛇的创建和初始化
贪吃蛇是基于链表实现的,因此贪吃蛇的每一个节点都相当于是一个链表的结点,我们知道,链表中的每一个结点都是由两个部分组成的,一个数节点中储存的数据,另一个是指向下一个结点的指针,而贪吃蛇的一个结点中存储的数据,是该节点的坐标位置,我们可以通过这个坐标,来打印出贪吃蛇的这一个结点,如下所示,我们先定义出贪吃蛇的节点:
//定义的一个结点的坐标
typedef struct CoordinateOfSnackNode
{
short x;
short y;
}NodeCoord;
//定义一个蛇的结点————由链表实现
typedef struct SnackNode
{
NodeCoord coord;
struct SnakcNode* next;
}SnackNode, * pSnackNode;
在定义出蛇的节点后,我们不妨再把整个贪吃蛇要用到的所有数据同时定义出来,这便是很重要的面向对象的思维如下,整个贪吃蛇应包含如下信息:
//蛇的移动方向
enum DIRECTION
{
UP,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
enum STATE
{
OK,
END_NORMAL,
HIT_BYSELF,
HIT_BYWALL
};
//定义一整条蛇————包含如下信息
typedef struct Snack
{
//蛇头
pSnackNode _SnackHead;
//蛇的移动方向
enum DIRECTION _dire;
//每走一步的休眠时间
int _sleeptime;
//蛇的状态
enum STATE _state;
//食物
pSnackNode _food;
//一个食物的分数
int _foodweight;
//当前获得的总分数
int _score;
}Snack, * pSnack;
下面来进行蛇的初始化:
我们默认开始的贪吃蛇本身有5个蛇结点,每个结点的申请构造方式如下
2.1.5.1.BuySnackNode
pSnackNode BuySnackNode(NodeCoord coord)
利用malloc申请空间,最后返回申请到的节点:
pSnackNode BuySnackNode(NodeCoord coord)
{
pSnackNode newnode = (pSnackNode)malloc(sizeof(SnackNode));
if (newnode == NULL)
{
perror("BuySnackNode()::malloc");
exit(-1);
}
newnode->coord = coord;
newnode->next = NULL;
return newnode;
}
2.1.5.2.SnackInit
我们假定第一个蛇节点的位置为x=24,y=6(根据个人习惯),来循环创建结点,最后采用头插的方式并接到蛇身上:
void SnackInit(pSnack ps)
{
//初始蛇有5个结点,假设蛇头的初位置为24,6;
for (int i = 0; i < 5; i++)
{
NodeCoord coord = { POS_X + i * 2, POS_Y };
if (ps->_SnackHead == NULL)
{
ps->_SnackHead = BuySnackNode(coord);
}
else
{
//头插数据
pSnackNode newnode = BuySnackNode(coord);
newnode->next = ps->_SnackHead;
ps->_SnackHead = newnode;
}
}
}
既然创建好了蛇,我们当然是要将其显示出来,因此我们创建一个函数,这个函数的功能就是打印出整条蛇,蛇的打印,相当于链表的遍历,这里我们为了可视性,将蛇头用宽字符方块表示,蛇身用宽字符圆点表示:如下所示:
void ShowSnack(pSnack ps)
{
pSnackNode cur = ps->_SnackHead;
while (cur)
{
if (cur == ps->_SnackHead)
{
SetPos(cur->coord.x, cur->coord.y);
wprintf(L"%lc", HEAD);
}
else
{
SetPos(cur->coord.x, cur->coord.y);
wprintf(L"%lc", BODY);
}
cur = cur->next;
}
}
2.1.6.食物的创建和初始化
食物的本身也是一个蛇结点,所以我们仍然可以使用BuySnackNode来创建食物,只不过我们食物的坐标需要随机生成,这个时候我们就要想起随机数的生成方法:
srand利用时间戳在每时每刻获得不同的unsigned int类型的值,然后用rand函数就可以生成随机数,这里不要忘了包含头文件time.h和stdlib.h,
srand的调用如下:
//调用srand函数,便于生成随机数
srand((unsigned int)time(NULL));
有了随机数的生成,我们就能随机生成食物的坐标,但这里有两个需要注意的地方:
1.食物坐标的x要是2的倍数,否则无法贪吃蛇的头结点无法与食物重合,也就无法吃到食物;
2.食物的坐标不能和蛇身重合,当生成的随机数与蛇身有重合,应使用goto语句再次生成,直到符合条件为止;
再创造完食物后可将食物给显示出来,食物我们用宽字符五角星来表示;
如下所示:
void CreateFood(pSnack ps)
{
int x = 0, y = 0;
again:
//食物的坐标要在地图范围内,同时x要是2的倍数
do
{
x = 2 + rand() % (52 - 2 + 1);
y = 1 + rand() % (25 - 1 + 1);
} while (x % 2 != 0);
//坐标不能与蛇身重合
pSnackNode cur = ps->_SnackHead;
while (cur)
{
if (cur->coord.x == x && cur->coord.y == y)
goto again;
cur = cur->next;
}
NodeCoord FoodPos = { x,y };
ps->_food = BuySnackNode(FoodPos);
//显示出食物
SetPos(ps->_food->coord.x, ps->_food->coord.y);
wprintf(L"%lc", FOOD);
}
再将贪吃蛇中未初始化的几个变量初始化一下,那整个GameStart函数就算实现好了;
//初始化蛇自身的部分
ps->_state = OK;//状态默认OK
ps->_dire = RIGHT;//方向默认向右
ps->_sleeptime = 200;//每走一步中间的间隔时间默认为200ms;
ps->_SnackHead = NULL;
//初始化食物的其余数据
ps->_foodweight = 10;
ps->_score = 0;
GameStart整体效果展示:
2.2.GameRun
游戏运行,这个功能的实现就比较直接了,简单来说,这个函数要做的就是通过按键,来改变贪吃蛇中的各个变量,再根据各个变量的改变来进行对应功能的实现:
“↑、↓、←、→分别控制蛇的上下左右移动”
“空格(space)表示暂停”
“Esc表示正常退出游戏”
这是我们的玩法说明,怎么判断我们是否按了这几个按键,这就需要使用到我们前面提到的API函数GetAsyncKeyState,该函数中传入对应按键的虚拟按键值,当按下这个按键之后,该函数的二进制最低位会返回1,否则返回0,以此我们就可以来定义一个宏KET_PRESS(VK),来判断我们是否按下过这个按键,用GetAsyncKeyState的返回值按位与(&)上一个1之后,得到的就是他的二进制最低位的数:
//定义一个宏,用于确定按键是否被按,按后则返回1,否则返回0;
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x1)
那GameRun函数的执行,就是在一个do......while循环中,通过判断某个按键来改变蛇的状态,每循环一次蛇就会往指定方向走一步,每次循环都有间隔,间隔时间为蛇每走一步的休眠时间,那么do...while循环的结束条件是什么呢?在前面我们定义蛇的时候就知道,蛇有四个状态,用一个枚举变量表示出来,如下所示:
//蛇的状态
enum STATE
{
OK,
END_NORMAL,
HIT_BYSELF,
HIT_BYWALL
};
正常、正常结束、撞到自己和撞到墙,当我们蛇的状态是正常的时候,它就能持续运动,否则就会退出循环,结束游戏:
当按到上下左右按键时,对应的运动方向发生改变,按到空格时,就需要暂停(暂停的方法是使其休眠时间变为无穷),按到Esc时,状态变为自动结束,F3或者F4时,休眠的时间变长或者变短,对应一个食物的分数增多或者减少;
因此该函数的大体框架就如下所示:
void GameRun(pSnack ps)
{
do
{
//显示得分情况
ShowScore(ps);
if (KEY_PRESS(VK_UP) && ps->_dire != DOWN)
ps->_dire = UP;
else if (KEY_PRESS(VK_DOWN) && ps->_dire != UP)
ps->_dire = DOWN;
else if (KEY_PRESS(VK_LEFT) && ps->_dire != RIGHT)
ps->_dire = LEFT;
else if (KEY_PRESS(VK_RIGHT) && ps->_dire != LEFT)
ps->_dire = RIGHT;
else if (KEY_PRESS(VK_SPACE))
SleepForever();
else if (KEY_PRESS(VK_ESCAPE))
ps->_state = END_NORMAL;
else if (KEY_PRESS(VK_F3))
{
//加速,每走一步休眠时间变短,得分更高
if (ps->_sleeptime > 50)
{
ps->_sleeptime -= 30;//不能一直加速,最多加速5次
ps->_foodweight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//加速,每走一步休眠时间变短,得分更高
if (ps->_sleeptime < 320)
{
ps->_sleeptime += 30;//不能一直加速,最多加速4次
ps->_foodweight -= 2;
}
}
SnackMove(ps);
ShowSnack(ps);
Sleep(ps->_sleeptime);
} while (ps->_state == OK);
}
然后就通过按键所改变的状态,决定是暂停。退出、或者是向哪边移动;
2.2.1.暂停
暂停函数,SleepForever,就是使其休眠时间无线延长,我们可以用一个死循环来表示,当然,再按一次空格,我们就退出暂停状态,因此这个循环中还需要存在是否按到空格这么一个宏
void SleepForever()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
//再按一次,继续运动
break;
}
}
}
2.2.2.移动
void SnackMove(pSnack ps)
该函数的实现需要考虑到很多因素,大概思路如下所示,通过蛇的移动方向找到下一个结点,然后通过这下一个节点判断蛇是吃到食物、撞到自己还是撞墙、当这三者都不发生,蛇就是正常移动,我们先来找到下一个节点:
2.2.2.1.NextNode
根据对应状态,调整坐标,根据调整后的坐标申请节点,采用头插法使这个下一个结点成为新的头结点:
pSnackNode NextNode(pSnack ps)
{
pSnackNode nextnode = NULL;
switch (ps->_dire)
{
case UP:
{
NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y - 1 };
nextnode = BuySnackNode(coord);
break;
}
case DOWN:
{
NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y + 1 };
nextnode = BuySnackNode(coord);
break;
}
case RIGHT:
{
NodeCoord coord = { ps->_SnackHead->coord.x + 2,ps->_SnackHead->coord.y };
nextnode = BuySnackNode(coord);
break;
}
case LEFT:
{
NodeCoord coord = { ps->_SnackHead->coord.x - 2,ps->_SnackHead->coord.y };
nextnode = BuySnackNode(coord);
break;
}
}
return nextnode;
}
2.2.2.2.判断是否撞墙或者撞到自己
当撞墙或者撞到自己,就改变贪吃蛇的状态,本次循环走完后就会结束do...while循环,意味着本次游戏的结束,当下一个结点的坐标和墙或者自己重合的时候,意味着撞墙或者撞到了自己,撞墙,即x为0或者54,y为0或26,撞到自己需要以此遍历自身结点,观察下一个结点的坐标是否与自身的某个结点重合,如下所示:
bool IsBeHitedByWall(pSnackNode nextnode)
{
return nextnode->coord.x == 0 || nextnode->coord.x == 54 ||
nextnode->coord.y == 0 || nextnode->coord.y == 26;
}
bool IsBeHitedBySelf(pSnack ps, pSnackNode nextnode)
{
pSnackNode cur = ps->_SnackHead;
while (cur)
{
if (cur->coord.x == nextnode->coord.x && cur->coord.y == nextnode->coord.y)
return true;
cur = cur->next;
}
return false;
}
2.2.2.3.下一个节点是不是食物
bool IsFood(pSnack ps, pSnackNode nextnode)
这是整个贪吃蛇游戏的一个比较核心的环节,贪吃蛇只有吃掉食物,自身长度才能增长,才能获得更高的得分,那怎么样才能吃到食物呢,首先,我们需要判断下一个结点是不是食物,即下一个结点的坐标和食物的坐标是否相同,若相同,则返回true,否则返回false,如下:
bool IsFood(pSnack ps, pSnackNode nextnode)
{
return ps->_food->coord.x == nextnode->coord.x &&
ps->_food->coord.y == nextnode->coord.y;
}
2.2.2.4.吃掉食物
void EatFood(pSnack ps)
如果判断好下一个结点就是食物(IsFood的返回值为true),那这个时候我们就应该吃掉食物,吃食物的方法很简单,我们只掉,食物本身也是贪吃蛇的一个节点,那这个时候,我们只需要让这个食物成为贪吃蛇这个链表的头结点,还是采用简单的头插数据就可以实现,但不要忘了,吃掉食物之后一定记得再生成一个新的食物,如下所示:
void EatFood(pSnack ps)
{
//食物成为新一个头结点,蛇的结点增加
ps->_food->next = ps->_SnackHead;
ps->_SnackHead = ps->_food;
//加分
ps->_score += ps->_foodweight;
//创造一个新食物
CreateFood(ps);
}
2.2.2.5.正常移动Move
如果说贪吃蛇既没有吃到食物,也没有撞墙或者是撞到自己,那么它就需要正常移动,将下一个结点采用头插法插入到贪吃蛇的链表中,同时需要尾删贪吃蛇的最后一个蛇结点,因为贪吃蛇的正常移动,它的蛇身长度是不会改变的,只是它的位置改变了,删除节点我们需要使用到free函数
但不要忘了,再释放最后一个结点之前,应该将最后一个节点对应坐标处用两个空格覆盖掉,要不然控制台所展示出来的就不会是正常移动的图像,而是一条越拉越长的蛇,该函数的实现如下:
void Move(pSnack ps, pSnackNode nextnode)
{
nextnode->next = ps->_SnackHead;
ps->_SnackHead = nextnode;
pSnackNode cur = ps->_SnackHead;
pSnackNode prev = NULL;
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
SetPos(cur->coord.x, cur->coord.y);
printf(" ");
free(cur);
prev->next = NULL;
}
在实现完以上功能后,我们SnackMove的整体框架和逻辑就出现了,如下:
void SnackMove(pSnack ps)
{
//下一个头结点
pSnackNode nextnode = NextNode(ps);
//判断下一个头结点是否是食物
if (IsFood(ps, nextnode))
{
//吃掉食物
EatFood(ps);
}
//判断是否撞墙
else if (IsBeHitedByWall(nextnode))
{
ps->_state = HIT_BYWALL;
}
//判断是否撞到自己
else if (IsBeHitedBySelf(ps, nextnode))
{
ps->_state = HIT_BYSELF;
}
else
{
//正常移动
Move(ps, nextnode);
}
}
那当我们移动一步后,我们当然就需要把整条贪吃蛇再一次显示在控制台上,也就是再调用一次ShowSnack函数,这样来看,贪吃蛇移动一步就算是已经实现好了,我们只需要在每次移动的间隙中利用Sleep命令停顿上我们所设置好的休眠时间,就可以实现贪吃蛇游戏的运行:
2.3.GameOver
//游戏结束
GameOver(&snack);
游戏结束这个函数功能中主要由两点要实现的功能:
1.游戏是如何结束的声明,是主动结束还是撞到墙,亦或是撞到自己;
2.贪吃蛇是基于链表实现的,而链表的每个结点,包括食物,都是利用动态内存开辟malloc在堆上开辟的空间,因此我们需要释放掉这部分空间,避免造成内存泄露;整体的实现也比较简单,只需遍历整个链表,释放当前结点前,储存好下一个结点的指针,再依次向后跑,如下所示:
void GameOver(pSnack ps)
{
SetPos(40, 12);
if (ps->_state == HIT_BYWALL)
printf("您已撞墙\n");
else if (ps->_state == HIT_BYSELF)
printf("您撞到了自己\n");
else if (ps->_state == END_NORMAL)
printf("您主动结束了游戏\n");
pSnackNode cur = ps->_SnackHead;
while (cur)
{
pSnackNode next = cur->next;
free(cur);
cur = next;
}
free(ps->_food);
ps->_food = NULL;
}
游戏到这那我们的贪吃蛇游戏基本就算完全实现好了,我们就可以在主函数中依次调用这三个函数,来进行贪吃蛇游戏,如果我们想要在一局游戏结束后,通过某一个条件来判断我们是否要重新再开一把游戏,那这个时候我们就可以发挥自己的想象,用一个do...while循环来包含这三个函数,通过某一条件来确定循环是否继续执行,这里也就不再过多介绍,参考代码会放在整篇文章的最后;
3.结语:
整体来说,贪吃蛇小游戏这个项目,虽然说并不是很复杂,但是它完美结合了很多C语言的知识,在我们实现贪吃蛇的过程中,也算是对我们以往知识的一个复习与巩固,我们所实现的只不过是一个很简单的贪吃蛇基层逻辑,这个游戏还有很多可以优化的地方,这可以锻炼我们的思维,在学习的同时,我们一定要着手写出代码,只有多写才会有进步,对代码才会更加熟练;
那这篇文章就介绍到这里,感谢大家观看!
4.参考代码:
4.1.snack.h
#pragma once
//贪吃蛇小游戏的完全实现
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <Windows.h>
#include <locale.h>
//定义的一个结点的坐标
typedef struct CoordinateOfSnackNode
{
short x;
short y;
}NodeCoord;
//定义一个蛇的结点————由链表实现
typedef struct SnackNode
{
NodeCoord coord;
struct SnakcNode* next;
}SnackNode, * pSnackNode;
//蛇的移动方向
enum DIRECTION
{
UP,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
enum STATE
{
OK,
END_NORMAL,
HIT_BYSELF,
HIT_BYWALL
};
//定义一整条蛇————包含如下信息
typedef struct Snack
{
//蛇头
pSnackNode _SnackHead;
//蛇的移动方向
enum DIRECTION _dire;
//每走一步的休眠时间
int _sleeptime;
//蛇的状态
enum STATE _state;
//食物
pSnackNode _food;
//一个食物的分数
int _foodweight;
//当前获得的总分数
int _score;
}Snack, * pSnack;
//游戏开始
void GameStart(pSnack ps);
//设置光标位置
void SetPos(short x, short y);
//隐藏光标
void HideCursorInfo();
//调整好控制台的大小和名称
void AdjustConsole();
//打印欢迎界面
void Welcome();
//打印游戏说明界面
void PrintHelp();
//创造游戏地图
void CreateMap(pSnack ps);
//蛇头的初位置
#define POS_X 24
#define POS_Y 5
//初始化蛇身
void SnackInit(pSnack ps);
//创建蛇的一个结点
pSnackNode BuySnackNode(NodeCoord coord);
//打印蛇
//蛇头
#define HEAD L'■'
//蛇身
#define BODY L'●'
void ShowSnack(pSnack ps);
//创造一个食物
#define FOOD L'★'
void CreateFood(pSnack ps);
//游戏运行
void GameRun(pSnack ps);
//定义一个宏,用于确定按键是否被按,按后则返回1,否则返回0;
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 0x1)
//显示得分情况
void ShowScore(pSnack ps);
//空格space暂停
void SleepForever();
//移动一步
void SnackMove(pSnack ps);
//下一个头结点
pSnackNode NextNode(pSnack ps);
//判断下一个结点是否是食物
bool IsFood(pSnack ps, pSnackNode nextnode);
//吃掉食物
void EatFood(pSnack ps);
//判断是否撞墙
bool IsBeHitedByWall(pSnackNode nextnode);
//判断是否撞到自己
bool IsBeHitedBySelf(pSnack ps, pSnackNode nextnode);
//正常移动
void Move(pSnack ps, pSnackNode nextnode);
//游戏结束
void GameOver(pSnack ps);
4.2.snack.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snack.h"
//设置光标位置
void SetPos(short x, short y)
{
//获得输出句柄
HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(Houtput, pos);
}
void HideCursorInfo()
{
//获得输出句柄
HANDLE Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo = { 0 };
GetConsoleCursorInfo(Houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(Houtput, &CursorInfo);
}
void AdjustConsole()
{
//调整控制台的大小
system("mode con cols=100 lines=30");
//修改控制台的名称
system("title 贪吃蛇");
}
void Welcome()
{
SetPos(38, 13);
wprintf(L"欢迎来到贪吃蛇小游戏");
SetPos(38, 20);
system("pause");
}
void PrintHelp()
{
SetPos(30, 10);
wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
SetPos(30, 11);
wprintf(L"空格(space)表示暂停,Esc表示正常退出游戏");
SetPos(30, 13);
wprintf(L"争取获得更高的分数");
SetPos(30, 22);
system("pause");
}
void CreateMap(pSnack ps)
{
//以56*27的大小来创建地图
//上
SetPos(0, 0);
for (int i = 0; i < 56; i += 2)
{
wprintf(L"□");
}
//下
SetPos(0, 26);
for (int i = 0; i < 56; i += 2)
{
wprintf(L"□");
}
//左
for (int i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"□");
}
//右
for (int i = 1; i < 26; i++)
{
SetPos(54, i);
wprintf(L"□");
}
//打印出帮助信息
SetPos(60, 19);
wprintf(L"↑、↓、←、→分别控制蛇的上下左右移动");
SetPos(60, 21);
wprintf(L"空格(space)表示暂停");
SetPos(60, 23);
wprintf(L"Esc表示正常退出游戏");
}
pSnackNode BuySnackNode(NodeCoord coord)
{
pSnackNode newnode = (pSnackNode)malloc(sizeof(SnackNode));
if (newnode == NULL)
{
perror("BuySnackNode()::malloc");
exit(-1);
}
newnode->coord = coord;
newnode->next = NULL;
return newnode;
}
void SnackInit(pSnack ps)
{
//初始蛇有5个结点,假设蛇头的初位置为24,6;
for (int i = 0; i < 5; i++)
{
NodeCoord coord = { POS_X + i * 2, POS_Y };
if (ps->_SnackHead == NULL)
{
ps->_SnackHead = BuySnackNode(coord);
}
else
{
//头插数据
pSnackNode newnode = BuySnackNode(coord);
newnode->next = ps->_SnackHead;
ps->_SnackHead = newnode;
}
}
}
void ShowSnack(pSnack ps)
{
pSnackNode cur = ps->_SnackHead;
while (cur)
{
if (cur == ps->_SnackHead)
{
SetPos(cur->coord.x, cur->coord.y);
wprintf(L"%lc", HEAD);
}
else
{
SetPos(cur->coord.x, cur->coord.y);
wprintf(L"%lc", BODY);
}
cur = cur->next;
}
}
void CreateFood(pSnack ps)
{
int x = 0, y = 0;
again:
//食物的坐标要在地图范围内,同时x要是2的倍数
do
{
x = 2 + rand() % (52 - 2 + 1);
y = 1 + rand() % (25 - 1 + 1);
} while (x % 2 != 0);
//坐标不能与蛇身重合
pSnackNode cur = ps->_SnackHead;
while (cur)
{
if (cur->coord.x == x && cur->coord.y == y)
goto again;
cur = cur->next;
}
NodeCoord FoodPos = { x,y };
ps->_food = BuySnackNode(FoodPos);
//显示出食物
SetPos(ps->_food->coord.x, ps->_food->coord.y);
wprintf(L"%lc", FOOD);
}
void GameStart(pSnack ps)
{
//隐藏光标
HideCursorInfo();
//调整好控制台的大小和名称
AdjustConsole();
//初始化蛇自身的部分
ps->_state = OK;//状态默认OK
ps->_dire = RIGHT;//方向默认向右
ps->_sleeptime = 200;//每走一步中间的间隔时间默认为200ms;
ps->_SnackHead = NULL;
//打印欢迎界面
Welcome();
system("cls");
//打印游戏说明界面
PrintHelp();
system("cls");
//创造游戏地图
CreateMap(ps);
//初始化蛇身
SnackInit(ps);
//打印蛇
ShowSnack(ps);
//创造一个食物
CreateFood(ps);
//初始化食物的其余数据
ps->_foodweight = 10;
ps->_score = 0;
}
void ShowScore(pSnack ps)
{
SetPos(60, 10);
wprintf(L"一个食物的分数为%2d", ps->_foodweight);
SetPos(60, 11);
wprintf(L"目前总的分数为%2d", ps->_score);
}
void SleepForever()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
//再按一次,继续运动
break;
}
}
}
pSnackNode NextNode(pSnack ps)
{
pSnackNode nextnode = NULL;
switch (ps->_dire)
{
case UP:
{
NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y - 1 };
nextnode = BuySnackNode(coord);
break;
}
case DOWN:
{
NodeCoord coord = { ps->_SnackHead->coord.x,ps->_SnackHead->coord.y + 1 };
nextnode = BuySnackNode(coord);
break;
}
case RIGHT:
{
NodeCoord coord = { ps->_SnackHead->coord.x + 2,ps->_SnackHead->coord.y };
nextnode = BuySnackNode(coord);
break;
}
case LEFT:
{
NodeCoord coord = { ps->_SnackHead->coord.x - 2,ps->_SnackHead->coord.y };
nextnode = BuySnackNode(coord);
break;
}
}
return nextnode;
}
bool IsFood(pSnack ps, pSnackNode nextnode)
{
return ps->_food->coord.x == nextnode->coord.x &&
ps->_food->coord.y == nextnode->coord.y;
}
void EatFood(pSnack ps)
{
//食物成为新一个头结点,蛇的结点增加
ps->_food->next = ps->_SnackHead;
ps->_SnackHead = ps->_food;
//加分
ps->_score += ps->_foodweight;
//创造一个新食物
CreateFood(ps);
}
bool IsBeHitedByWall(pSnackNode nextnode)
{
return nextnode->coord.x == 0 || nextnode->coord.x == 54 ||
nextnode->coord.y == 0 || nextnode->coord.y == 26;
}
bool IsBeHitedBySelf(pSnack ps, pSnackNode nextnode)
{
pSnackNode cur = ps->_SnackHead;
while (cur)
{
if (cur->coord.x == nextnode->coord.x && cur->coord.y == nextnode->coord.y)
return true;
cur = cur->next;
}
return false;
}
void Move(pSnack ps, pSnackNode nextnode)
{
nextnode->next = ps->_SnackHead;
ps->_SnackHead = nextnode;
pSnackNode cur = ps->_SnackHead;
pSnackNode prev = NULL;
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
SetPos(cur->coord.x, cur->coord.y);
printf(" ");
free(cur);
prev->next = NULL;
}
void SnackMove(pSnack ps)
{
//下一个头结点
pSnackNode nextnode = NextNode(ps);
//判断下一个头结点是否是食物
if (IsFood(ps, nextnode))
{
//吃掉食物
EatFood(ps);
}
//判断是否撞墙
else if (IsBeHitedByWall(nextnode))
{
ps->_state = HIT_BYWALL;
}
//判断是否撞到自己
else if (IsBeHitedBySelf(ps, nextnode))
{
ps->_state = HIT_BYSELF;
}
else
{
//正常移动
Move(ps, nextnode);
}
}
void GameRun(pSnack ps)
{
do
{
//显示得分情况
ShowScore(ps);
if (KEY_PRESS(VK_UP) && ps->_dire != DOWN)
ps->_dire = UP;
else if (KEY_PRESS(VK_DOWN) && ps->_dire != UP)
ps->_dire = DOWN;
else if (KEY_PRESS(VK_LEFT) && ps->_dire != RIGHT)
ps->_dire = LEFT;
else if (KEY_PRESS(VK_RIGHT) && ps->_dire != LEFT)
ps->_dire = RIGHT;
else if (KEY_PRESS(VK_SPACE))
SleepForever();
else if (KEY_PRESS(VK_ESCAPE))
ps->_state = END_NORMAL;
else if (KEY_PRESS(VK_F3))
{
//加速,每走一步休眠时间变短,得分更高
if (ps->_sleeptime > 50)
{
ps->_sleeptime -= 30;//不能一直加速,最多加速5次
ps->_foodweight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//加速,每走一步休眠时间变短,得分更高
if (ps->_sleeptime < 320)
{
ps->_sleeptime += 30;//不能一直加速,最多加速4次
ps->_foodweight -= 2;
}
}
SnackMove(ps);
ShowSnack(ps);
Sleep(ps->_sleeptime);
} while (ps->_state == OK);
}
void GameOver(pSnack ps)
{
SetPos(40, 12);
if (ps->_state == HIT_BYWALL)
printf("您已撞墙\n");
else if (ps->_state == HIT_BYSELF)
printf("您撞到了自己\n");
else if (ps->_state == END_NORMAL)
printf("您主动结束了游戏\n");
pSnackNode cur = ps->_SnackHead;
while (cur)
{
pSnackNode next = cur->next;
free(cur);
cur = next;
}
free(ps->_food);
ps->_food = NULL;
}
4.3.test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snack.h"
int main()
{
//将环境改为本地环境,方便宽字符的使用
setlocale(LC_ALL, "");
//调用srand函数,便于生成随机数
srand((unsigned int)time(NULL));
int ch = 0;
do
{
system("cls");
//定义一条蛇
Snack snack;
//游戏开始——准备工作
GameStart(&snack);
//游戏运行
GameRun(&snack);
//游戏结束
GameOver(&snack);
SetPos(40, 13);
wprintf(L"需要再来一局吗?(Y/N)");
scanf("%c", &ch);
while (getchar() != '\n')
;
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
return 0;
}