注 :本文是基于链表实现贪吃蛇游戏
1.Win32 API
本篇文章中实现贪吃蛇会用到一些Win32 API的知识,接下来简单做下介绍
1.1 Win32 API
1.2 控制台程序
平常我们运行起来的黑框程序其实就是控制台程序
mode指令
#include<stdio.h>
#include<Windows.h>
int main()
{
system("mode con cols=30 lines=30");
system("pause");
return 0;
}
#include<stdio.h>
#include<Windows.h>
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
system("pause");//这里给一个暂停,因为程序结束了就看不出来效果了
return 0;
}
1.3 控制台屏幕上的坐标COORD
在数学中有坐标的概念,有X轴,y轴等
在控制台窗口中,同样存在坐标的概念
typedef struct _COORD
{
SHORT X;
SHORT Y;
} COORD, *PCOORD;
X,Y分别对应横纵坐标
1.4 GetStdHandle
HANDLE GetStdHandle(DWORD nStdHandle);
int main()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
return 0;
}
拿到这个句柄,就可以对我们的控制台面板进行一些操作
1.5 GetConsoleCursorInfo
那么在控制台输出时,光标会不停闪烁,希望将光标去掉
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
1.5.1 CONSOLE_CURSOR_INFO
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
int main()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houput, &cursor_info);
printf("%d\n", cursor_info.dwSize);
return 0;
}
1.6 SetConsoleCursorInfo
int main()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houput, &cursor_info);
printf("%d\n", cursor_info.dwSize);
cursor_info.dwSize = 50;
SetConsoleCursorInfo(houput, &cursor_info);
system("pause");
return 0;
}
int main()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houput相关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houput, &cursor_info);
printf("%d\n", cursor_info.dwSize);
//修改光标的占比值
cursor_info.dwSize = 50;
cursor_info.bVisible = false;
//设置和houput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houput, &cursor_info);
system("pause");
return 0;
}
这样就看不见光标了
1.7 SetConsoleCursorPosition
int main()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { 10,20 };
SetConsoleCursorPosition(houput, pos);
system("title 贪吃蛇");
system("pause");
return 0;
}
光标的位置发生了改变
那么我们将上述操作封装成一个函数SetPos
void Setpos(short X , short Y)
{
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { X,Y };
SetConsoleCursorPosition(houput, pos);
}
int main()
{
//获得标准输出设备的句柄
Setpos(5, 10);
printf("1");
Setpos(10, 20);
printf("1");
Setpos(15, 25);
printf("1");
system("title 贪吃蛇");
getchar();
return 0;
}
1.8 getAsyncKeyState
SHORT GetAsyncKeyState(
int vKey
);
虚拟键代码
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1.
那我们让GetAsyncKeyState的返回值与1进行&运算就可以判断最低位是不是1了
将这个过程写成一个宏
#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;
}
测试是可行的
1.9 <locale.h>本地化
特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
1.9.1 类项
1.10 setlocale
char* setlocale (int category, const char* locale);
setlocale(LC_ALL, "C");
int main()
{
char* p = NULL;
p = setlocale(LC_ALL, "");
printf("%s\n", p);
p = setlocale(LC_ALL, "C");
printf("%s\n", p);
return 0;
}
1.10.1 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
int main()
{
setlocale(LC_ALL, "");
wchar_t ch1 = L'国';
char ch2 = 'a';
char ch3 = 'b';
printf("%c%c\n", ch2, ch3);
wprintf(L"%lc\n", ch1);
return 0;
}
从输出的结果来看,⼀个普通字符占⼀个字符的位置
7. 贪吃蛇的游戏设计
在正式实现贪吃蛇游戏之前,先来看一下最终想要的效果是什么
首先有一个欢迎界面
接下来是操作介绍
再就是游戏界面
那么可以将整个游戏实现的过程分为以下步骤
欢迎界面
说明界面
游戏界面
其中游戏界面又分为
初始化游戏
游戏进行
结束游戏
接下来进行实现
7.1 欢迎界面与说明界面
这里没啥好说的,改变光标位置,让输出的信息出现在我们想要的位置即可
void WlecSc()
{
SetPos(50,16);
printf("欢迎来到贪吃蛇游戏\n");
SetPos(50, 18);
system("pause");
SetPos(30, 16);
printf("使用↑ ↓ ← → 控制蛇的方向,F3为加速,F4为减速,加速将获得更高的分数\n");
SetPos(35, 17);
printf("撞到墙体或者吃到自己的身体都会导致死亡,游戏结束\n");
SetPos(50, 18);
system("pause");
}
7.2 地图的绘制
在本文中,实现地图墙体的绘制采用的是宽字符□。
那么这里只需要改变光标所在的位置,然后再用循环打印指定数量的□即可
#include<stdio.h>
#include<Windows.h>
int main()
{
system("mode con cols=30 lines=30");
system("pause");
return 0;
}
前文中该代码打印出来的效果是这种
int main()
{
system("mode con cols=60 lines=30");
system("pause");
return 0;
}
这样就是正方形了
所以可以得出结论控制台的坐标关系是2X = Y
这里假如说我们要绘制的棋盘是30*30的一个正方形棋盘
先从上方开始绘制
此时就不需要移动光标,直接绘制即可
int main()
{
setlocale(LC_ALL, "");
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc",L'□');
}
getchar();
return 0;
}
那么再打印下面和左右的墙体,此时就需要移动光标
int main()
{
setlocale(LC_ALL, "");
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc",L'□');
}
SetPos(0, 29);//将光标移动到(0,29)这个位置
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc", L'□');
}//下方
for (int i = 1; i < 29; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}//左方
for (int i = 1; i < 29; i++)
{
SetPos(58, i);//2*X = Y
wprintf(L"%lc", L'□');
}//右方
getchar();
return 0;
}
到这里地图墙体就绘制完成了,将他封装为一个函数
void draw()
{
setlocale(LC_ALL, "");
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc", L'□');
}
SetPos(0, 29);//将光标移动到(0,29)这个位置
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc", L'□');
}//下方
for (int i = 1; i < 29; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}//左方
for (int i = 1; i < 29; i++)
{
SetPos(58, i);//2*X = Y
wprintf(L"%lc", L'□');
}//右方
getchar();
}
7.3 蛇与食物
蛇的身体的绘制采用宽字符●
食物绘制采用宽字符▲
在游戏中,蛇可以被分为两种状态
7.3.1 蛇
将蛇的身体用数个●连接起来就组成了一条蛇
蛇自身有很多属性
包括
蛇身
蛇的方向
蛇的状态
食物的分数
总分数
蛇的速度
那么首先就要创建蛇身的结构体类型
这个节点肯定有对应的坐标,以及指向下一个节点的指针
struct SnakeNode
{
//坐标
short X;
short Y;
struct SnakeNode* next;
};
为方便书写,进行重命名
typedef struct SnakeNode SNO;
蛇的方向无非就是上下左右
这里直接给一个枚举类型
enum Direction
{
UP = 1,
DOWN,
LEFT,
RIGHT,
};
蛇的状态无非就是四种
正常
撞墙死
吃自己死
正常退出
也给个枚举类型
enum Statement
{
NORMAL=1,//正常行动
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//吃到自己
END_NORMAL//正常退出
};
单个食物的分数和蛇的总分数直接给整形就好了
int food_score;
int Score;
在玩贪吃蛇游戏的时候,贪吃蛇的移动是一闪一闪的
其实就是在移动的时候调用了sleep函数,休息了一段时间
这个时间越短速度就越快
那还是给这个时间一个整形
这样就能描述蛇的速度
将上述属性放到一个蛇结构体里,就创建了蛇
struct Snake
{
SNO* phead;//指向蛇头的指针
Direction direct;//方向
Statement state;//状态
int food_score;//单个食物分数
int Score;//总分数
int sleep_time;//休息时间
};
因为蛇会吃掉食物,那就可以认为,食物与蛇身属于同一类型
struct Snake
{
SNO* phead;//指向蛇头的指针
SNO* food;//指向食物的指针
Direction direct;//方向
Statement state;//状态
int food_score;//单个食物分数
int Score;//总分数
int sleep_time;//休息时间
};
8 核心逻辑
8.1 游戏主逻辑
程序开始就设置支持本地化,然后进入游戏主逻辑
主逻辑分为三个过程
游戏开始
游戏运行
游戏结束
8.2 游戏开始
定义一个函数为GameStart
这里进行游戏开始需要做的事
游戏开始后,需要进行以下步骤
1.控制台窗口大小的设置
2.控制台窗口名字的设置
3.鼠标光标的隐藏
4.欢迎界面
5.说明界面
6.初始化蛇
7.初始化第一个食物
其中1.2.3.4.5在前文中已经实现了,就不再赘述
这里主要说6.7.
蛇的身体就是由几个蛇身节点组成
那么这里设定一开始蛇身的长度是5个宽字符●组成
这意味着一开始需要动态申请5个蛇身类型的节点
将申请身体节点的过程封装成一个函数
//申请身体节点
SNO* BuyBodyNode(short a , short b)
{
SNO* newnode = (SNO*)malloc(sizeof(SNO));
if (newnode == NULL)
{
printf("游戏出现错误,请退出后重试\n");
exit(1);
}
newnode->X = a;
newnode->Y = b;
newnode->next = NULL;
return newnode;
}
申请出来的五个节点,需要将他们连接起来,这里就可以用到数据结构链表来进行处理
这里想让蛇一开始是成水平排列,就说明这五个节点中的X坐标是需要移动的,Y坐标是相
等的设定从(20,10)这个位置组成蛇,因为●是宽字符占了两个字节,所以X坐标必须是2的倍
数,而一个Y坐标是可以容纳一个宽字符大小的,就没有特别要求
这里采用宏定义,以便后续想要进行更改的时候只用更改宏定义一处就好了
#define INITPOSX 20
#define INITPOSY 10
给一个循环,申请五个节点,再将他们串起来
这里采取的连接方法是尾插法
但是尾插完成后是让最后一个插入的节点成为头
snake->phead = NULL;
for (int i = 0; i < 5; i++)
{
SNO* BodyNode = BuyBodyNode(INITPOSX + (i * 2), INITPOSY);
if (snake->phead == NULL)
{
//如果还没有身体就让蛇头指向这个节点
snake->phead = BodyNode;
}
else
{
BodyNode->next = snake->phead;
snake->phead = BodyNode;
}
}
打印出来看下行不行
为了方便修改,我们将墙体,蛇身,食物图形都采用宏定义
#define INITPOSX 20
#define INITPOSY 10
#define BODY L'●'
#define FOOD L'▲'
#define WALL L'□'
SNO* cur = snake->phead;
//蛇身的打印
while (cur)
{
SetPos(cur->X, cur->Y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
符合预期
然后,需要创建第一个食物
将创建食物的过程同样封装成一个函数
食物的生成位置采用随机生成
需要用到rand和srand
需要注意的是生成的随机数中X的坐标也需要满足是2的倍数
且食物不能生成在墙体里面,也不能生成在蛇的身上
//创建食物
void Creatfood(Snake* snake)
{
SNO* cur = NULL;
again:
cur = snake->phead;
srand((unsigned int)time(NULL));
short a = rand() % 56 + 1;
short b = rand() % 28 + 1;//食物不能生成在墙体内
while (cur)
{
if ((cur->X == a && cur->Y == b) || a % 2 == 1)
{
goto again;//生成到蛇的身体上了就再生成一次
}
cur = cur->next;
}
//坐标合法就打印出来
SNO* food = BuyBodyNode(a, b);
snake->food = food;
Sleep(10);
SetPos(a, b);
wprintf(L"%lc", FOOD);
}
在这之后,再将蛇的其他属性初始化一下
snake->direct = RIGHT;//游戏开始时蛇的方向
snake->Score = 0;// 游戏刚开始时总分数为0
snake->food_score = 10;//默认状态下单个食物的分数
snake->food = NULL;//初始食物位置
snake->state = NORMAL;//初始状态
snake->sleep_time = 1000;//初始速度 = 10;//初始速度
//蛇身的初始化
void InitSnake(Snake* snake)
{
snake->phead = NULL;
for (int i = 0; i < 5; i++)
{
SNO* BodyNode = BuyBodyNode(INITPOSX + (i * 2), INITPOSY);
if (snake->phead == NULL)
{
//如果还没有身体就让蛇头指向这个节点
snake->phead = BodyNode;
}
else
{
BodyNode->next = snake->phead;
snake->phead = BodyNode;
}
}
SNO* cur = snake->phead;
//蛇身的打印
while (cur)
{
SetPos(cur->X, cur->Y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
snake->direct = RIGHT;//游戏开始时蛇的方向
snake->Score = 0;// 游戏刚开始时总分数为0
snake->food_score = 20;//默认状态下单个食物的分数
snake->food = NULL;//初始食物位置
snake->state = NORMAL;//初始状态
snake->sleep_time = 1000;//初始速度
}
再整体封装进GameStart函数中看看效果
void GameStart()
{
WlecSc();
Snake* snake = (Snake*)malloc(sizeof(Snake));
InitSnake(snake);
Creatfood(snake);
getchar();
}
void GameStart()
{
WlecSc();
Snake* snake = (Snake*)malloc(sizeof(Snake));
InitSnake(snake);
Creatfood(snake);
getchar();
}
此时在游戏进行时,还是希望有帮助信息在屏幕上显示
void HelpInfo(Snake* snake)
{
SetPos(80, 15);
printf("当前单个食物分数是:%d\n", snake->food_score);
SetPos(80, 17);
printf("按F3加速,按F4减速\n");
SetPos(80, 19);
printf("按空格暂停,按ESC退出\n");
SetPos(80, 21);
printf("当前的总分数为%d\n", snake->Score);
}
void GameStart()
{
WlecSc();
Snake* snake = (Snake*)malloc(sizeof(Snake));
InitSnake(snake);
Creatfood(snake);
HelpInfo(snake);
getchar();
}
void test()
{
system("title 贪吃蛇");
GameStart();
}
int main()
{
test();
return 0;
}
那么游戏开始这个阶段就完成了
接下来需要完成的是游戏运行这个阶段
8.3 游戏运行
那么在游戏运行这个阶段,要完成的逻辑是
那么在游戏开始后,玩家需要通过按键来控制蛇的移动,这里就需要用到前文中说到的
getAsyncKeyState以及定义的宏
首先理一下逻辑
蛇肯定不能直接朝着当前运动方向的相反方向运动,这样就会吃到自己
void SnakeMove(Snake* snake)
{
if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
{
snake->direct = UP;
}
else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
{
snake->direct = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
{
snake->direct = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
{
snake->direct = RIGHT;
}
}
那么在移动蛇其实就是改变节点坐标
这里使蛇走的下一步是再申请一个节点出来,将这个新的节点再与蛇身连接起来
如果没有吃到食物
那么就将尾节点处的图案使用两个空格覆盖掉
再将尾节点释放掉,置空
首先实现初始状态下蛇的移动,即向右移动
void SnakeMove(Snake* snake)
{
if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
{
snake->direct = UP;
}
else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
{
snake->direct = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
{
snake->direct = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
{
snake->direct = RIGHT;
}
SNO* cur = snake->phead;
SNO* newnode = NULL;
switch (snake->direct)
{
case RIGHT:
newnode = BuyBodyNode(cur->X + 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;
snake->phead = newnode;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
break;
case LEFT:
MoveLeft(snake);
break;
case UP:
MoveUp(snake);
break;
case DOWN:
MoveDown(snake);
break;
}
}
类似的,向左向上向下也是上述逻辑
将他们封装成函数
//向右移动
void MoveRight(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X + 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;
snake->phead = newnode;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
//向左移动
void MoveLeft(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X - 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;
snake->phead = newnode;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
//向上移动
void MoveUp(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X , cur->Y - 1);
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;
snake->phead = newnode;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
//向下移动
void MoveDown(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X , cur->Y + 1);
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;
snake->phead = newnode;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
进行移动就GameRun函数中进行,每次打印间隔一个Sleep函数,就可以实现移动的效果
void GameRun(Snake* snake)
{
do
{
SnakeMove(snake);
Sleep(snake->sleep_time);
} while (snake->state == NORMAL);
}
然后,要实现蛇的加速 / 减速,其实就是在按下对应按键后让sleep_time减少 / 增加
对应单个食物的分数也要加减
else if (KEY_PRESS(VK_F3))
{
if (snake->sleep_time >= 200)
{
snake->sleep_time -= 100;
snake->food_score += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (snake->sleep_time <= 1500)
{
snake->sleep_time += 100;
snake->food_score -= 2;
}
}
空格暂停就是直接让程序休眠
再按一次空格就让程序继续执行
按了一次空格后就死循环的执行Sleep
再按一次就跳出这个循环
else if (KEY_PRESS(VK_SPACE))
{
while (1)
{
Sleep(1);
if (KEY_PRESS(VK_SPACE))
break;
}
}
而按下ESC后,让状态变成END_NORMAL
else if (KEY_PRESS(VK_ESCAPE))
{
snake->state = END_NORMAL;
}
到这里,蛇的移动完成了一部分
//蛇的移动
void SnakeMove(Snake* snake)
{
if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
{
snake->direct = UP;
}
else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
{
snake->direct = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
{
snake->direct = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
{
snake->direct = RIGHT;
}
else if (KEY_PRESS(VK_F3))
{
if (snake->sleep_time > 100)
{
snake->sleep_time -= 100;
}
}
else if (KEY_PRESS(VK_F4))
{
if (snake->sleep_time <= 1500)
{
snake->sleep_time += 100;
}
}
else if (KEY_PRESS(VK_SPACE))
{
while (1)
{
Sleep(1);
if (KEY_PRESS(VK_SPACE))
break;
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
snake->state = END_NORMAL;
}
switch (snake->direct)
{
case RIGHT:
MoveRight(snake);
break;
case LEFT:
MoveLeft(snake);
break;
case UP:
MoveUp(snake);
break;
case DOWN:
MoveDown(snake);
break;
}
}
接下来,就是吃食物的实现
首先,蛇每走一步,都要判断下一步是不是食物,给一个函数判断
//判断蛇走的下一步是不是食物
int NextIsFood(SNO* head,SNO* food)
{
return (head->X == food->X && head->Y == food->Y);
}
如果蛇走的下一步生成的新节点的坐标和食物的坐标相同
那么就吃掉食物
吃掉食物后,蛇的长度会增
这个过程就是将食物的节点挂到蛇的身上同时,不释放尾节点
封装成一个函数叫EatFood
代码直接套用前文中的移动函数,不释放尾节点就好了
void EatFood(Snake* snake, SNO* food)
{
food->next = snake->phead;//连接起来
snake->phead = food;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
Creatfood(snake);
}
那么没吃到食物就是NotFood
把前文的移动函数中的过程封装在里面即可
//没吃到食物
void NotFood(Snake* snake, SNO* newnode,SNO* cur)
{
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;//连接起来
snake->phead = newnode;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
再将前面的移动代码更改一下,使用这两个函数
//向右移动
void MoveRight(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X + 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode,cur);
}
}
//向左移动
void MoveLeft(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X - 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode,cur);
}
}
//向上移动
void MoveUp(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X , cur->Y - 1);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode,cur);
}
}
//向下移动
void MoveDown(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X , cur->Y + 1);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode,cur);
}
}
接下来来判定是否撞墙
逻辑就是判断走的下一步新生成的节点的X坐标是否等于0或60 Y坐标是否等于0或30
是的话就让蛇的状态为KILL_BY_WALL
//撞到墙了
int HitWall(Snake* snake)
{
SNO* newnode = snake->phead;
if (newnode->X == 0 || newnode->Y == 0 || newnode->X == 60 || newnode->Y == 30)
{
snake->state = KILL_BY_WALL;
return 1;
}
return 0;
}
通过返回值判断是不是撞墙,然后再将这个函数封装进NotFood中即可
//没吃到食物
void NotFood(Snake* snake, SNO* newnode, SNO* cur)
{
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;//连接起来
snake->phead = newnode;
if (HitWall(snake))
{
return;
}
else
{
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
}
迟到自己也封装成一个函数KillSelf
判断走的那一步节点是不是身体的节点就可以了
是的话就让蛇的状态为KILL_BY_SELF
//吃到自己了
int KillSelf(Snake* snake)
{
SNO* head = snake->phead->next;
while (head)
{
if (snake->phead->X == head->X && snake->phead->Y == head->Y)
{
snake->state = KILL_BY_SELF;
return 1;
}
head = head->next;
}
return 0;
}
同样的也放进NotFood中
//没吃到食物
void NotFood(Snake* snake, SNO* newnode, SNO* cur)
{
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;//连接起来
snake->phead = newnode;
if (HitWall(snake))
{
return;
}
else if (KillSelf(snake))
{
return;
}
else
{
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
}
那么到这里,游戏运行过程就基本完成了
//蛇的移动
void SnakeMove(Snake* snake)
{
if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
{
snake->direct = UP;
}
else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
{
snake->direct = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
{
snake->direct = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
{
snake->direct = RIGHT;
}
else if (KEY_PRESS(VK_F3))
{
if (snake->sleep_time > 100)
{
snake->sleep_time -= 100;
snake->food_score += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (snake->sleep_time < 1500)
{
snake->sleep_time += 100;
snake->food_score -= 2;
}
}
else if (KEY_PRESS(VK_SPACE))
{
SetPos(80, 10);
printf("当前游戏处于暂停状态!\n");
SetPos(80, 11);
printf("再按一次空格游戏继续!\n");
while (1)
{
Sleep(1);
if (KEY_PRESS(VK_SPACE))
{
SetPos(80, 10);
printf(" ");
SetPos(80, 11);
printf(" ");
break;
}
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
snake->state = END_NORMAL;
}
switch (snake->direct)
{
case RIGHT:
MoveRight(snake);
break;
case LEFT:
MoveLeft(snake);
break;
case UP:
MoveUp(snake);
break;
case DOWN:
MoveDown(snake);
break;
}
}
void GameRun(Snake* snake)
{
do
{
Sleep(snake->sleep_time);
SnakeMove(snake);
HelpInfo(snake);
} while (snake->state == NORMAL);
switch (snake->state)
{
case END_NORMAL:
SetPos(80, 30);
wprintf(L"您主动退出!\n");
break;
case KILL_BY_SELF:
SetPos(80, 30);
wprintf(L"您咬到了自己\n");
break;
case KILL_BY_WALL:
SetPos(80, 30);
wprintf(L"您撞到了墙\n");
break;
}
}
8.4 游戏结束
由于采用的是动态申请节点
在游戏结束后,需要将蛇和场上剩下的食物释放掉
//释放蛇和食物
void DestorySnake(Snake* snake)
{
free(snake->food);
snake->food = NULL;//释放剩下的食物节点
SNO* cur = snake->phead;
while (snake->phead)
{
SNO* cur = snake->phead->next;//保存释放节点的下一个节点
free(snake->phead);//释放蛇身
snake->phead = NULL;
snake->phead = cur;
}
}
结束后还需要问一下是不是要再来一局
SetPos(80, 35);
wprintf(L"想要再来一局吗(Y/N)\n");
SetPos(80, 36);
CursorShow();
char ch = getchar();
getchar();
if (ch == 'Y')
{
system("cls");
ch = '0';
goto again;
}
else
{
system("cls");
SetPos(50, 16);
wprintf(L"Exit!\n");
return;
}
到这里整个游戏就算是基本完成了
9. 完整代码
//Snake.h
#pragma once
#pragma once
#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<Windows.h>
#include<locale.h>
#include<stdlib.h>
#include<time.h>
#define INITPOSX 20
#define INITPOSY 10
#define BODY L'●'
#define FOOD L'▲'
#define WALL L'□'
#define KEY_PRESS(VK) (GetAsyncKeyState(VK)&1?1:0)
enum Direction
{
UP = 1,
DOWN,
LEFT,
RIGHT,
};
typedef enum Direction Direction;
enum Statement
{
NORMAL = 1,//正常行动
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//吃到自己
END_NORMAL//主动退出
};
typedef enum Statement Statement;
void draw();//绘制游戏地图
void SetPos(short X, short Y);//定位光标位置
void WlecSc();//欢迎界面
//蛇身结构体
struct SnakeNode
{
//坐标
short X;
short Y;
struct SnakeNode* next;
};
typedef struct SnakeNode SNO;
//蛇
struct Snake
{
SNO* phead;//指向蛇头的指针
SNO* food;//指向食物的指针
Direction direct;//方向
Statement state;//状态
int food_score;//单个食物分数
int Score;//总分数
int sleep_time;//休息时间
};
typedef struct Snake Snake;
//隐藏光标
void HiddenCursor();
//帮助信息
void HelpInfo(Snake* snake);
//申请节点
SNO* BuyNode(short a, short b);
//初始化蛇
void InitSnake(Snake* snake);
//创建食物
void Creatfood();
//蛇的移动
void SnakeMove(Snake* snake);
//向上移动
void MoveUp(Snake* snake);
//向下移动
void MoveDown(Snake* snake);
//向右移动
void MoveRight(Snake* snake);
//向左移动
void MoveLeft(Snake* snake);
//判断蛇走的下一步是不是食物
int NextIsFood(SNO* head, SNO* food);
//蛇吃食物
void EatFood(Snake* snake, SNO* food);
//没吃到食物
void NotFood(Snake* snake, SNO* newnode);
//撞到墙了
int HitWall(Snake* snake);
//吃到自己了
int KillSelf(Snake* snake);
//释放蛇和食物
void DestorySnake(Snake* snake);
//显示光标
void CursorShow();
//Start.c
#include"Snake.h"
//地图的绘制
void draw()
{
SetPos(0, 0);
setlocale(LC_ALL, "");
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc", L'□');
}
SetPos(0, 29);//将光标移动到(0,29)这个位置
for (int i = 0; i < 30; i++)
{
wprintf(L"%lc", L'□');
}//下方
for (int i = 1; i < 29; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}//左方
for (int i = 1; i < 29; i++)
{
SetPos(58, i);//2*X = Y
wprintf(L"%lc", L'□');
}//右方
}
//设置光标位置
void SetPos(short X, short Y)
{
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { X,Y };
SetConsoleCursorPosition(houput, pos);
}
void HelpInfo(Snake* snake)
{
SetPos(80, 15);
printf("当前单个食物分数是:%d\n", snake->food_score);
SetPos(80, 17);
printf("按F3加速,按F4减速\n");
SetPos(80, 19);
printf("按空格暂停,按ESC退出\n");
SetPos(80, 21);
printf("当前的总分数为%d\n", snake->Score);
}
//界面
void WlecSc()
{
HiddenCursor();
SetPos(50, 16);
printf("欢迎来到贪吃蛇游戏\n");
SetPos(50, 18);
system("pause");
SetPos(30, 16);
printf("使用↑ ↓ ← → 控制蛇的方向,F3为加速,F4为减速,加速将获得更高的分数\n");
SetPos(35, 17);
printf("撞到墙体或者吃到自己的身体都会导致死亡,游戏结束\n");
SetPos(50, 18);
system("pause");
system("cls");
draw();
}
//申请身体节点
SNO* BuyBodyNode(short a, short b)
{
SNO* newnode = (SNO*)malloc(sizeof(SNO));
if (newnode == NULL)
{
printf("游戏出现错误,请退出后重试\n");
exit(1);
}
newnode->X = a;
newnode->Y = b;
newnode->next = NULL;
return newnode;
}
//创建食物
void Creatfood(Snake* snake)
{
SNO* cur = NULL;
again:
cur = snake->phead;
srand((unsigned int)time(NULL));
short a = rand() % 56 + 1;
short b = rand() % 28 + 1;//食物不能生成在墙体内
while (cur)
{
if ((cur->X == a && cur->Y == b) || a % 2 == 1)
{
goto again;//生成到蛇的身体上了就再生成一次
}
cur = cur->next;
}
//坐标合法就打印出来
SNO* food = BuyBodyNode(a, b);
snake->food = food;
Sleep(10);
SetPos(a, b);
wprintf(L"%lc", FOOD);
}
//蛇身的初始化
void InitSnake(Snake* snake)
{
snake->phead = NULL;
for (int i = 0; i < 5; i++)
{
SNO* BodyNode = BuyBodyNode(INITPOSX + (i * 2), INITPOSY);
if (snake->phead == NULL)
{
//如果还没有身体就让蛇头指向这个节点
snake->phead = BodyNode;
}
else
{
BodyNode->next = snake->phead;
snake->phead = BodyNode;
}
}
SNO* cur = snake->phead;
//蛇身的打印
while (cur)
{
SetPos(cur->X, cur->Y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
snake->direct = RIGHT;//游戏开始时蛇的方向
snake->Score = 0;// 游戏刚开始时总分数为0
snake->food_score = 10;//默认状态下单个食物的分数
snake->food = NULL;//初始食物位置
snake->state = NORMAL;//初始状态
snake->sleep_time = 1000;//初始速度
}
//隐藏光标
void HiddenCursor()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houput, &cursor_info);
cursor_info.bVisible = false;
SetConsoleCursorInfo(houput, &cursor_info);
}
//Run.c
#include"Snake.h"
//向右移动
void MoveRight(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X + 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode, cur);
}
}
//向左移动
void MoveLeft(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X - 2, cur->Y);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode, cur);
}
}
//向上移动
void MoveUp(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X, cur->Y - 1);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode, cur);
}
}
//向下移动
void MoveDown(Snake* snake)
{
SNO* cur = snake->phead;
SNO* newnode = NULL;
newnode = BuyBodyNode(cur->X, cur->Y + 1);
while (cur->next->next)
{
cur = cur->next;
}
if (NextIsFood(newnode, snake->food))
{
EatFood(snake, snake->food);
free(newnode);
newnode = NULL;
}
else
{
NotFood(snake, newnode, cur);
}
}
//蛇的移动
void SnakeMove(Snake* snake)
{
if (KEY_PRESS(VK_UP) && snake->direct != DOWN)
{
snake->direct = UP;
}
else if (KEY_PRESS(VK_DOWN) && snake->direct != UP)
{
snake->direct = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && snake->direct != RIGHT)
{
snake->direct = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && snake->direct != LEFT)
{
snake->direct = RIGHT;
}
else if (KEY_PRESS(VK_F3))
{
if (snake->sleep_time > 100)
{
snake->sleep_time -= 100;
snake->food_score += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (snake->sleep_time <= 1500)
{
snake->sleep_time += 100;
snake->Score -= 1;
}
}
else if (KEY_PRESS(VK_SPACE))
{
SetPos(80, 10);
printf("当前游戏处于暂停状态!\n");
SetPos(80, 11);
printf("再按一次空格游戏继续!\n");
while (1)
{
Sleep(1);
if (KEY_PRESS(VK_SPACE))
{
SetPos(80, 10);
printf(" ");
SetPos(80, 11);
printf(" ");
break;
}
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
snake->state = END_NORMAL;
}
switch (snake->direct)
{
case RIGHT:
MoveRight(snake);
break;
case LEFT:
MoveLeft(snake);
break;
case UP:
MoveUp(snake);
break;
case DOWN:
MoveDown(snake);
break;
}
}
//判断蛇走的下一步是不是食物
int NextIsFood(SNO* head, SNO* food)
{
return (head->X == food->X && head->Y == food->Y);
}
//移动均是生成新的节点
//头插到蛇的身体上
//如果吃到食物就让不释放蛇的最后一个节点
void EatFood(Snake* snake, SNO* food)
{
food->next = snake->phead;//连接起来
snake->phead = food;
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
snake->Score += snake->food_score;
Creatfood(snake);
}
//没吃到食物
void NotFood(Snake* snake, SNO* newnode, SNO* cur)
{
SetPos(cur->next->X, cur->next->Y);
printf(" ");
free(cur->next);
cur->next = NULL;
newnode->next = snake->phead;//连接起来
snake->phead = newnode;
if (HitWall(snake))
{
return;
}
else if (KillSelf(snake))
{
return;
}
else
{
SetPos(snake->phead->X, snake->phead->Y);
wprintf(L"%lc", BODY);
}
}
//撞到墙了
int HitWall(Snake* snake)
{
SNO* newnode = snake->phead;
if (newnode->X == 0 || newnode->Y == 0 || newnode->X == 60 || newnode->Y == 30)
{
snake->state = KILL_BY_WALL;
return 1;
}
return 0;
}
//吃到自己了
int KillSelf(Snake* snake)
{
SNO* head = snake->phead->next;
while (head)
{
if (snake->phead->X == head->X && snake->phead->Y == head->Y)
{
snake->state = KILL_BY_SELF;
return 1;
}
head = head->next;
}
return 0;
}
//End.c
#include"Snake.h"
//释放蛇和食物
void DestorySnake(Snake* snake)
{
free(snake->food);
snake->food = NULL;//释放剩下的食物节点
SNO* cur = snake->phead;
while (snake->phead)
{
SNO* cur = snake->phead->next;//保存释放节点的下一个节点
free(snake->phead);//释放蛇身
snake->phead = NULL;
snake->phead = cur;
}
}
//显示光标
void CursorShow()
{
//获得标准输出设备的句柄
HANDLE houput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houput, &cursor_info);
cursor_info.bVisible = true;
SetConsoleCursorInfo(houput, &cursor_info);
}
//Game.c
#include"Snake.h"
void GameStart(Snake* snake)
{
WlecSc();
InitSnake(snake);
Creatfood(snake);
HelpInfo(snake);
}
void GameRun(Snake* snake)
{
do
{
Sleep(snake->sleep_time);
SnakeMove(snake);
HelpInfo(snake);
} while (snake->state == NORMAL);
switch (snake->state)
{
case END_NORMAL:
SetPos(80, 30);
wprintf(L"您主动退出!\n");
break;
case KILL_BY_SELF:
SetPos(80, 30);
wprintf(L"您咬到了自己\n");
break;
case KILL_BY_WALL:
SetPos(80, 30);
wprintf(L"您撞到了墙\n");
break;
}
}
void GameEnd(Snake* snake)
{
DestorySnake(snake);//释放蛇和食物
SetPos(80, 35);
wprintf(L"想要再来一局吗(Y/N)\n");
SetPos(80, 36);
CursorShow();
}
本篇文章到此结束