谈到贪吃蛇游戏,我们都不陌生,贪吃蛇游戏最早是由瑞典程序员伊维·达赫尔(Yevgeny Dvorzhak)在1979年开发的,它在1980年开始流行。今天我将使用Win11 VS2022社区版带领大家一步一步实现贪吃蛇,希望这能多多少少对大家有些帮助。
一、贪吃蛇游戏的预期实现结果
我们可以参考下面这个视频:
贪吃蛇实现效果
二、贪吃蛇实现的大致思路及Snake.h
结合平时玩贪吃蛇的经验,我们可以按照如下思路来实现贪吃蛇代码:
其中游戏开始部分是创建初始环境,欢迎信息和地图、蛇、食物信息。
游戏进行部分是核心,处理蛇的移动。
游戏结束部分是完成游戏的善后。
我们按照上述思路先写一份头文件,把思路先理一理:
//"Snake.h"
#pragma once
# include<stdio.h>
# include<stdlib.h>//动态内存开辟、时间函数
# include<stdbool.h>//布尔类型
# include<time.h>//时间函数
# include<windows.h>//需要用到Win32的方法
# include<locale.h>//设置本地环境,方便打印宽字符'□'等
# define KEY_STATE(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//定义按键宏,获取按键信息
# define WALL L'□'
# define BODY L'●'
# define FOOD L'★'
# define POS_X 24//定义初始x坐标
# define POS_Y 5//定义初始y坐标
//定义蛇节点
typedef struct SnakeNode
{
//坐标信息
int x;
int y;
struct SnakeNode* next;//方便链接下一个蛇节点
}SnakeNode, * pSnakeNode;
//定义方向状态
enum DIRECTION
{
UP,//向上
DOWN,//向下
LEFT,//向左
RIGHT//向右
};
//定义游戏状态
enum GAMESTATE
{
OK,//正常进行
BREAK_NORMAL,//正常退出
KILL_BY_WALL,//撞墙而死
KILL_BY_SELF//撞到自身而死
};
//定义整个蛇整体变量
typedef struct Snake
{
pSnakeNode _pSnake;//存放小蛇的头指针
pSnakeNode _pFood;//存放食物的地址
int _Count;//记录游戏总得分
int _FoodWeight;//记录当前食物的得分(由于蛇速度影响)
int _SleepTime;//存放蛇每走一个格子需要停顿的时间,停顿时间越短,蛇移动越快
enum DIRECTION _Dir;//存放蛇的运动方向
enum GAMESTATE _Game_State;//存放蛇的游戏状态
}Snake,*pSnake;
//设置坐标
void SetPos(int x, int y);
//隐藏光标
void ConsoleCursorHide();
//设置窗口大小以及标题
//游戏开始
void GameStart(pSnake ps);
//打印欢迎界面
void WelcomeToGame();
//打印地图
void CreateMap();
//初始化小蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行
void GameRun(pSnake ps);
//打印提示信息
void PrintHelpInfo();
//暂停
void Pause();//空格为暂停
//蛇的移动
void MoveSnake(pSnake ps);//更新蛇头等
//判断下一个是不是食物
bool IsNextFood(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);//销毁动态蛇节点内存
咱们为什么要定义比较复杂的结构体变量和枚举变量,这是为了减少因为使用太多的函数而增加实现难度,我们在下面就会感受到这样定义的好处。
三、基本知识
在打印欢迎页面之前,我们需要了解一个知识,就是我们使用VS2022运行的时候调用的命令提示框(黑框框)是有坐标的概念的,如图:
默认左上角的坐标是(0,0),而且x,y坐标的含义和数学的不太一样,具体如下:
在输出的时候,输出的英文字符占据1个x长度1个y长度,代表占据一个字节,而输出汉字占据两2个x单位1个y单位,代表占据两个字节。换算关系:2个x长度等于1个y长度。即随着数值的增大,坐标往y轴移动的速度是坐标往x轴移动的速度的2倍。
当谈到设置坐标以确保相关文字能在指定的位置输出,我们需要掌握一部分的Win32API知识,注意,这一部分涉及的函数仅仅在Windows操作系统下生效,在其他的如Linux、mac等不支持。
那么,WindowsAPI是干什么的呢?
# include<stdlib.h>
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
return 0;
}
即可以将窗口设置成往x轴方向100个x单位,y轴方向30个y单位,标题变为“贪吃蛇”,如下图:
变化前:
变化后:
为了确保我们能够成功更改标题,我们需要做如下设置:
Win11:
打开cmd,点击这个小三角:
点击设置:
点击该处,选择“Windows控制台主机”
保存并关闭
就设置成功了。
Win10:
打开cmd,鼠标划到“命令提示符”页面上端白色部分,右键后,点击“属性”
点击终端->默认终端改为Windows控制台主机,点击确定,再关掉cmd,就设置好了。
为了实现能够更改光标位置,在特定位置打印文字,我们需要在include<windows.h>的前提下实现以下两个函数:
//设置坐标
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获得输出句柄,方便设置位置
SetConsoleCursorPosition(hOutput, pos);//传入句柄和位置,设置光标位置
}
//隐藏光标
void ConsoleCursorHide()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO console_cursor_info;//定义鼠标结构体,控制光标时是否隐藏
GetConsoleCursorInfo(hOutput,&console_cursor_info);//获取输出句柄和鼠标信息
console_cursor_info.bVisible = false;//设置光标不可见
SetConsoleCursorInfo(hOutput, &console_cursor_info);//重新设置鼠标信息
}
我这里简单介绍一下句柄,句柄用来标识不同设备,获得句柄并将其传递个相关函数,方可更改光标属性(显示和不显示,显示的位置)。上述函数和数据类型都在windows.h中声明过了,我们只需要知道它们能够改变光标位置、设置光标不可见,并把上面的代码记一下会用就行了。如果想要进一步了解函数的具体使用方法,可以进一步查询微软官网。
光标隐藏可设置可不设置,不影响效果,但是设置光标位置是至关重要的。
以上是基本知识,我们下面具体实现。
为了方便调试、函数调用等,我们需要三个文件,分别是Snake.h(类型、定义、函数声明)、Snake.c(函数的实现)、test.c(测试函数)。
注意:若您想按照下面的函数块实现相同的效果,您不妨自己设置主函数,在您的Snake.c和test.c中包括我上面给到的头文件Snake.h,然后在主函数里面调用一下看看效果就行了。在以函数为单位产生效果的时候,可以不计较在您的Snake.c中的函数先后顺序,随后弄清楚每一个函数是在做什么,再在Snake.c中做进一步的封装就行了。
四、游戏开始GameStart()
按照逻辑,我们依次进行以下操作:
1.设置窗口大小和名称
在上文已经提及到,我们此处略过。
2.打印欢迎界面WelComeToGame()
为了实良好的人机交互页面,我们需要打印欢迎信息,具体代码如下:
//Snake.c
//打印欢迎界面
void WelcomeToGame()
{
//第一张页面
SetPos(40, 14);
printf("欢迎来到贪吃蛇!");
SetPos(40, 15);
system("pause");//暂停且显示“按任意键继续”
system("cls");//清除当前画面
//第二张页面
SetPos(20, 14);
printf("请使用 ↑ ↓ ← → 键控制蛇的移动");
SetPos(20, 15);
printf("F3加速,F4减速,ESC退出,空格暂停");
SetPos(40, 25);
system("pause");
system("cls");
}
由于我们设置的窗口大小是100*30,我们引入下面这张图您会更理解光标的位置:
效果对应视频里面的前两张页面:
3.打印地图CreateMap()
我们先把墙体打印好,我们定义的墙体的范围是x:0~57,y:0~27,示意图如下:
//Snake.c
# include"Snake.h"
//打印地图
void CreateMap()
{
SetPos(0, 0);//坐标归位
int i = 0;
//先打印最上面的墙
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc",WALL);
}
//再打印左右两边的墙
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
SetPos(56, i);
wprintf(L"%lc", WALL);
}
//最后打印最下面的墙
SetPos(0, 26);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
}
为了观看效果,到目前为止,您的主函数可以按照如下设置:
//test.c
# include"Snake.h"
//# include<locale.h>
//# include<windows.h>//在Snake.h中已经有定义,此处可以不写,仅方便演示
int main()
{
setlocale(LC_ALL, "");
//srand((unsigned int)time(NULL));
test();//传您的测试函数
return 0;
}
打印的效果如下:
注意:为什么我在头文件中# define WALL L"□" ,是为了在需要更换墙体时方便一键替换,下文的BODY和FOOD同理,且方便记忆。
4.初始化小蛇InitSnake()
其核心是动态开辟若干块内存,并打印蛇。这里需要用上链表的相关知识。
代码如下:
//Snake.c
# include"Snake.c"
//初始化小蛇
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;//坐标必须是隔2,否则打印错位
cur->y = POS_Y;//存储坐标
cur->next = NULL;
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);//记得要打印
//典型的链表头插拼接
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
ps->_Count = 0;//设置初始得分为0
ps->_FoodWeight = 10;//设置初始吃掉单个食物的分数为10
ps->_SleepTime = 300;//设置初始休眠时间为300
ps->_Dir = RIGHT;//设置蛇的初始运动方向为向右
ps->_Game_State = OK;//设置游戏状态为正常
ps->_pFood = NULL;//设置食物的地址为空
//顺便初始化其他参数
}
注意:您在主函数测试该函数的时候,别忘了在主函数中初始化Snake ps = {0},将ps的地址传入该函数,或者在主函数中pSnake ps = (pSnake)malloc(sizeof(Snake)),将ps->的所有成员初始化为0后直接将ps传入该函数。我这里以第二种为例。
预期效果如下:
5.创建食物CreateFood()
创建食物的时候,我们要注意:
1.食物不能和墙重合,且食物不能出墙,不能和蛇本身重合。
2.食物的x坐标必须是2的倍数,否则打印错位。
3.食物的出现是随机的。
具体的实现代码如下:
//Snake.c
//创建食物
void CreateFood(pSnake ps)
{
int x = 0, y = 0;
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
pSnakeNode cur = ps->_pSnake;
repeat:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);//x必须是偶数
while (cur)//遍历蛇
{
if (cur->x == x && cur->y == y)
{
goto repeat;//如果坐标和蛇重合,那么必须重新生成随机数
}
cur = cur->next;
}
pFood->x = x;
pFood->y = y;
ps->_pFood = pFood;//获取食物正确坐标后,赋给ps->_pFood
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
注意:我们用上了随机数,因此,我们需要包括stdlib.h,但这已经在Snake.h中导入了。您在主函数中应当包含srand((unsigned int)time(NULL)),以确保随机数的生成。
实现的效果如下:
食物是随机生成的。
到目前为止,小分块已经完成,我们将以上几个小部分放入GameStart()中,方便主函数调用他们:
//"Snake.c"
# include"Snake.h"
//游戏开始
void GameStart(pSnake ps)
{
//设置窗口大小以及标题
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//ConsoleCursorHide();//隐藏光标,可隐藏可不隐藏
WelcomeToGame();//打印欢迎界面
CreateMap();//打印地图
InitSnake(ps);//初始化小蛇
CreateFood(ps);//创建食物
}
五、游戏运行GameRun()
在保留上述函数的前提下,我们接着实现运行阶段。
为了便于理解,我们直接从GameRun()的框架入手。进入运行阶段,肯定要检测按键的情况。大体逻辑如下:
//Snake.c
# include"Snake.h"
void GameRun(pSnake ps)
{
PrintHelpInfo();//再次打印页面提示信息,防止玩家忘记游戏规则
do {
SetPos(64, 10);
printf("您的得分为:%05d", ps->_Count);
SetPos(64, 11);
printf("单个食物的分数为:%d", ps->_FoodWeight);//需要不断打印得分情况
if (KEY_STATE(VK_LEFT) && ps->_Dir != RIGHT)//检测运动和按键状态
{
ps->_Dir = LEFT;
}
else if (KEY_STATE(VK_RIGHT) && ps->_Dir != LEFT)
{
ps->_Dir = RIGHT;
}
else if (KEY_STATE(VK_UP) && ps->_Dir != DOWN)
{
ps->_Dir = UP;
}
else if (KEY_STATE(VK_DOWN) && ps->_Dir != UP)
{
ps->_Dir = DOWN;
}
else if (KEY_STATE(VK_F3))
{
if (ps->_SleepTime >= 100)
{
ps->_SleepTime -= 30;
ps->_FoodWeight += 2;
}
}
else if (KEY_STATE(VK_F4))
{
if (ps->_SleepTime <= 300)
{
ps->_SleepTime += 30;
ps->_FoodWeight -= 2;
}
}
else if (KEY_STATE(VK_ESCAPE))
{
ps->_Game_State = BREAK_NORMAL;
}
else if (KEY_STATE(VK_SPACE))
{
Pause();
}
Sleep(ps->_SleepTime);
MoveSnake(ps);
} while (ps->_Game_State == OK);
SetPos(20, 10);
if (ps->_Game_State == KILL_BY_WALL)
{
printf("您撞墙而死,游戏结束!");
}
else if (ps->_Game_State == KILL_BY_SELF)
{
printf("您自杀而死,游戏结束!");
}
}
注意:
1.在实际的贪吃蛇中,不支持蛇直走还能直接掉头的情况,故在上述代码中我们不支持这种逻辑。
2.打印提示信息的目的是防止玩家在游戏过程中忘记游戏规则。
3.检测按键需要用到GetAsyncKeyState()函数,为了回顾宏的知识、三元运算符、表达简便,我们使用KEY_STATE(VK)宏来封装GetAsyncKeyState()函数。
4.检测蛇的下一步移动是否合法(合法即没有挂,但判断有没有吃食物也比较复杂)(不合法即撞墙、自杀),逻辑较为复杂,需要重点考虑。
5.得分需要实时更新。
6.为了激励按F3加速的玩家和惩罚按F4减速的玩家,在休眠时间合理的情况下,对单个食物得分做出一定的调整。
下面我们逐个实现函数。
1.打印页面提示信息PrintHelpInfo()
其目的是打印游戏规则,防止玩家忘记,代码如下:
//Snake.c
#include"Snake.h"
//打印提示信息
void PrintHelpInfo()
{
SetPos(64, 17);
printf("1.按 ↑ ↓ ← → 控制蛇的移动");
SetPos(64, 18);
printf("2.不能撞墙,不能吃到自己");
SetPos(64, 19);
printf("3.按F3加速,F4减速");
SetPos(64, 20);
printf("4.按ESC退出,空格暂停");
}
效果如下:
2.检测按键情况GetAsyncKeyState()
定义的宏如下:
# define KEY_STATE(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)//定义按键宏,获取按键信息
这在Snake.h中已经定义过。
GetAsyncKeyState(VK)用于检测按键是否被用过,如果按过,其按位与0x1得到1,否则得到零,这使得其套用三目表达式时方便。
3.暂停Pause()
其目的是当玩家按下空格键时,停止但不结束游戏,其实现如下:
//"Snake.c"
# include"Snake.h"
void Pause()//空格为暂停
{
while(1)
{
Sleep(200);
if (KEY_STATE(VK_SPACE))
{
break;
}
}
}
解释:当玩家按下空格键时,函数从GameRun()进入Pause();当玩家再按下空格时,自动跳出暂停,游戏继续。
4.蛇移动MoveSnake()
为了便于描述蛇如何运动,我将IsNextFood()、EatFood()、NoFood()、KILLBYWALL()、KILLBYSELF()放入MoveSnake(),代码如下:
//蛇的移动
void MoveSnake(pSnake ps)//更新蛇头等
{
//判断蛇的状态,有没有撞墙,有没有自杀,有没有吃到食物
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL)
{
perror("MoveSnake()::malloc()");
return;
}
//考虑到蛇要走下一步,那么我们要先求出下一步是什么
switch (ps->_Dir)
{
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;
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;
}//这样就求得下一个点的信息了
if (IsNextFood(ps, pNext))//如果下一步是食物
{
EatFood(ps, pNext);
}
else//没有吃到食物
{
NoFood(ps, pNext);
}
KillByWall(ps);//检查是否撞墙
KillBySelf(ps);//检查是否自杀了
}
解释:
1.由于蛇要走向下一步,因此我们需要开辟额外的空间给下一个节点。
2.在创建好空间的同时,获取相应的坐标,并移动光标打印蛇身,再进行下一步判断。
5.下一个点是不是食物IsNextFood()
如果下一个点是食物,返回true,否则返回false,当然这是布尔类型,需要include<stdbool.h>才能正常使用。
代码如下:
//Snake.c
# include"Snake.h"
bool IsNextFood(pSnake ps, pSnakeNode pNext)//比较下一个位置和食物的位置
{
if (ps->_pFood->x == pNext->x && ps->_pFood->y == pNext->y)
{
return true;
}
return false;
}
6.吃食物EatFood()
当下一个点是食物的时候,蛇要吃掉食物,同时蛇的长度会增加1,因此需要将被吃掉的食物的节点转换为蛇的头结点,并存储、打印相关坐标。同时,蛇吃掉了食物,食物需要重新生成,因此需要再次调用上文已经写好的CreateFood().
代码如下:
//Snake.c
# include"Snake.h"
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext)//覆盖、更新食物
{
ps->_Count += ps->_FoodWeight;//加上食物的分数
pSnakeNode cur = NULL;
pNext->next = ps->_pSnake;
ps->_pSnake = pNext;//头插,将被吃掉的食物的节点转换为蛇的头结点
cur = ps->_pSnake;
while (cur)//遍历,打印蛇身
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//创建新食物
CreateFood(ps);
}
通过坐标的不断调用,我们不难理解为什么要定义pSnakeNode或SnakeNode,原因就是既能存储相关坐标,在内存上又连在一起,方便连续访问内存。
我们来看一下效果:
吃掉食物前:
吃掉食物后:
不难发现得分也增加了。
7.不吃食物NoFood()
如果下一个点不是食物,那下一个点成为蛇的心头结点,并打印蛇身,且蛇的尾将要去掉1,因为蛇的长度不能改变。因此,在蛇尾处,我们需要打印两个英文空格将蛇尾抹掉,并且释放掉蛇尾内存。涉及到链表的头插和尾删操作,代码如下:
//不吃食物
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;
}
//留下cur->next为尾结点,释放
SetPos(cur->next->x, cur->next->y);
free(cur->next);
cur->next = NULL;
printf(" ");//打印两个空格补充
}
注意:为什么判断条件是while(cur->next->next)呢,因为我要释放最后一个节点,此时退出循环后cur指向倒数第二个节点,让cur->next释放并置为空就行了,这是常见的链表尾删操作。
8.判断是否撞到墙KILLBYWALL()
怎么判断有没有撞到墙呢?
将墙的图拿过来,不难发现(蛇头坐标)当x == 0 或 x == 56 或 y == 0 或 y == 26时,蛇撞墙了,游戏状态也要变为KILL_BY_WALL使得游戏终止了,如图所示:
代码如下:
//Snake.c
# include"Snake.h"
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
{
ps->_Game_State = KILL_BY_WALL;
}
}
9.判断是否撞到自己KILLBYSELF()
判断蛇是否撞到自己时,由于我们定义的蛇节点中存储了内存信息和坐标信息,理论上比较内存指针是否相同或者比较坐标是否重合都可以,在这里我们选择遍历比较坐标,代码如下:
//Snake.c
# include"Snake.h"
//自杀
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;//从头的下一个开始哈,要不然走一步就寄了
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_Game_State = KILL_BY_SELF;
}
cur = cur->next;
}
}
注意:cur应当从蛇的头结点的下一个节点开始遍历而不是头结点,否则直接自杀,因为这是恒成立的;且注意结点要不断指向下一个,否则死循环。
至此,在MoveSnake()里的IsNextFood()、EatFood()、NoFood()、KILLBYWALL()、KILLBYSELF()全部完成,GameRun()也完成实现。
效果可以参考上方视频。
六、游戏结束GameEnd()
这里的游戏结束比较简单,就是将之前为蛇malloc动态开辟的所有空间释放掉,并置为空。代码如下:
//Snake.c
# include"Snake.h"
//游戏结束
void GameEnd(pSnake ps)//销毁动态蛇节点内存
{
pSnakeNode cur = ps->_pSnake;
pSnakeNode del = cur;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
注意:这里为什么要设置cur和del呢?若不定义del,cur释放置空后就找不到下一个节点了,造成了内存泄漏,因此要cur = cur->next后 再释放del。
七、Snake.c的整理
通过以上三大函数GameStart()、GameRun()、GameEnd()的编写,我们不难将其整理为完整的Snake.c,代码如下:
//Snake.c
# include"Snake.h"
//设置坐标
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获得输出柄,方便设置位置
SetConsoleCursorPosition(hOutput, pos);//传入操作柄和位置,设置光标位置
}
//隐藏光标
void ConsoleCursorHide()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO console_cursor_info;//定义鼠标结构体,控制光标时是否隐藏
GetConsoleCursorInfo(hOutput,&console_cursor_info);//获取输出柄和鼠标信息
console_cursor_info.bVisible = false;//设置光标不可见
SetConsoleCursorInfo(hOutput, &console_cursor_info);//重新设置鼠标信息
}
//游戏开始
void GameStart(pSnake ps)
{
//设置窗口大小以及标题
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//ConsoleCursorHide();//隐藏光标
WelcomeToGame();//打印欢迎界面
CreateMap();//打印地图
InitSnake(ps);//初始化小蛇
CreateFood(ps);//创建食物
}
//打印欢迎界面
void WelcomeToGame()
{
//第一张页面
SetPos(40, 14);
printf("欢迎来到贪吃蛇!");
SetPos(40, 15);
system("pause");//暂停且显示“按任意键继续”
system("cls");
//第二张页面
SetPos(20, 14);
printf("请使用 ↑ ↓ ← → 键控制蛇的移动");
SetPos(20, 15);
printf("F3加速,F4减速,ESC退出,空格暂停");
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);
}
//再打印左右两边的墙
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
SetPos(56, i);
wprintf(L"%lc", WALL);
}
//最后打印最下面的墙
SetPos(0, 26);
for (i = 0; i <= 56; i += 2)
{
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;
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);//记得要打印
//拼接
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
ps->_Count = 0;
ps->_FoodWeight = 10;
ps->_SleepTime = 300;
ps->_Dir = RIGHT;
ps->_Game_State = OK;
ps->_pFood = NULL;
}
//创建食物
void CreateFood(pSnake ps)
{
int x = 0, y = 0;
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
pSnakeNode cur = ps->_pSnake;
repeat:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);//x必须是偶数
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto repeat;
}
cur = cur->next;
}
pFood->x = x;
pFood->y = y;
ps->_pFood = pFood;
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
//游戏运行
void GameRun(pSnake ps)
{
PrintHelpInfo();
do {
SetPos(64, 10);
printf("您的得分为:%05d", ps->_Count);
SetPos(64, 11);
printf("单个食物的分数为:%d", ps->_FoodWeight);
if (KEY_STATE(VK_LEFT) && ps->_Dir != RIGHT)
{
ps->_Dir = LEFT;
}
else if (KEY_STATE(VK_RIGHT) && ps->_Dir != LEFT)
{
ps->_Dir = RIGHT;
}
else if (KEY_STATE(VK_UP) && ps->_Dir != DOWN)
{
ps->_Dir = UP;
}
else if (KEY_STATE(VK_DOWN) && ps->_Dir != UP)
{
ps->_Dir = DOWN;
}
else if (KEY_STATE(VK_F3))
{
if (ps->_SleepTime >= 100)
{
ps->_SleepTime -= 30;
ps->_FoodWeight += 2;
}
}
else if (KEY_STATE(VK_F4))
{
if (ps->_SleepTime <= 300)
{
ps->_SleepTime += 30;
ps->_FoodWeight -= 2;
}
}
else if (KEY_STATE(VK_ESCAPE))
{
ps->_Game_State = BREAK_NORMAL;
}
else if (KEY_STATE(VK_SPACE))
{
Pause();
}
Sleep(ps->_SleepTime);
MoveSnake(ps);
} while (ps->_Game_State == OK);
SetPos(20, 10);
if (ps->_Game_State == KILL_BY_WALL)
{
printf("您撞墙而死,游戏结束!");
}
else if (ps->_Game_State == KILL_BY_SELF)
{
printf("您自杀而死,游戏结束!");
}
}
//打印提示信息
void PrintHelpInfo()
{
SetPos(64, 17);
printf("1.按 ↑ ↓ ← → 控制蛇的移动");
SetPos(64, 18);
printf("2.不能撞墙,不能吃到自己");
SetPos(64, 19);
printf("3.按F3加速,F4减速");
SetPos(64, 20);
printf("4.按ESC退出,空格暂停");
}
//暂停
void Pause()//空格为暂停
{
while(1)
{
Sleep(200);
if (KEY_STATE(VK_SPACE))
{
break;
}
}
}
//蛇的移动
void MoveSnake(pSnake ps)//更新蛇头等
{
//判断蛇的状态,有没有撞墙,有没有自杀,有没有吃到食物
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL)
{
perror("MoveSnake()::malloc()");
return;
}
//考虑到蛇要走下一步,那么我们要先求出下一步是什么
switch (ps->_Dir)
{
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;
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;
}//这样就求得下一个点的信息了
if (IsNextFood(ps, pNext))//如果下一步是食物
{
EatFood(ps, pNext);
}
else//没有吃到食物
{
NoFood(ps, pNext);
}
KillByWall(ps);//检查是否撞墙
KillBySelf(ps);//检查是否自杀了
}
//判断下一个是不是食物
bool IsNextFood(pSnake ps, pSnakeNode pNext)//比较下一个位置和食物的位置
{
if (ps->_pFood->x == pNext->x && ps->_pFood->y == pNext->y)
{
return true;
}
return false;
}
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pNext)//覆盖、更新食物
{
ps->_Count += ps->_FoodWeight;//加上食物的分数
pSnakeNode cur = NULL;
pNext->next = ps->_pSnake;
ps->_pSnake = pNext;//头插
cur = ps->_pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//创建新食物
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;
}
//留下cur->next为尾结点,释放
SetPos(cur->next->x, cur->next->y);
free(cur->next);
cur->next = NULL;
printf(" ");//打印两个空格补充
}
//撞墙
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 || ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
{
ps->_Game_State = KILL_BY_WALL;
}
}
//自杀
void KillBySelf(pSnake ps)
{
pSnakeNode cur = ps->_pSnake->next;//从头的下一个开始哈,要不然走一步就寄了
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_Game_State = KILL_BY_SELF;
}
cur = cur->next;
}
}
//游戏结束
void GameEnd(pSnake ps)//销毁动态蛇节点内存
{
pSnakeNode cur = ps->_pSnake;
pSnakeNode del = cur;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
八、test.c的整理
还有一个部分是询问玩家要不要继续游戏,这一部分放在主函数里面比较好,如果选择继续玩就重新开始,选择退出就彻底终止程序,因此考虑使用do-while循环,代码如下:
//test.c
# include"Snake.h"
void test();//测试函数
int main()
{
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL));
test();
return 0;
}
void test()
{
pSnake ps = (pSnake)malloc(sizeof(Snake));
if (ps == NULL)
{
perror("test()::malloc()");
return;
}
int ch = 0;
do
{
//初始化为0
ps->_pSnake = NULL;
ps->_Count = 0;
ps->_Dir = RIGHT;
ps->_FoodWeight = 0;
ps->_pFood = NULL;
ps->_SleepTime = 0;
ps->_Game_State = OK;
//游戏开始
GameStart(ps);
//游戏运行
GameRun(ps);
//游戏结束
GameEnd(ps);
SetPos(20, 12);
printf("再来一局?(Y/N)");
SetPos(36, 12);
ch = getchar();
getchar();//吸收回车
} while (ch == 'Y' || ch == 'y');//小写和大写都能够继续玩
SetPos(0, 27);//方便在最下面打印退出信息,美观
free(ps);
ps = NULL;
}
九、总结
完成像贪吃蛇一样的项目,逻辑思维至关重要,只有在实现项目之前明白大概需要哪些函数,定义哪些变量,等等,才能够较为快速的实现代码;本次实现代码,我们大量使用了结构体、指针、动态内存开辟,这些是接下来我们学习数据结构的基础部分;我的代码可能还存在很多可以优化的地方,还请各位大佬指正,谢谢。