当前位置: 首页 > article >正文

【C语言数据结构初阶】---基于单链表在控制台上实现贪吃蛇小游戏

1.要实现的功能

• 贪吃蛇地图绘制
• 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
最终效果展示:
在这里插入图片描述

2.游戏逻辑

在这里插入图片描述
主逻辑分为3个过程:
• 游戏开始(GameStart)完成游戏的初始化
• 游戏运⾏(GameRun)完成游戏运⾏逻辑的实现
• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

3.游戏开始(GameStart)

3.1设置游戏窗口大小和窗口名字

//设置游戏窗口大小
system("mode con cols=100 lines=31");
//设置窗口的名字
system("title:贪吃蛇");

cols是列,lines是行,我们把控制台的大小设置成了31行100列
效果展示
在这里插入图片描述

3.2隐藏屏幕光标

当我们运行一段代码时我们会在控制台上发现一个一直闪烁的光标,当我们想要实现一个贪吃蛇游戏的时候,蛇在那里走,玩家还能看见光标一直在那里闪烁,这样子很影响游戏体验,所以我们要想办法隐藏这个光标。
在这里插入图片描述
实现代码如下:

//隐藏屏幕光标
HANDLE hOutPut = NULL;
hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);//获取屏幕句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutPut, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false;//修改光标可见度
SetConsoleCursorInfo(hOutPut, &CursorInfo);//设置光标可见度

上面代码中HANDLE是一个void*的类型,而GetStdHandle是⼀个WindowsAPI函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
因为我们这边是需要获取控制台上的信息,也就是从标准输出中获取句柄,因此我们传的是STD_OUTPUT_HANDLE
在这里插入图片描述
在这里插入图片描述
CONSOLE_CURSOR_INFO是一个结构体,结构体具体形式如下:
在这里插入图片描述
GetConsoleCursorInf也是⼀个WindowsAPI函数。他接受两个参数,一个是标准设备的句柄,一个是CONSOLE_CURSOR_INFO类型指针,我们用这个函数是为了将控制台上的光标信息放到结构体CONSOLE_CURSOR_INFO类型变量CursorInfo中。
然后CursorInfo.bVisible = false;这段代码的意思是将光标的可见度改成false,也就是不可见,这样还没完,我们还要设置一下光标信息才行,如下:SetConsoleCursorInfo(hOutPut, &CursorInfo);//设置光标可见度
SetConsoleCursorInfo也是一个WindowsAPI函数 ,这个函数有两个参数,第一个是标准设备的句柄,一个是CONSOLE_CURSOR_INFO类型指针,这样子我们就将false这个可见度信息写入了,这样子光标就不可见了。

3.3打印欢迎界面

我们要在控制台的中间打印欢迎信息,但我们怎么在这个位置进行打印呢?我们需要把光标移动到这个位置,因此我们还要专门写一个设置光标位置的函数,方便后续我们经常使用。

//设置光标位置
void SetPos(short x, short y)
{
    COORD pos = { x, y };
    HANDLE hOutPut = NULL;
    hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄
    SetConsoleCursorPosition(hOutPut, pos);//设置光标位置
}

COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。SetConsoleCursorPosition是一个WindowsAPI函数,接收两个参数,第一个参数是标准设备的句柄,第二个参数是COORD类型的结构体变量。我们可以用这个函数把光标设置到控制台的(x,y)处。
在这里插入图片描述
在这里插入图片描述

有了设置光标的这个函数后,我们就可以开始打印欢迎界面了,我们先设置光标位置到我们想要的位置,我这边设置的是(40,14),然后我们打印一段话:“欢迎来到贪吃蛇游戏
system(“pause”)的作用是暂停程序,如果我们想要继续执行程序我们在键盘上随便按一个键就可以继续运行。
system(“cls”);的作用是清屏,就是把当前屏幕上打印的信息全部清理掉,我们清理屏幕是为了在屏幕中间打印游戏的按键信息,因为之前在中间打印了欢迎信息,所以我们需要进行清理,当然我们再次设置光标位置才能在屏幕中间打印信息。

//打印欢迎界面
void WelcomeToGame()
{
    //设置光标位置
    SetPos(40, 14);
    printf("欢迎来到贪吃蛇游戏!\n");
    SetPos(40, 15);
    system("pause");//暂停
    system("cls");//清屏
    //打印游戏按键信息
    SetPos(25, 14);
    printf("用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
    SetPos(40, 15);
    system("pause");
    system("cls");
}

效果如下:
在这里插入图片描述

3.4创建游戏地图

地图指的是这四个边框。
在这里插入图片描述
创建地图我们就在屏幕相应位置打印字符’□’就行了,不过这个字符是宽字符,什么是宽字符呢?
在这里插入图片描述
想要打印宽字符,我们得先将程序本地化。
setlocale函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:“C”(正常模式)和""(本地模式)。
在任意程序执⾏开始,都会隐藏式执⾏调⽤。
如下:

//设置本地环境
setlocale(LC_ALL, "");

设置本地环境完后我们如何打印宽字符呢?形式如下:


 wchar_t ch1 = L'★';
 wprintf(L"%lc\n", ch1);

知道了如何打印宽字符后我们就可以开始打印地图了,我们先打印上面那条边,然后打印下面那条边,然后打印左右两条边。
参考代码如下:

//创建游戏地图
void CreatMap()
{
    //打印上面
    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);

    }
}

里面这个WALL是我们特殊定义的,为的是方便我们使用,不然我们还要特地去找这个符号,此外我们还定义了蛇身和食物的符号,具体如下:

#define WALL L'□' //墙体符号
#define BODY L'●' //蛇身符号
#define FOOD L'★' //食物符号

3.5初始化蛇身

初始化蛇身之前我们先定义一下蛇身节点的结构,结构如下:

//定义蛇身的结构
//坐标和指向下一个节点的指针
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

除了蛇身节点结构的定义外,我们不妨再定义一个结构体用于存储蛇的数据。
在这个结构体中我们会存储一个指向蛇头的指针、一个指向食物的指针、蛇头的运动方向、游戏的状态、游戏当前获得的总分数、每个食物的分数和蛇每走一步所休息的时间。
具体如下:

//枚举出蛇头的方向
enum DIRECTION
{
	UP,//上
	DOWN,//下
	LEFT,//左
	RIGHT//右
};

//枚举出游戏目前的状态
enum GAME_STATUS
{
	OK,//正常运行
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//按esc正常退出
};

//维护蛇的信息
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头的指针
	pSnakeNode _pFood;//指向食物的指针
	enum DIRECTION _Dir;//蛇头的方向,默认是右
	enum GAME_STATUS _Status;//游戏状态
	int _Socre;//游戏当前获得的分数
	int _foodWight;//每个食物的分数,默认是10分
	int _sleepTime;//蛇每走一步休息的时间,默认是200ms
}Snake, * pSnake;

有了这两个结构后我们就可以开始初始化蛇身了,游戏一开始的蛇蛇身长度为5,所以我们动态申请5个蛇身节点,并且我们采用单链表头插的方式将5个节点连接起来。
连接完成后我们先设置一下光标位置,然后利用蛇的头节点在屏幕上依次打印蛇身。
打印完蛇身后我们再初始化一下蛇的信息:
• 我们将蛇头的方向初始化为向右
• 将每个食物的分数初始化为10分
• 蛇每走一步休息的时间设置成200ms,也就是0.2秒
• 将游戏总分初始化为0分
• 将游戏的状态初始化为正常运行(OK)

//初始化蛇身
void InitSnake(pSnake ps)
{
    //申请五个节点
    for (int i = 24; i <= 32; i += 2)
    {
        pSnakeNode Node = ByNode(i, 5);
        if (ps->_pSnake == NULL)
        {
            ps->_pSnake = Node;
        }
        else
        {
            //头插
            Node->next = ps->_pSnake;
            ps->_pSnake = Node;
        }
    }
    //在屏幕上打印蛇身
    pSnakeNode cur = ps->_pSnake;
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //初始化一下蛇的数据
    ps->_Dir = RIGHT;//默认向右
    ps->_foodWight = 10;//每个食物10分
    ps->_sleepTime = 200;//走一次休息200ms
    ps->_Socre = 0;//总分是0
    ps->_Status = OK;//正常运行
}

3.6创建食物

我们玩贪吃蛇的时候,蛇每吃掉一个食物,屏幕上是不是就会随机再生成一个食物。我们初始化这个食物的时候,这个食物也是随机生成在地图上的某个位置的。
而在屏幕上随机一个位置(x,y)我们是不是就要想办法随机生成出x,y两个随机数呀,怎么样能得到这两个随机数呢?办法如下:

srand((unsigned int)time(NULL));//设置种子
x = rand() % 53 + 2;
y = rand() % 25 + 1;

rand,srand,time这几个函数的作用我在之前的博客中有做介绍,博客链接如下:
链接: 简单猜数字游戏

知道了如何生成随机数我们接下来就好办了,不过我们对生成的随机数也是有要求的
• 生成的(x,y)坐标不能与蛇身坐标重合
• 生成的(x,y)坐标得在地图范围内
• 生成的(x,y)坐标中x得是2的倍数,为什么要是2的倍数呢?
我们可以看到,右边的这个墙体是从第56列打印的,如果我们生成的x是55,又因为我们的食物符号’★’也是个宽字符,它打印出来会占一行两列,这样子食物岂不是有一半在墙里吗?,所以我们要保证是2的倍数
在这里插入图片描述
代码实现相对简单,不过需要注意的是:
x = rand() % 53 + 2;
y = rand() % 25 + 1;
我们为什么要把rand生成的随机数模上一个数再加上一个数呢?我们参考上面这张地图,我们发现我们食物得再地图范围内生成,那我们x的范围得[2,54],y的范围得是[1,25],一个数模上53,模完后剩下的数范围是不是就在[0,52],再+2,范围就变成了[2,54],y也同理。
参考代码如下:

//创建食物
void CreatFood(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;
    }
    //生成好了
    //为食物申请空间
    ps->_pFood = ByNode(x, y);
    //打印食物
    SetPos(x, y);
    wprintf(L"%lc", FOOD);
}
// A code block
var foo = 'bar';

4.游戏运⾏(GameRun)

4.1在地图右侧打印帮助信息

为了避免玩家忘了怎么操作蛇,我们会在地图右侧打印帮助信息
具体如下:

//右侧打印帮助信息
void PrintHelpInfo()
{
    //帮助信息
    SetPos(64, 14);
    wprintf(L"%ls", L"不能穿墙,不能咬到自己");
    SetPos(64, 15);
    wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
    SetPos(64, 16);
    wprintf(L"%ls", L"按F3加速,F4减速");
    SetPos(64, 17);
    wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
    SetPos(64, 19);
    wprintf(L"%ls", L"制作by:小王C语言");
    SetPos(64, 20);
}

4.2打印当前总分和每个食物的分数

这个分数也是打印在地图右侧,需要注意的是这段代码是写在循环中的,因为蛇吃到食物或者蛇加速减速会导致分数变化。

//打印已经获得的分数和每个食物的分数
SetPos(64, 11);
printf("总分:%d", ps->_Socre);
SetPos(64, 12);
printf("每个食物的分数:%2d", ps->_foodWight);

4.3根据按键情况移动蛇

4.3.1获取按键情况

下方代码中出现的KEY_PRESS和VK_UP等是什么意思我在这先介绍下
首先KEY_PRESS是我们定义的一个东西,如下:

//按键被按下,最低为是1,与上1结果就是1
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)

GetAsyncKeyState是一个WindowsAPI函数,这个函数的作用是将键盘上的某个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。VK就表示某个键的虚拟键值。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.因此我们可以用三木操作符来进行判断(GetAsyncKeyState(VK) & 0x1) ? 1 : 0
我们将这个返回的short值与上一个十六进制的1就能得到最低位,最低为如果是1,与的结果就是1,反之为0
需要的虚拟按键的罗列:
• 上:VK_UP
• 下:VK_DOWN
• 左:VK_LEFT
• 右:VK_RIGHT
• 空格:VK_SPACE
• ESC:VK_ESCAPE
• F3:VK_F3
• F4:VK_F4
知道了这些后,下方的代码我们就可以理解了:

//获取按键情况 
//蛇不能180度转弯
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->_Status = END_NORMAL;
}
//加速,一次休息时间-30,休息时间最少是80,加速一次食物分数+2
else if (KEY_PRESS(VK_F3))
{
    if (ps->_sleepTime > 80)
    {
        ps->_sleepTime -= 30;
        ps->_foodWight += 2;
    }
}
//减速,减速一次食物分数-2,食物分数最少是2
else if (KEY_PRESS(VK_F4))
{
    if (ps->_foodWight > 2)
    {
        ps->_sleepTime += 30;
        ps->_foodWight -= 2;
    }
}

4.3.2根据蛇头的方向,计算下一个节点的坐标

根据蛇头的方向我们就可以计算下一个节点的坐标(x,y)
参考代码如下:

void SnakeMove(pSnake ps)
{
    int x = 0;
    int y = 0;
    //计算下一个节点的坐标
    if (ps->_Dir == UP)
    {
        x = ps->_pSnake->x;
        y = ps->_pSnake->y - 1;
    }
    else if (ps->_Dir == DOWN)
    {
        x = ps->_pSnake->x;
        y = ps->_pSnake->y + 1;
    }
    else if (ps->_Dir == LEFT)
    {
        x = ps->_pSnake->x - 2;
        y = ps->_pSnake->y;
    }
    else if (ps->_Dir == RIGHT)
    {
        x = ps->_pSnake->x + 2;
        y = ps->_pSnake->y;
    }
}

4.3.3判断下一个节点是否是食物

下一个节点坐标(x,y)和食物坐标(x,y)一样说明下一个节点是食物,是食物我们返回1,反之返回0

//判断下一个节点是否是食物
int NextIsFood(pSnake ps, int x, int y)
{
    return (x == ps->_pFood->x && y == ps->_pFood->y);
}

4.3.4下一个坐标是食物,吃掉食物

如果下一个坐标是食物,我们吃掉食物,怎么个吃法呢?
首先我们要知道我们申请的这个食物是和蛇身一样的结构体指针,吃掉食物就是让我们将这个食物用单链表的头插法头插到蛇身中从而成为新的蛇头,然后从蛇头开始重新打印坐标直到尾节点。
需要注意的是吃掉食物后我们要对游戏总分进行++,然后还要再创造出一个食物到地图中。
参考代码如下:

//是食物,吃掉食物
void EatFood(pSnake ps)
{
    //食物节点变成头节点
    ps->_pFood->next = ps->_pSnake;
    ps->_pSnake = ps->_pFood;
    //打印蛇身
    pSnakeNode cur = ps->_pSnake;
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //总分++
    ps->_Socre += ps->_foodWight;
    //再次创建食物
    CreatFood(ps);
}

4.3.5不是食物,蛇移动一步

如果下一个节点不是食物,我们只需要将蛇向着下一个坐标移动一步即可,但是怎么移动呢?其实和上面吃掉食物的逻辑一样,我们先要创造下一个坐标的节点,然后把这个节点头插到我们的链表中,然它成为新的头节点,然后从这个节点开始向后打印蛇身。
需要注意的是,因为我们没有吃食物,所以我们蛇身的长度是会发生改变的,而又因为我们没有进行清屏操作,所以如果我们不对原本蛇身的最后一个位置进行处理,那个蛇身符号’●’是不是会一直留在原处呀,所以我们要让他消失,怎么才能让他消失呢?很简单,我们直接在这个位置打印两个空格是不是就将原本蛇身的符号’●’覆盖了呀。
参考代码如下:

//不是食物,蛇移动一步
void NoFood(pSnake ps, int x, int y)
{
    //创建新节点
    pSnakeNode Node = ByNode(x, y);
    //下一个节点变成头节点
    Node->next = ps->_pSnake;
    ps->_pSnake = Node;
    //打印蛇身
    pSnakeNode cur = ps->_pSnake;
    while (cur->next->next)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //不用打印,这个节点之前以及打印过了
    //SetPos(cur->x, cur->y);
    //wprintf(L"%lc", BODY);
    //最后一个节点打印两个空格
    SetPos(cur->next->x, cur->next->y);
    printf("  ");
    //释放最后一个节点
    free(cur->next);
    //将倒数第二个节点的next置为空,因为蛇移动一步后,倒数第二个节点就是最后一个节点
    cur->next = NULL;
}

4.3.6判断蛇是否撞墙

如果蛇头的坐标与墙体坐标重合了是不是就代表蛇撞墙了呀,这个很好判断
参考代码如下:

//判断是否撞墙
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.7判断蛇是否撞上自己

判断蛇是否撞上自己和判断撞墙一样,如果蛇头的坐标和除蛇头外的其他蛇身坐标重合了就代表蛇头撞到自己了
参考代码如下:

//判断是否撞上自己
void KillBySelf(pSnake ps)
{
    //蛇头节点和蛇身节点相同
    pSnakeNode cur = ps->_pSnake->next;
    while (cur)
    {
        if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
        {
            ps->_Status = KILL_BY_SELF;
        }
        cur = cur->next;
    }
}

4.4游戏运⾏(GameRun)的总代码

//右侧打印帮助信息
void PrintHelpInfo()
{
    //帮助信息
    SetPos(64, 14);
    wprintf(L"%ls", L"不能穿墙,不能咬到自己");
    SetPos(64, 15);
    wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
    SetPos(64, 16);
    wprintf(L"%ls", L"按F3加速,F4减速");
    SetPos(64, 17);
    wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
    SetPos(64, 19);
    wprintf(L"%ls", L"制作by:小王C语言");
    SetPos(64, 20);
}

//暂停
void Pause()
{
    while (1)
    {
        Sleep(200);
        //再按空格继续游戏
        if (KEY_PRESS(VK_SPACE))
            break;
    }
}

//判断下一个节点是否是食物
int NextIsFood(pSnake ps, int x, int y)
{
    return (x == ps->_pFood->x && y == ps->_pFood->y);
}

//是食物,吃掉食物
void EatFood(pSnake ps)
{
    //食物节点变成头节点
    ps->_pFood->next = ps->_pSnake;
    ps->_pSnake = ps->_pFood;
    //打印蛇身
    pSnakeNode cur = ps->_pSnake;
    while (cur)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //总分++
    ps->_Socre += ps->_foodWight;
    //再次创建食物
    CreatFood(ps);
}

//不是食物,蛇移动一步
void NoFood(pSnake ps, int x, int y)
{
    //创建新节点
    pSnakeNode Node = ByNode(x, y);
    //下一个节点变成头节点
    Node->next = ps->_pSnake;
    ps->_pSnake = Node;
    //打印蛇身
    pSnakeNode cur = ps->_pSnake;
    while (cur->next->next)
    {
        SetPos(cur->x, cur->y);
        wprintf(L"%lc", BODY);
        cur = cur->next;
    }
    //不用打印,这个节点之前以及打印过了
    //SetPos(cur->x, cur->y);
    //wprintf(L"%lc", BODY);
    //最后一个节点打印两个空格
    SetPos(cur->next->x, cur->next->y);
    printf("  ");
    //释放最后一个节点
    free(cur->next);
    //将倒数第二个节点的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 (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
        {
            ps->_Status = KILL_BY_SELF;
        }
        cur = cur->next;
    }
}

//根据按键情况移动蛇
void SnakeMove(pSnake ps)
{
    int x = 0;
    int y = 0;
    //计算下一个节点的坐标
    if (ps->_Dir == UP)
    {
        x = ps->_pSnake->x;
        y = ps->_pSnake->y - 1;
    }
    else if (ps->_Dir == DOWN)
    {
        x = ps->_pSnake->x;
        y = ps->_pSnake->y + 1;
    }
    else if (ps->_Dir == LEFT)
    {
        x = ps->_pSnake->x - 2;
        y = ps->_pSnake->y;
    }
    else if (ps->_Dir == RIGHT)
    {
        x = ps->_pSnake->x + 2;
        y = ps->_pSnake->y;
    }
    //判断下一个节点是否是食物
    if (NextIsFood(ps, x, y))
    {
        //是食物,吃掉食物
        EatFood(ps);
    }
    //不是食物,蛇移动一步
    else
    {
        NoFood(ps, x, y);
    }
    //判断是否撞墙
    KillByWall(ps);
    //判断是否撞上自己
    KillBySelf(ps);
}

//游戏运行
void GameRun(pSnake ps)
{
    PrintHelpInfo();
    do
    {
        //打印已经获得的分数和每个食物的分数
        SetPos(64, 11);
        printf("总分:%d", ps->_Socre);
        SetPos(64, 12);
        printf("每个食物的分数:%2d", ps->_foodWight);
        //获取按键情况 
        //蛇不能180度转弯
        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->_Status = END_NORMAL;
        }
        //加速,一次休息时间-30,休息时间最少是80,加速一次食物分数+2
        else if (KEY_PRESS(VK_F3))
        {
            if (ps->_sleepTime > 80)
            {
                ps->_sleepTime -= 30;
                ps->_foodWight += 2;
            }
        }
        //减速,减速一次食物分数-2,食物分数最少是2
        else if (KEY_PRESS(VK_F4))
        {
            if (ps->_foodWight > 2)
            {
                ps->_sleepTime += 30;
                ps->_foodWight -= 2;
            }
        }
        SnakeMove(ps);
        Sleep(200);//移动一次休息一次     
    } while (ps->_Status == OK);
}

5.游戏结束(GameEnd)

这个部分是游戏结束后的善后工作,告知玩家游戏结束的原因,以及是否在玩一次。
如果害怕内存泄露的话也可以自己手动释放一下动态申请的空间。

5.1告知结束原因

//游戏结束,告知结束原因
void GameEnd(pSnake ps)
{
    SetPos(20, 12);
    switch (ps->_Status)
    {
    case END_NORMAL:
        printf("您主动退出,游戏结束!");
        break;
    case KILL_BY_WALL:
        printf("您撞到墙了,游戏结束!");
        break;
    case KILL_BY_SELF:
        printf("您撞到自己了,游戏结束!");
        break;
    }
}

5.2是否再来一盘

我们使用do…while循环,如果玩家输入Y或者y,while条件为真重新进入循环,游戏得以从头开始,输入n,游戏彻底结束。

int ch = 0;
do
{
	//初始化游戏
	Snake s = { 0 };

	GameStart(&s);

	GameRun(&s);

	GameEnd(&s);

	SetPos(20, 14);
	printf("再来一局吗?(Y/N):");
	ch = getchar();
	getchar();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 26);

http://www.kler.cn/a/578157.html

相关文章:

  • go mod文件 项目版本管理
  • 向量内积(点乘)和外积(叉乘)
  • 深入解析:使用 Python 爬虫获取淘宝店铺所有商品接口
  • 支持向量简要理解
  • FOC 控制笔记【二】无感控制、滑膜观测器和PLL
  • WebView2网页封装桌面软件
  • RBAC 权限系统管理模型 学习笔记
  • [排序算法]直接插入排序
  • MPPT与PWM充电原理及区别详解
  • 【每日八股】Golang篇(二):关键字(上)
  • 锂电池组的串数设计研究
  • 手写一个Tomcat
  • 【HarmonyOS Next】鸿蒙应用故障处理思路详解
  • 微服务面试题:服务网关和链路追踪
  • 运行OpenManus项目(使用Conda)
  • 高效运行 QwQ-32B + 错误修复
  • java项目springboot 项目启动不了解决方案
  • 服务器配置完成后如何启动或者终止java后端,相关运行文件如下:
  • 大白话react第十八章
  • openharmory-鸿蒙生态设备之间文件互传发现、接入认证和文件传输