
贪吃蛇和众多经典小游戏如三子棋、扫雷、俄罗斯方块一样,都是我们童年的回忆。今天就带着大家一起来做一做吧。
代码分成三个文件:snake.h负责函数的声明、头文件的包含、宏定义、创建结构体和枚举类型;snake.c负责实现游戏的各种功能;test.c负责游戏的测试。
如图所示,我们分成好几个模块去一步一步来构建游戏的思路,从而更好的实现它。

目录
一、创建维护蛇的信息(变量)
用链表的方式来模拟一条蛇,而每一个链表的节点就像是蛇的身体,身体与身体串联起来就成了一条蛇。一个节点有三个变量,x表示该位置的横坐标,y表示该位置的纵坐标,next表示指向下一个节点的指针

typedef struct SnakeNode//蛇身的节点
{
int x;//X坐标
int y;//Y坐标
struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;
蛇行动的方向分为:上、下、左、右,所以定义一个枚举类型来概括
enum DIRECTION//蛇的方向
{
UP, //上
DOWN,//下
LEFT,//左
RIGHT//右
};
也创建一个枚举类型来记录游戏的四种状态分别是:正常、撞到墙、撞到自己、主动退出游戏
enum STATUS//游戏的状态
{
OK, //正常
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到自己
END_NORMAL //主动退出
};
我们还得记录下蛇头和食物在哪个位置、游戏总成绩、单个食物的分数、蛇的速度。所以创建一个结构体在游戏中实时维护这条蛇
typedef struct Snake//蛇的信息
{
SnakeNode* head;//蛇头的地址
SnakeNode* food;//食物的地址
enum DIRECTION direction;//蛇的方向
enum STATUS status;//游戏的状态
int score;//总成绩
int food_weight;//一个食物的分数
int speed;//蛇的速度
}Snake;

总体代码
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <locale.h>
#include <stdlib.h>
#include <stdbool.h>
#include <Windows.h>
#include <stdio.h>
#include <time.h>
#include <assert.h>
typedef struct SnakeNode//蛇身的节点
{
int x;//X坐标
int y;//Y坐标
struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;
enum DIRECTION//蛇的方向
{
UP, //上
DOWN,//下
LEFT,//左
RIGHT//右
};
enum STATUS//游戏的状态
{
OK, //正常
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到自己
END_NORMAL //主动退出
};
typedef struct Snake//蛇的信息
{
SnakeNode* head;//蛇头的地址
SnakeNode* food;//食物的地址
enum DIRECTION direction;//蛇的方向
enum STATUS status;//游戏的状态
int score;//总成绩
int food_weight;//一个食物的分数
int speed;//蛇的速度
}Snake;
二、游戏开始 GameStart
创建一个蛇的信息的结构体变量snake,如果函数内只传变量snake的话,传值调用并不会影响实参,所以我们得传地址才能修改里面的数值,这里用了snake变量的地址。
#include "snake.h"
void test()
{
Snake snake = { 0 };
GameStart(&snake);//游戏开始
}
int main()
{
test();
return 0;
}
1.设置游戏窗口的大小

控制台窗口的大小是可以通过system函数来设置的,假设我们设置行是30行,列是100列吧。
#include "snake.h"
void GameStart(Snake* ps)//游戏开始
{
system("mode con cols=100 lines=30");//设置游戏窗口的大小
printf("hehe");
}

2.设置游戏窗口的名字

我们想更改控制台窗口默认的名字该怎么办呢?答案也是用system函数
void GameStart(Snake* ps)//游戏开始
{
system("mode con cols=100 lines=30");//设置游戏窗口的大小
system("title 贪吃蛇");//设置游戏窗口的名字
printf("hehe");
}
设置后变成了 : 贪吃蛇

3.隐藏屏幕光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得屏幕操作权限
CONSOLE_CURSOR_INFO CursorInfo;//创建控制台光标信息结构体
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态

4.打印欢迎界面 WelcomeToGame

我们想在屏幕中央打印这段欢迎文字该怎么办呢?
其实在控制台的底层逻辑存在着坐标系这个概念,不过它和我们数学的平面直角坐标系的Y轴是反过来的。X轴的数值向右依次增长,Y轴的数值向下依次增长。

我们需要封装一个函数把光标定位到中间的位置再打印
void SetPosition(short x, short y)//设置光标的位置
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出流(屏幕)的句柄
COORD position = { x, y };//创建定位光标结构体
SetConsoleCursorPosition(houtput, position);//设置控制台光标位置
}
WelcomeToGame()//打印欢迎界面 和 游戏提示
{
SetPosition(40, 14);
printf("欢迎来到贪吃蛇小游戏");
}

但打印完,又出现了请按任意键继续这段字,不好看,那我们把光标定位到下面,让它美观一点。
SetPosition(42, 20);

接下来是打印 游戏的操作提示

但在这之前上个画面得让它暂停,否则会一闪而过,这里用到system函数的pause(暂停)。而且还得让控制台清屏,否则会残留上一个画面的信息。也是用到system函数的cls(清屏)。
system("pause");
system("cls");
SetPosition(24, 12);
printf("用↑.↓.←.→来控制蛇的移动,按F3加速,按F4减速");
SetPosition(24, 13);
printf("加速能获得更高的分数");

同理,再把“请按任意键继续”移动到下面来,再暂停(直到按下任意键打破暂停),再清屏
SetPosition(42, 20);
system("pause");
system("cls");

总体代码
void SetPosition(short x, short y)//设置光标的位置
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出流(屏幕)的句柄
COORD position = { x, y };//创建定位光标结构体
SetConsoleCursorPosition(houtput, position);//设置控制台光标位置
}
void WelcomeToGame()//打印欢迎界面 和 游戏提示
{
SetPosition(40, 14);
printf("欢迎来到贪吃蛇小游戏");
SetPosition(42, 20);
system("pause");
system("cls");
SetPosition(24, 12);
printf("用↑.↓.←.→来控制蛇的移动,按F3加速,按F4减速");
SetPosition(24, 13);
printf("加速能获得更高的分数");
SetPosition(42, 20);
system("pause");
system("cls");
}
void GameStart(Snake* 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();//打印欢迎界面 和 游戏提示
}
5.创建地图 CreateMap
控制台的横坐标和纵坐标,可以理解为单元格,并不是严格1:1的正方形,而是上下高,左右窄的长方形,高和宽的比例差不多是2:1 。就像这样

如果是打印abcd这类英文字符只占一个格子,而打印中文字符和特殊符号则占两个格子

用 □ 这个正方形符号来表示墙体,先宏定义一下,这样方便我们以后换其他类型的墙体,改一次代码就行,不用在每一行用到墙体的代码都去修改,这样很麻烦。
#define WALL "□" //墙体
创建列为58,行为27的四堵墙,由于坐标是从0开始的,所以列是0~57,行是0~26 。一个正方形占两列一行,那一行打印58÷2=29个正方形就行了。先把上面一堵墙打印出来

void CreateMap()//创建地图
{
int i = 0;
for (i = 0; i < 29; i++)//上
{
printf(WALL);
}
}

再把光标定位到0列26行处,依次打印正方形

SetPosition(0, 26);
for (i = 0; i < 29; i++)//下
{
printf(WALL);
}

打印左边一列可就讲究了,由于我们已经打印了上下两行,所以只需要打印27-2=25个格子就行。下图可以看出,由上往下,纵坐标不变都是0,横坐标是1~25 。写一个循环,定义 i 变量从1到25,打印25次。每次打印都先设置光标的位置。

for (i = 1; i <= 25; i++)//左
{
SetPosition(0, i);
printf(WALL);
}

右边也是同理,只是这次纵坐标都是56不变,横坐标从1~25 。

for (i = 1; i <= 25; i++)//右
{
SetPosition(56, i);
printf(WALL);
}

总体代码
void CreateMap()//创建地图
{
int i = 0;
for (i = 0; i < 29; i++)//上
{
printf(WALL);
}
SetPosition(0, 26);
for (i = 0; i < 29; i++)//下
{
printf(WALL);
}
for (i = 1; i <= 25; i++)//左
{
SetPosition(0, i);
printf(WALL);
}
for (i = 1; i <= 25; i++)//右
{
SetPosition(56, i);
printf(WALL);
}
}
6.初始化蛇 InitSnake
在墙体内创建一条蛇,那他的坐标必需在墙体内,而且横坐标必需是2的倍数,这样方便和墙体对齐。我们宏定义蛇的初始坐标,这样方便我们后期修改蛇的位置。定义变量 i 从0开始,每次进入循环让X加上i 就能自动创建五个坐标了。
#define POS_X 24 //初始化蛇的X坐标
#define POS_Y 5 //初始化蛇的Y坐标

用变量i循环申请五个蛇身节点
void InitSnake(Snake* ps)//初始化蛇
{
SnakeNode* cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(cur);
cur->x = POS_X + i * 2;
cur->y = POS_Y;
cur->next = NULL;
}
}
怎么让他们连起来呢?第一次申请的节点肯定是作为蛇头了,如果蛇头为空指针,那我就把新申请的节点赋给蛇头。那第二次及以后申请的节点就作为蛇身了,让新申请的节点的next指针指向蛇头,再让蛇头指向新节点,以头插法的方式连接节点。



//头插法
if (ps->head == NULL)
{
ps->head = cur;
}
else
{
cur->next = ps->head;
ps->head = cur;
}
宏定义蛇的身体,打印蛇
#define BODY "●" //蛇身
cur = ps->head;
while (cur)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
设置蛇的方向、游戏状态、食物分数、总成绩、蛇的速度
ps->direction = RIGHT;//蛇的方向默认向右
ps->status = OK;//蛇的状态为OK正常
ps->food_weight = 10;//一个食物分数
ps->score = 0;//总成绩
ps->speed = 200;//蛇的速度

总体代码
void InitSnake(Snake* ps)//初始化蛇
{
SnakeNode* cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(cur);
cur->x = POS_X + i * 2;
cur->y = POS_Y;
cur->next = NULL;
//头插法
if (ps->head == NULL)
{
ps->head = cur;
}
else
{
cur->next = ps->head;
ps->head = cur;
}
}
cur = ps->head;
while (cur)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
ps->direction = RIGHT;//蛇的方向默认向右
ps->status = OK;//蛇的状态为OK正常
ps->food_weight = 10;//一个食物分数
ps->score = 0;//总成绩
ps->speed = 200;//停顿时间
}
7.创建食物 CreateFood

创建食物必需是在墙体内,那怎么产生2~54 和 1~25的数值呢?

srand((unsigned int)time(NULL));
int x = 0;//食物的横坐标
int y = 0;//食物的纵坐标
x = rand() % 53 + 2;
y = rand() % 25 + 1;
有了范围内的数值还不行,还得保证横坐标是2的倍数,这样才能和蛇对齐,让蛇能吃到它。
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);//x为奇数继续进循环,直到为偶数停下来
并且食物不能和蛇身重复,重复了就重新上去生成一遍
int x = 0;//食物的横坐标
int y = 0;//食物的纵坐标
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);//x为奇数继续进循环,直到为偶数停下来
SnakeNode* cur = ps->head;
while (cur)//判断食物是否与蛇身重复
{
if ((cur->x == x) && (cur->y == y))
{
goto again;
}
cur = cur->next;
}
先定义食物的图标
#define FOOD "★" //食物
有了正确的横坐标和纵坐标之后,就可以创建食物的节点 、打印食物了。再把食物信息关联到贪吃蛇结构体的food指针中。
SnakeNode* food = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(food);
food->x = x;
food->y = y;
food->next = NULL;
ps->food = food;
SetPosition(x, y);
printf(FOOD);

总体代码
void CreateFood(Snake* ps)//创建食物
{
int x = 0;//食物的横坐标
int y = 0;//食物的纵坐标
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);//x为奇数继续进循环,直到为偶数停下来
SnakeNode* cur = ps->head;
while (cur)//判断食物是否与蛇身重复
{
if ((cur->x == x) && (cur->y == y))
{
goto again;
}
cur = cur->next;
}
SnakeNode* food = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(food);
food->x = x;
food->y = y;
food->next = NULL;
ps->food = food;
SetPosition(x, y);
printf(FOOD);
}
三、游戏运行 GameRun
1.右侧打印帮助信息 PrintHeloInfo

总体代码
void PrintHelpInfo()//打印提示信息
{
SetPosition(64, 14);
printf("不能穿墙,不能咬到自己");
SetPosition(64, 15);
printf("用↑.↓.←.→来控制蛇的移动");
SetPosition(64, 16);
printf("按F3加速,按F4减速");
SetPosition(64, 17);
printf("按ESC退出游戏,按空格暂停游戏");
SetPosition(64, 20);
printf("小比特-峰哥 制作");
}
2.打印总成绩和当前食物的分数

和上面的打印提示信息不同,总分数和当前食物分数都会随着蛇吃食物、加速加分、减速减分而实时变化的。所以我们用一个循环来运行游戏,把这两个数值放入循环内,当游戏状态是OK的时候继续进循环,当游戏状态不是OK了就退出循环,结束游戏,。前一步说的打印提示信息是至始至终贯穿全局的,不需要变动。
void GameRun(Snake* ps)//游戏运行
{
PrintHelpInfo();//打印提示信息
do
{
SetPosition(64, 10);
printf("总分数:%d", ps->score);
SetPosition(64, 11);
printf("当前食物的分数:%2d", ps->food_weight);
SetPosition(0, 27);
} while (ps->status == OK);
}
3.获取按键情况 KEY_PRESS
GetAsyncKeyState函数可以判断键盘的按键是否处于按下的状态,当按下时函数会返回一个最低位是1的二进制数,再把这个二进制数按位与1就得到了1;当没按时函数会返回一个最低位是0的二进制数,再把这个二进制数按位与1就得到了0 。 按下了就返回1,没按过就返回0 。

GetAsyncKeyState函数的参数就是键盘上按键的虚拟键值,例如按键F3对应的虚拟键值就是(VK_F3)。具体请参考:键盘按键和对应的虚拟键值表。以下是一些键盘按键和虚拟键值的举例:

来看看演示代码
#define KEY_PRESS(VK) (( GetAsyncKeyState(VK) & 1) ? 1 : 0)
int main()
{
while (1)
{
if (KEY_PRESS(0x30))
{
printf("0\n");
}
else if (KEY_PRESS(0x31))
{
printf("1\n");
}
else if (KEY_PRESS(0x32))
{
printf("2\n");
}
else if (KEY_PRESS(0x33))
{
printf("3\n");
}
else if (KEY_PRESS(0x34))
{
printf("4\n");
}
else if (KEY_PRESS(0x35))
{
printf("5\n");
}
else if (KEY_PRESS(0x36))
{
printf("6\n");
}
else if (KEY_PRESS(0x37))
{
printf("7\n");
}
else if (KEY_PRESS(0x38))
{
printf("8\n");
}
else if (KEY_PRESS(0x39))
{
printf("9\n");
}
}
return 0;
}
当我按下数字键的时候,屏幕就会输出相应的数字,这就很好证明了这个函数的功能。

宏定义去实现这个功能
#define KEY_PRESS(VK) (( GetAsyncKeyState(VK) & 1) ? 1 : 0)
会涉及几个按键:
一、方向键↑ 往上
二、方向键↓ 往下
三、方向键← 往左
四、方向键→ 往右
五、空格 暂停
六、ESC 退出游戏
七、F3 加速
八、F4 减速
当按下↑就把蛇的方向改为上,当然此时蛇的方向是不能向下的。因为蛇都往下走了,再按上不就让它倒退了嘛。
if ((KEY_PRESS(VK_UP)) && (ps->direction != DOWN))
{
ps->direction = UP;//上
}
当按下↓就把蛇的方向改为下,当然此时蛇的方向是不能向上的。
else if ((KEY_PRESS(VK_DOWN)) && (ps->direction != UP))
{
ps->direction = DOWN;//下
}
当按下←就把蛇的方向改为左,当然此时蛇的方向是不能向右的。
else if ((KEY_PRESS(VK_LEFT)) && (ps->direction != RIGHT))
{
ps->direction = LEFT;//左
}
当按下→就把蛇的方向改为右,当然此时蛇的方向是不能向左的。
else if ((KEY_PRESS(VK_RIGHT)) && (ps->direction != LEFT))
{
ps->direction = RIGHT;//右
}
当按下空格就暂停游戏,封装一个函数Pause让它死循环的睡眠,直到再次按下空格打破循环,游戏继续。
void Pause()//暂停
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();//暂停
}
当按下ESC就退出游戏,游戏状态变成END_NORMAL不等于OK跳出do.....while循环。游戏不再继续。
else if (KEY_PRESS(VK_ESCAPE))
{
ps->status = END_NORMAL;//主动退出
}
当按下F3就让游戏加速,那怎么加速呢?把暂停时间缩短就行,因为游戏运行的逻辑是蛇每走一步都会暂停一下,再走下一步的。而我们前面就设计了贪吃蛇结构体里面的变量speed默认暂停时间是200毫秒。
当然,也不能无限制的让它一直减少,减到后面就成负数了。Sleep()函数是不能给它传负数的。所以我们设置五个档位,每次加速减30就行了。减到80为止。每次加速,食物分数往上涨2分

else if (KEY_PRESS(VK_F3))
{
if (ps->speed > 80)
{
ps->speed -= 30;//加速
ps->food_weight += 2;
}
}
同理,当按下F4就让游戏减速,设置五个档位,每次减速加30就行了。加到320为止。每次减速,食物分数往下降2分

else if (KEY_PRESS(VK_F4))
{
if (ps->speed < 320)
{
ps->speed += 30;//减速
ps->food_weight -= 2;
}
}
4.根据按键情况移动蛇 SnakeMove
①根据蛇头的坐标和方向,计算下一个节点的坐标
前面已经记录下了按键的情况,现在根据按键情况预测蛇的下一步走到哪里,比如蛇往上走,假设蛇头的坐标为(x,y),他的下一步就在蛇头的上方,因此下一步的横坐标不变,纵坐标 - 1。

void SnakeMove(Snake* ps)//蛇走一步的过程
{
SnakeNode* next = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(next);
if (ps->direction== UP)//上
{
next->x = ps->head->x;
next->y = ps->head->y - 1;
}
}
蛇往下走,假设蛇头的坐标为(x,y),他的下一步就在蛇头的下方,因此下一步的横坐标不变,纵坐标 + 1。

else if (ps->direction== DOWN)//下
{
next->x = ps->head->x;
next->y = ps->head->y + 1;
}
蛇往左走,假设蛇头的坐标为(x,y),他的下一步就在蛇头的左边,因此下一步的横坐标 - 2,纵坐标不变。

else if (ps->direction == LEFT)//左
{
next->x = ps->head->x - 2;
next->y = ps->head->y;
}
蛇往右走,假设蛇头的坐标为(x,y),他的下一步就在蛇头的右边,因此下一步的横坐标 + 2,纵坐标不变。

else if (ps->direction == RIGHT)//右
{
next->x = ps->head->x + 2;
next->y = ps->head->y;
}
②判断下一个节点是否为食物

下一步的横坐标等于食物的横坐标,下一步的纵坐标等于食物的纵坐标,此时就说明下一步就是食物了,它俩刚好重叠在一起了。
if ((next->x == ps->food->x) && (next->y == ps->food->y))//判断下一个位置是不是食物
{
}
③是食物,吃掉食物 EatFood
把食物的next指针 指向 蛇头,再让食物的地址成为新的蛇头,依然采用的是头插法。



void EatFood(Snake* ps)//吃掉食物
{
ps->food->next = ps->head;
ps->head = ps->food;
}
吃完之后,打印蛇
SnakeNode* cur = ps->head;
while (cur)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
吃了食物,我们的总成绩也得加分
ps->score += ps->food_weight;//加分
吃了食物就没有食物啦,所以得创建新的食物
CreateFood(ps);//重新创建食物
别忘了释放我们刚刚申请的下一个节点的空间,虽然下一个节点和食物的横坐标、纵坐标一样,但是它们并不指向同一块内存空间。
free(next);//释放下一个节点
next = NULL;//置为空
④不是食物 NoFood

同理,把下一个节点的next指针 指向 蛇头,再让下一个节点的地址成为新的蛇头,

if ((next->x == ps->food->x) && (next->y == ps->food->y))//判断下一个位置是不是食物
{
EatFood(ps);//吃掉食物
free(next);//释放下一个节点
next = NULL;//置为空
}
else
{
NoFood(next, ps);//不是食物
}
void NoFood(SnakeNode* next,Snake* ps)//不是食物
{
next->next = ps->head;
ps->head = next;
}

不吃食物的话,蛇的身体前面多一个,相应的后面应该要少一个。所以打印到倒数第二个就停止
,假设蛇身有五个节点,定义一个指针cur,让它先打印前五个节点,一直到它的下一个节点的next指针为空就停下来。

SnakeNode* cur = ps->head;
while (cur->next->next != NULL)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
然后别忘了,把倒数第一个节点释放掉,再把倒数第二个节点的next指针置为空。
free(cur->next);
cur->next = NULL;
Sleep(ps->speed);//停顿
测试一下

咦?为什么蛇没吃食物,身体也会变长呢?原来我们每一次的打印都是基于上一次打印留下来的图片的基础上的,这一次的蛇尾没有被清理掉,那下一次打印尾巴还是在那。所以我们得把尾巴清理掉,定位到尾巴的位置,打印空格就会把尾巴覆盖掉啦。

SetPosition(cur->next->x, cur->next->y);
printf(" ");
总体代码
void NoFood(SnakeNode* next,Snake* ps)//不是食物
{
next->next = ps->head;
ps->head = next;
SnakeNode* cur = ps->head;
while (cur->next->next != NULL)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
SetPosition(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
⑤判断是否撞到墙 KillByWall

我们还得去判断蛇有没有撞到墙,否则就像上面这种情况一样了。
其实判断有没有撞墙很简单,蛇头的横坐标等于0 或者 等于56,纵坐标等于0 或者 等于26就是撞上了。

撞上了以后,把游戏的状态设置为KILL_BY_WALL,此时游戏状态不等于OK,跳出循环,结束游戏
void KillByWall(Snake* ps)//判断是否撞到墙
{
if ((ps->head->x == 0) || (ps->head->x == 56) ||
(ps->head->y == 0) || (ps->head->y == 26))
{
ps->status = KILL_BY_WALL;
}
}

⑥判断是否撞到自己 KillBySelf
看看蛇身的节点坐标有没有和蛇头重复的,重复了就证明撞上了
void KillBySelf(Snake* ps)//判断是否撞到自己
{
SnakeNode* cur = ps->head->next;
while (cur)
{
if ((cur->x == ps->head->x) && (cur->y == ps->head->y))
{
ps->status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}

四、游戏结束 GameEnd
1.说明游戏结束的原因
蛇撞到墙,蛇撞到自己,按ESC键退出都是导致游戏结束的原因。前面我们定义了一个枚举常量,可以通过枚举常量来得知游戏结束的原因是什么。
void GameEnd(Snake* ps)//游戏结束
{
SetPosition(21, 12);
switch (ps->status)
{
case END_NORMAL:
printf("你主动退出游戏");
break;
case KILL_BY_SELF:
printf("你撞到了自己,游戏结束");
break;
case KILL_BY_WALL:
printf("你撞到了墙上,游戏结束");
break;
}
}



2.释放蛇身节点
蛇身节点是我们向内存申请的空间,游戏结束后应该释放掉,还给操作系统,如果不还,会一直占用系统资源,造成资源浪费。所以定义两个指针cur、del都指向蛇头,从蛇头开始释放,只要cur不为空,那就让cur往后走,del来释放资源。直到cur指向NULL就释放完蛇身所有节点了。
SnakeNode* cur = ps->head;
while (cur)
{
SnakeNode* del = cur;
cur = cur->next;
free(del);
del = NULL;
}




到此,游戏就能够运行起来啦。

五、优化游戏
当你撞墙或者撞到自己了,又想再来一局怎么办?有没有啥办法能快速开启下一局的游戏呢?
这样,写一个do....while循环,每次游戏结束,假如我们输入大写Y或者小写y都可以重开一局游戏,这样就能想玩几把就玩几把了。
void test()
{
char ch = 0;
do
{
system("cls");
Snake snake = { 0 };
GameStart(&snake);//游戏开始
GameRun(&snake);//游戏运行
GameEnd(&snake);//游戏结束
SetPosition(19, 15);
printf("再玩一局吗?请输入Y或N");
SetPosition(22, 16);
ch = getchar();
while (getchar() != '\n');
SetPosition(0, 27);
} while ((ch == 'Y') || (ch == 'y'));
}
这里的while (getchar() != '\n');意思是假如有人输入了Y又不小心输入了一大串乱七八糟的东西,但是我循环条件依然可以读到第一个Y字符。只要没有读到回车,就会一直往后读,相当于把字母Y后面多余的信息删除了。这样到while ((ch == 'Y') || (ch == 'y'));这行代码ch变量得到的还是字母Y


六、参考代码
1.snake.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <locale.h>
#include <stdlib.h>
#include <stdbool.h>
#include <Windows.h>
#include <stdio.h>
#include <time.h>
#include <assert.h>
#define POS_X 24 //初始化蛇的X坐标
#define POS_Y 5 //初始化蛇的Y坐标
#define WALL "□" //墙体
#define BODY "●" //蛇身
#define FOOD "★" //食物
#define KEY_PRESS(VK) (( GetAsyncKeyState(VK) & 1) ? 1 : 0)
typedef struct SnakeNode//蛇身的节点
{
int x;//X坐标
int y;//Y坐标
struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;
enum DIRECTION//蛇的方向
{
UP, //上
DOWN,//下
LEFT,//左
RIGHT//右
};
enum STATUS//游戏的状态
{
OK, //正常
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到自己
END_NORMAL //主动退出
};
typedef struct Snake//蛇的信息
{
SnakeNode* head;//蛇头的地址
SnakeNode* food;//食物的地址
enum DIRECTION direction;//蛇的方向
enum STATUS status;//游戏的状态
int score;//总成绩
int food_weight;//一个食物的分数
int speed;//蛇的速度
}Snake;
void GameStart(Snake* ps);//游戏开始
void GameRun(Snake* ps);//游戏运行
void GameEnd(Snake* ps);//游戏结束
void SetPosition(short x, short y);//设置光标的位置
2.test.c
#include "snake.h"
void test()
{
char ch = 0;
do
{
system("cls");
Snake snake = { 0 };
GameStart(&snake);//游戏开始
GameRun(&snake);//游戏运行
GameEnd(&snake);//游戏结束
SetPosition(19, 15);
printf("再玩一局吗?请输入Y或N");
SetPosition(22, 16);
ch = getchar();
while (getchar() != '\n');
SetPosition(0, 27);
} while ((ch == 'Y') || (ch == 'y'));
}
int main()
{
srand((unsigned int)time(NULL));
test();
return 0;
}
3.snake.c
#include "snake.h"
void SetPosition(short x, short y)//设置光标的位置
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出流(屏幕)的句柄
COORD position = { x, y };//创建定位光标结构体
SetConsoleCursorPosition(houtput, position);//设置控制台光标位置
}
void WelcomeToGame()//打印欢迎界面 和 游戏提示
{
SetPosition(40, 14);
printf("欢迎来到贪吃蛇小游戏");
SetPosition(42, 20);
system("pause");
system("cls");
SetPosition(24, 12);
printf("用↑.↓.←.→来控制蛇的移动,按F3加速,按F4减速");
SetPosition(24, 13);
printf("加速能获得更高的分数");
SetPosition(42, 20);
system("pause");
system("cls");
}
void CreateMap()//创建地图
{
int i = 0;
for (i = 0; i < 29; i++)//上
{
printf(WALL);
}
SetPosition(0, 26);
for (i = 0; i < 29; i++)//下
{
printf(WALL);
}
for (i = 1; i <= 25; i++)//左
{
SetPosition(0, i);
printf(WALL);
}
for (i = 1; i <= 25; i++)//右
{
SetPosition(56, i);
printf(WALL);
}
}
void InitSnake(Snake* ps)//初始化蛇
{
SnakeNode* cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(cur);
cur->x = POS_X + i * 2;
cur->y = POS_Y;
cur->next = NULL;
//头插法
if (ps->head == NULL)
{
ps->head = cur;
}
else
{
cur->next = ps->head;
ps->head = cur;
}
}
cur = ps->head;
while (cur)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
ps->direction = RIGHT;//蛇的方向默认向右
ps->status = OK;//蛇的状态为OK正常
ps->food_weight = 10;//一个食物分数
ps->score = 0;//总成绩
ps->speed = 200;//停顿时间
}
void CreateFood(Snake* ps)//创建食物
{
int x = 0;//食物的横坐标
int y = 0;//食物的纵坐标
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);//x为奇数继续进循环,直到为偶数停下来
SnakeNode* cur = ps->head;
while (cur)//判断食物是否与蛇身重复
{
if ((cur->x == x) && (cur->y == y))
{
goto again;
}
cur = cur->next;
}
SnakeNode* food = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(food);
food->x = x;
food->y = y;
food->next = NULL;
ps->food = food;
SetPosition(x, y);
printf(FOOD);
}
void GameStart(Snake* 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()//打印提示信息
{
SetPosition(64, 14);
printf("不能穿墙,不能咬到自己");
SetPosition(64, 15);
printf("用↑.↓.←.→来控制蛇的移动");
SetPosition(64, 16);
printf("按F3加速,按F4减速");
SetPosition(64, 17);
printf("按ESC退出游戏,按空格暂停游戏");
SetPosition(64, 20);
printf("小比特-峰哥 制作");
}
void Pause()//暂停
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
void EatFood(Snake* ps)//吃掉食物
{
ps->food->next = ps->head;
ps->head = ps->food;
SnakeNode* cur = ps->head;
while (cur)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
ps->score += ps->food_weight;//加分
CreateFood(ps);//重新创建食物
}
void NoFood(SnakeNode* next,Snake* ps)//不是食物
{
next->next = ps->head;
ps->head = next;
SnakeNode* cur = ps->head;
while (cur->next->next != NULL)
{
SetPosition(cur->x, cur->y);
printf(BODY);
cur = cur->next;
}
SetPosition(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
void SnakeMove(Snake* ps)//蛇走一步的过程
{
SnakeNode* next = (SnakeNode*)malloc(sizeof(SnakeNode));
assert(next);
next->next = NULL;
if (ps->direction == UP)//上
{
next->x = ps->head->x;
next->y = ps->head->y - 1;
}
else if (ps->direction == DOWN)//下
{
next->x = ps->head->x;
next->y = ps->head->y + 1;
}
else if (ps->direction == LEFT)//左
{
next->x = ps->head->x - 2;
next->y = ps->head->y;
}
else if (ps->direction == RIGHT)//右
{
next->x = ps->head->x + 2;
next->y = ps->head->y;
}
if ((next->x == ps->food->x) && (next->y == ps->food->y))//判断下一个位置是不是食物
{
EatFood(ps);//吃掉食物
free(next);//释放下一个节点
next = NULL;//置为空
}
else
{
NoFood(next, ps);//不是食物
}
}
void KillByWall(Snake* ps)//判断是否撞到墙
{
if ((ps->head->x == 0) || (ps->head->x == 56) ||
(ps->head->y == 0) || (ps->head->y == 26))
{
ps->status = KILL_BY_WALL;
}
}
void KillBySelf(Snake* ps)//判断是否撞到自己
{
SnakeNode* cur = ps->head->next;
while (cur)
{
if ((cur->x == ps->head->x) && (cur->y == ps->head->y))
{
ps->status = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
void GameRun(Snake* ps)//游戏运行
{
PrintHelpInfo();//打印提示信息
do
{
SetPosition(64, 10);
printf("总分数:%d", ps->score);
SetPosition(64, 11);
printf("当前食物的分数:%2d", ps->food_weight);
SetPosition(0, 27);
if ((KEY_PRESS(VK_UP)) && (ps->direction != DOWN))
{
ps->direction = UP;//上
}
else if ((KEY_PRESS(VK_DOWN)) && (ps->direction != UP))
{
ps->direction = DOWN;//下
}
else if ((KEY_PRESS(VK_LEFT)) && (ps->direction != RIGHT))
{
ps->direction = LEFT;//左
}
else if ((KEY_PRESS(VK_RIGHT)) && (ps->direction != LEFT))
{
ps->direction = RIGHT;//右
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();//暂停
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->status = END_NORMAL;//主动退出
}
else if (KEY_PRESS(VK_F3))
{
if (ps->speed > 80)
{
ps->speed -= 30;//加速
ps->food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->speed < 320)
{
ps->speed += 30;//减速
ps->food_weight -= 2;
}
}
SnakeMove(ps);//蛇走一步的过程
Sleep(ps->speed);//停顿
KillByWall(ps);//判断是否撞到墙
KillBySelf(ps);//判断是否撞到自己
} while (ps->status == OK);
}
void GameEnd(Snake* ps)//游戏结束
{
SetPosition(21, 12);
switch (ps->status)
{
case END_NORMAL:
printf("你主动退出游戏");
break;
case KILL_BY_SELF:
printf("你撞到了自己,游戏结束");
break;
case KILL_BY_WALL:
printf("你撞到了墙上,游戏结束");
break;
}
//SetPosition(0, 27);
SnakeNode* cur = ps->head;
while (cur)
{
SnakeNode* del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
6982





