实现基本的功能:
- 贪吃蛇地图绘制
- 蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞⾃⾝死亡
- 计算得分
- 蛇⾝加速、减速
- 暂停游戏
一、地图的制作思路:
关于地图的制作,我们要将地图制作成什么样子呢?
在我们的预想中,我们需要使用控制台的界面进行贪吃蛇的游戏操作。
二、地图内的场景设置:
- 在游戏地图上,我们打印墙体使用宽字符‘□’,打印蛇使用宽字符'●',打印食物使用宽字符'★'
三、地图坐标和宽字符的冲突:
由于我们是在控制台上进行实现贪吃蛇游戏,所以我们需要在控制台上实现以宽字符为主体的墙面、蛇身、食物,但是在控制台中,我们是以普通字符为主,作为一个坐标点的。
而普通字符是以一个字节为单位,可是宽字符是以两个字节为一组的。
并且根据打印宽字符的特点,只需要打印一个坐标点,就能将宽字符完全显现,比如,在(5,5)上打印宽字符,则宽字符会占据两个字节的位置,会把(6,5)的位置也占了 。
- 所以,最后,我们在考虑墙面、蛇身、食物的时候,我们必须要往2的倍数方向进行考虑,特别是蛇身,我们需要将蛇身以2的倍数进行构造。
四、蛇的身体分析:
- 对于蛇的身体,我们需要用到链表的知识,将蛇的身体化为一个又一个字节,使用指针进行连接。
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode *next;
}SnakeNode;
- int x ;int y ;表示控制台的坐标。
- SnakeNode 是使用 typedef 将struct SankNode 进行重命名后的名字,方便以后书写suruct SankNode
五、贪吃蛇游戏的文件:
- test.c —— —— 游戏的整体运行测试
- snake.c —— —— 函数的实现
- snake.h —— —— 函数和类型的声明
六、贪吃蛇的游戏结构:
需要的内容:
对于一个贪吃蛇游戏,蛇是必须存在,且因为蛇是一个链表形式,所以我们需要蛇的头节点的指针
同时,根据贪吃蛇的游戏规则,我们需要一个食物,且因为吃掉食物蛇会变长,因此食物也可以看作链表中的一个节点,只是这个节点需要使用链表的某些操作进行插入到蛇的身体中,所以我们需要食物的节点指针
因为贪吃蛇是一个看长度积分的游戏,所以我们需要一个累计分数的变量
同时,我们也需要对食物赋予分数
同时,我们需要对蛇进行一个速度的附加值,让它加速减速
对于操作蛇,是一个非常重要的东西,我们要让蛇上下左右进行移动
最后,我们需要对整个游戏的状态进行判断和显示出该状态下会出现的结果
//维护蛇的方向
typedef enum Direction
{
Right = 1,
Left,
Up,
Down
}Direction;
//维护蛇的状态
typedef enum
{
OK=1,
KILL_BY_SELF,
KILL_BY_WALL,
END_NOMAL
}game_status;
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode *next;
}SnakeNode;
//用来维护整个蛇的全局状态
typedef struct Snake
{
SnakeNode* pSnake;
SnakeNode* pFood;
Direction Dir;
game_status Status;
int Score;
int Foodweight;
int Sleep_Time;
}Snake;
七、游戏的流程:
整个游戏分为三个部分:
游戏开始:进行游戏窗口的设置,游戏的欢迎界面,游戏的地图创建, 蛇和食物的创建和初始化。
游戏运行:游戏界面右侧的提示消息,分数的增减和食物的分数增减,获取按键的信息,蛇的移动。
游戏结束:结束的原因,提示语的清理,是否再来一把游戏的设置。
注意:三个部分均要使用到贪吃蛇游戏的结构体,所以传参必须传贪吃蛇游戏结构体。
八、代码准备:
宏定义宽字符:
#define WALL L'□'
#define BODY L'●' //★○●◇◆□■
#define FOOD L'★'
光标定位:
//光标定位函数
void SetPos(int x, int y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
九、游戏开始 :
9.1游戏窗口的设置和光标的隐藏:
//设置控制台窗口
system("mode con cols=150 lines=50");
system("title 贪吃蛇");
//使控制台光标消失
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄(⽤来标识不同设备的数值)
//定义一个存贮光标信息的指针
CONSOLE_CURSOR_INFO CursorInfo;
//利用控制台句柄获取控制台光标信息,存到CursorInfo中
GetConsoleCursorInfo(hOutput, &CursorInfo);
//修改指针使光标隐藏
CursorInfo.bVisible = false;
//将修改好的指针传过去修改控制台信息;
SetConsoleCursorInfo(hOutput, &CursorInfo);
9.2欢迎界面的打印:
把光标定位到相应的位置后,使用printf进行打印欢迎界面的文字
同时,因为欢迎界面有两个,所以需要在打印完一个欢迎界面后,使用cls进行重置清屏
之后再度将光标进行定位以此进行第二个欢迎界面的打印。
之后以此类推,但是在打完最后一个欢迎界面后,我们要再次cls进行清屏操作,为之后的操作腾出空屏
SetPos(65, 20);
printf("欢迎来到贪吃蛇小游戏");
SetPos(65, 30);
system("pause");
system("cls");
SetPos(70, 20);
printf("游戏开始");
system("cls");
9.3创造地图:
如图所示,我们需要对墙体进行打印,需要用到定位光标。
因为墙体是宽字符,所以在进行墙体计算的时候,我们根据墙体的左上角的坐标来进行计算
在得知控制台中的x轴和y轴后,我们对墙体的打印顺序分为,上、下、左、右
又因为宽字符的原因,墙体的x坐标必须以2进行增长的,但y并不是,y是竖着的,宽字符只是宽度变成普通字符的两倍,并不是大小。
所以,上下两行的打印要以2增长,左右是以一倍增长,但是左右是在上下打印之后打印的,所以左右不能重复打印上下的部分。
且左右需要进行挨个打印,所以需要动态的坐标点。
注意:打印宽字符需要特定的宽字符打印形式。
int i = 0;
for (i = 0; i <= 100; i += 2)
{
SetPos(i, 0);
wprintf(L"%c", WALL);//语句全用宽字符类型
}
for (i = 0; i <= 100; i += 2)
{
SetPos(i, 40);
wprintf(L"%c", WALL);
}
for (i = 1; i <= 40; i++)
{
SetPos(0, i);
wprintf(L"%c", WALL);
}
for (i = 1; i <= 40; i++)
{
SetPos(100, i);
wprintf(L"%c", WALL);
}
9.4蛇的状态初始化:
步骤:
蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
创建5个节点,然后将每个节点存放在链表中进⾏管理。
创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
设蛇的初始位置从 (24,5) 开始。
再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,每个⻝物的分数。
初始化的游戏状态:
游戏状态是:OK
蛇的移动速度:200毫秒
蛇的默认⽅向:RIGHT
初始成绩:0
每个⻝物的分数:10
首先,对于蛇的身体,我们需要使用蛇身体的结构体,其次因为在我们的预想中,蛇的身体是有五个节点组成的链表,于是我们需要使用malloc函数进行空间的开辟,并且使用循环进行开辟五次。
void SnakeCreat(Snake*ps)
{
int i = 0;
SnakeNode* cur = NULL;
for (i = 0; i < 5; i++)
{
SnakeNode* node = (SnakeNode*)malloc(sizeof(SnakeNode));
node->x = 24 + 2 * i;
node->y = 20;
node->next = NULL;
if (cur==NULL)
{
cur = node;
}
else
{
node->next = cur;
cur = node;
}
}
ps->pSnake = cur;
//蛇身打印
SnakePrint(ps);
//将剩下蛇的信息初始化补全(除了食物信息)
ps->Dir = Right;
ps->Foodweight = 10;
ps->Score = 0;
ps->Sleep_Time = 200;
ps->Status = OK;
}
void SnakePrint(Snake* ps)
{
SnakeNode* cur = ps->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
}
9.5食物的初始化:
- 先随机⽣成⻝物的坐标
- x坐标必须是2的倍数
- ⻝物的坐标不能和蛇⾝每个节点的坐标重复
- 食物不能再墙的外面
- 创建⻝物节点,打印⻝物
-
因为必须是随机的,所以我们需要再主函数中加入srand
void FoodCreate(Snake* ps)
{
int x = 0, y = 0;
SnakeNode* cur = ps->pSnake;
//保证食物不出墙,且x为偶数
again:
do {
x = rand() % 97 + 2;//x范围2-98
y = rand() % 39 + 1;//y范围1-39
} while (x % 2 != 0);
//保证食物与蛇身不重合
while(cur)
{
if (cur->x == x && cur->y == y)
goto again;//重合时重新生成食物
cur = cur->next;
}
SnakeNode* food = (SnakeNode*)malloc(sizeof(SnakeNode));
food->x = x; food->y = y;
SetPos(x, y);
wprintf(L"%c", FOOD);
ps->pFood = food;
}
十、游戏运行:
- 游戏运⾏期间,右侧打印帮助信息,提⽰玩家,坐标开始位置(64, 15)
- 同时再右侧打印分数信息,分数是动态的
- 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
- 如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
10.1 右侧提示信息:
void GameHelp()
{
SetPos(110, 26);
printf("使用↑↓←→进行操作");
SetPos(110, 28);
printf("按F3键加速,F4键减速");
SetPos(110, 30);
printf("按ESC键退出,空格键暂停");
SetPos(110, 32);
printf("贪吃蛇小游戏--凤曦出品");
}
10.2 检测按键状态,我们封装了⼀个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
10.3分数的动态打印和按键的检测与方向的调整 / 蛇的加速和减速
do {
//打印得分信息
SetPos(110, 15);
printf("总得分:%d", ps->Score);
SetPos(110, 16);
printf("每个食物得分:%5d", ps->Foodweight);
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_ESCAPE))
{
ps->Status = END_NOMAL;
break;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->Sleep_Time > 50)
{
ps->Sleep_Time -= 30;
ps->Foodweight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->Sleep_Time<260)
{
ps->Sleep_Time +=30;
ps->Foodweight -=2;
}
}
else if (KEY_PRESS(VK_SPACE))
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
break;
}
}
//休眠一帧移动一帧
Sleep(ps->Sleep_Time);
10.4 蛇的移动
蛇的移动分为两个部分,一个是蛇移动到的节点是食物,另一个是蛇移动到的节点不是食物
创造下一个节点:
//创建蛇方向的下一个节点
SnakeNode* node = (SnakeNode*)malloc(sizeof(SnakeNode));
根据当前蛇头节点对下一个节点的位置进行预估判断
//监测蛇的方向
switch (ps->Dir)
{
case Up:
{
node->x = ps->pSnake->x;
node->y = ps->pSnake->y - 1;
}
break;
case Down:
{
node->x = ps->pSnake->x;
node->y = ps->pSnake->y + 1;
}
break;
case Left:
{
node->x = ps->pSnake->x-2;
node->y = ps->pSnake->y;
}
break;
case Right:
{
node->x = ps->pSnake->x+2;
node->y = ps->pSnake->y;
}
break;
}
下一个节点是食物:
如果移动的下一个节点是食物,则更具食物的特点,直接将食物所在的节点进行头插,头插到蛇头上,再将原先食物的节点变成新的蛇头。
蛇头和食物的节点结构体是一样的。
同时根据游戏的规则,迟到食物就相当于要加分,加的分数就是食物的分数
而吃掉食物后,我们要新建了一个食物,保持食物一直存在,并把之前的食物节点指针进行释放
然后就是打印蛇,打印蛇就直接进行遍历打印即可
//吃到食物的蛇
if (ps->pFood->x == node->x && ps->pFood->y == node->y)
{
ps->Score += ps->Foodweight;
node->next = ps->pSnake;
ps->pSnake = node;
//打印蛇
SnakeNode* cur = ps->pSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
free(ps->pFood);
FoodCreate(ps);}
下一个节点不是食物:
- 如果下一个节点不是食物的话,蛇是不会增长长度的
- 于是我们还是需要使用头插将食物的节点进行插入到蛇的头节点中,然后蛇的头节点变成原来食物的节点
- 但是再打印的时候把蛇的最后一个节点不打印,保持蛇的长度不变,给视觉造成效果
- 同时把最后一个节点置空,然后打印为 “ ”
//没吃到食物的蛇
else
{
node->next = ps->pSnake;
ps->pSnake = node;
SnakeNode* cur = ps->pSnake;
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
- while (cur->next->next)的目的是使得遍历的过程中,只在倒数第二个节点便会停下
- 因为我们并没有直接把最后一个节点置空,所以cur->next==NULL(相当于 while (cur->next))是无效的
10.5 蛇的死亡状态
在游戏运行的最后,我们需要对蛇的状态进行判断,通过判断来判断游戏是否该结束。
死于墙面:
死于墙面,就判断蛇的头指针坐标是否和墙面的坐标冲突重复
而判断蛇的头指针坐标是否和墙面起到冲突就看x坐标和y坐标。
上下墙面的y坐标是一样的,左右墙面的x坐标是一样的
所以看x坐标是否和左右墙面x坐标重复,看y坐标是否和上下墙面的y坐标重复
void KillByWall(Snake* ps)
{
if (ps->pSnake->x >= 100 || ps->pSnake->x <= 0 || ps->pSnake->y <= 0 || ps->pSnake->y >= 40)
ps->Status = KILL_BY_WALL;
}
死于自杀:
死于自杀,就判断蛇的头指针是否和蛇的身体坐标冲突重复
而判断蛇的头指针是否和蛇身体坐标重复,就得遍历蛇的身体坐标进行对比判断
void KillBySelf(Snake* ps)
{
SnakeNode* cur = ps->pSnake->next;
while (cur)
{
if ((ps->pSnake->x == cur->x)
&& (ps->pSnake->y == cur->y))
{
ps->Status = KILL_BY_SELF;
return;
}
cur = cur->next;
}
return;
}
十一、游戏结束:
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。
void GameOver(Snake* ps)
{
SetPos(1, 41);
switch (ps->Status)
{
case KILL_BY_SELF:
printf("自杀死亡\n"); break;
case KILL_BY_WALL:
printf("撞墙死亡\n"); break;
case END_NOMAL:
printf("退出游戏\n"); break;
}
}