一、前言
通过上一篇文章----《贪吃蛇的实现(一)》,讲述了贪吃蛇所要用到的控制台的函数,那么本章要实现的就是贪吃蛇游戏的运行,接下来,就请我们一起来学习吧,此外在这里附上上一篇文章的链接,以供大家了解控制台函数的相关知识。贪吃蛇的实现(一)-CSDN博客
二、贪吃蛇的游戏框架
2.1准备阶段
准备阶段总共分为三个:吃蛇进入界面的设置、贪吃蛇围墙、蛇身和食物的初始化。
2.2游戏运行阶段
游戏运行阶段所要注意的细节就多了,包括贪吃蛇的移动和移动快慢,用来控制贪吃蛇的按键设置,游戏暂停、退出、游戏的运行状态(正常、撞自己死亡、撞墙死亡、正常结束)等。
2.3游戏退出后阶段
当游戏结束后,我们应做好游戏的善后工作,及时的释放蛇身的空间,并提示玩家是否再玩一局,如果不是则完全退出游戏。
三、主程序设计
主程序设计主要有三种:游戏初始化、游戏运行、游戏结束。
四、结构定义
贪吃蛇的实现是基于链表的结构,所以应对蛇定义一个链表的结构,而蛇的全局情况则分为多类:蛇的状态、蛇的移动、蛇本身、食物、蛇的方向、当前获得的分数、每个食物的分数、每走一步所要休眠的时间。
具体定义如下:
//蛇节点的定义
typedef struct SNAKE
{
int x;
int y;
struct SNAKE* next;
}SnakeSelf;
//蛇的状态
enum SnakeState
{
OK = 1,
Kill_By_Wall,
Kill_By_Self,
Exit
};
//蛇的方向
enum Direction
{
Up = 1,
Down,
Left,
Right
};
//蛇本身的情况
typedef struct SNAKESTATE
{
SnakeSelf* snake;
SnakeSelf* food;
enum SnakeState;
enum Direction;
int score;
int food_score;
int Sleep_time;
}snakestate;
五、游戏程序设计
5.1游戏初始化
初始化贪吃蛇,首先要设置窗口大小,再绘制欢迎界面、游戏地图、贪吃蛇蛇身建立和食物的生成。
5.1.1设置窗口大小
//0.先设置窗口大小,再光标隐藏
system("mode con cols=100 lines=35");
system("title 贪吃蛇");
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput, &CursorInfo);
5.1.2绘制欢迎界面
绘制欢迎界面,首先要将光标定位,再根据界面中合适的位置进行打印游戏规则。
//光标设置
void Set_cur_pos(short x, short y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
// 欢迎界面
void WelcomeToGame()
{
//光标定位
Set_cur_pos(40, 13);//光标定位
wprintf(L"欢迎来到贪吃蛇小游戏\n");
Set_cur_pos(41, 25);
system("pause");
system("cls");
Set_cur_pos(25, 13);
wprintf(L"用↑.↓.←.→控制贪吃蛇的方向,F3为加速,F4为减速");
Set_cur_pos(25, 14);
wprintf(L"吃掉食物可以获得更高的分数\n");
Set_cur_pos(41, 25);
system("pause");
system("cls");
}
5.1.3绘制地图
对于游戏中地图的设置,大家可以根据自己的想法去实现,但是要特别注意的是,系统内部的坐标轴与我们平时理解的并不相同,具体如下:
如图所示,图上的y轴与我们平时所认知的一样,都是依次增加1,但是如果我们要用符号或者是中文环境打印的话,就是一个中文记号(包括中文文字、符号等)占两个单位,也就是说在打印墙体的时候,要把握好横向和竖向的比例关系,否则就会出现许多意料不到的场景。
//光标设置
void Set_cur_pos(short x, short y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
//绘制地图
void CreatMap()
{
//1.上
//Set_cur_pos(0, 0);//定位坐标
for (int i = 0; i < 29; i++)
wprintf(L"%lc", WALL);
//2.下
Set_cur_pos(2,26);
for (int i = 1 ; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//3.左
Set_cur_pos(0, 1);
for (int i = 0; i < 26; i++)
{
Set_cur_pos(0, i+1);
wprintf(L"%lc", WALL);
}
//4.右
Set_cur_pos(58, 1);
for (int i = 0; i < 25; i++)
{
Set_cur_pos(56, i+1);
wprintf(L"%lc", WALL);
}
Set_cur_pos(61, 30);
}
如此一来,一个简易的围墙就建好了。
5.1.4创建蛇身
对蛇身的定义,我们是基于链表的实现,在创建过程中蛇身的x轴要是一个偶数,因为在游戏运行中,蛇的移动是一格一格的向上下左右移动,如果x轴坐标为奇数,就可能会出现蛇尾卡在墙体中等情况。
//初始化蛇身
void SnakeInit(ppsnake pf)
{
psnake cur = NULL;
for (int i = 0; i < 3; i++)
{
cur = (psnake)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("SnakeInit():malloc");
return;
}
cur->x = POS_X + (i * 2);
cur->y = POS_Y;
cur->next = NULL;
if (pf->snakehead == NULL)
{
pf->snakehead = cur;
}
else
{
cur->next = pf->snakehead;
pf->snakehead = cur;
}
}
cur = pf->snakehead;
while (cur)
{
Set_cur_pos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置贪吃蛇属性
pf->direction = RIGHT;
pf->food_score = 10;
pf->sleep_time = 200;
pf->state = OK;
}
5.1.5创建食物
食物的创建主要分为3个注意项:一是每次食物的出现都必须是随机的,二是食物必须在围墙的范围内,三是食物的位置不能卡在墙体中或者卡在蛇身的位置中,所以食物节点的生成必须是随机且在范围之内。
//创建食物
void CreatFood(ppsnake pf)
{
int x = 0;
int y = 0;
again:
x = rand() % 53 + 2;
y = rand() % 25 + 1;
while (x % 2 != 0)
{
x = rand() % 53 + 2;
}
psnake cur = pf->snakehead;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->next;
}
//创建食物节点
psnake tmp = (psnake)malloc(sizeof(SnakeNode));
if (tmp == NULL)
{
perror("SnakeInit():malloc");
return;
}
tmp->x = x;
tmp->y = y;
tmp->next = NULL;
Set_cur_pos(x,y);
wprintf(L"%lc", FOOD);
pf->food = tmp;
//getchar();
}
至此,游戏初始化就完成了。
5.2运行游戏
游戏运行环节是整个游戏的核心,它包括了分数的更新、蛇的移动、每走一步蛇的状态的判定、蛇的速度、蛇吃掉食物后的变化等等。
在写程序前,先把贪吃蛇的头文件中定义的类型一一列举出来 ,供大家参考。
#include<stdio.h>
#include<locale.h>
#include<assert.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>
#define POS_X 24
#define POS_Y 3
#define BODY L'●'
#define WALL L'■'
#define FOOD L'◆'
//键值定义
#define key_val(vk) ( (GetAsyncKeyState(vk) & 0x1) ? 1 : 0 )
//类型定义
//蛇的方向
enum SnakeDirection
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum SnakeState
{
OK,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞到自己
EXIT//退出
};
//贪吃蛇结构的定义
typedef struct SnakeNode
{
//贪吃蛇的坐标
int x;
int y;
//贪吃蛇的下一个节点
struct SnakeNode* next;
}SnakeNode, *psnake;
//贪吃蛇定义
typedef struct Snake
{
//指向蛇头的节点
psnake snakehead;
//指向食物的节点
psnake food;
//蛇的方向
enum SnakeDirection direction;
//蛇的状态
enum SnakeState state;
//总分数
int score;
//食物的分数
int food_score;
//蛇的速度
int sleep_time;
}Snake,*ppsnake;
游戏运行程序:
//撞墙
void Kill_By_Wall(ppsnake pf)
{
if (pf->snakehead->x == 0 ||
pf->snakehead->x == 56 ||
pf->snakehead->y == 0 ||
pf->snakehead->y == 26)
{
Set_cur_pos(35, 10);
wprintf(L"%s", L"你撞墙了");
pf->state = KILL_BY_WALL;
}
}
//撞到自己
void Kill_By_Self(ppsnake pf)
{
psnake cur = NULL;
if (pf->snakehead->next->next)
{
cur = pf->snakehead->next->next;
while (cur)
{
if (pf->snakehead->x == cur->x && pf->snakehead->y == cur->y)
{
Set_cur_pos(35, 10);
wprintf(L"%s", L"你撞到自己了");
pf->state = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
}
//蛇的移动
void SnakeMove(ppsnake pf)
{
psnake tmp = (psnake)malloc(sizeof(SnakeNode));
if (tmp == NULL)
{
perror("SnakeMove():malloc");
return;
}
switch (pf->direction)
{
case UP:
tmp->x = pf->snakehead->x;
tmp->y = pf->snakehead->y - 1;
break;
case DOWN:
tmp->x = pf->snakehead->x;
tmp->y = pf->snakehead->y + 1;
break;
case LEFT:
tmp->x = pf->snakehead->x - 2;
tmp->y = pf->snakehead->y;
break;
case RIGHT:
tmp->x = pf->snakehead->x + 2;
tmp->y = pf->snakehead->y;
break;
}
if (NextIsFood(pf,tmp))
{
EatFood(pf, tmp);//pf是蛇定义的节点,tmp是蛇走的下一个节点的指针
}
else
{
NoFood(pf, tmp);//pf是蛇定义的节点,tmp是蛇走的下一个节点的指针
}
Kill_By_Wall(pf);
Kill_By_Self(pf);
}
//游戏运行
void GameRun(ppsnake pf)
{
//打印帮助信息
PrintHelpInfo(pf);
do
{
Set_cur_pos(65, 10);
printf("总分数:%d,当前食物分数:%d", pf->score, pf->food_score);
if (key_val(VK_UP) && pf->direction != DOWN)
{
pf->direction = UP;
}
else if (key_val(VK_DOWN) && pf->direction != UP)
{
pf->direction = DOWN;
}
else if (key_val(VK_LEFT) && pf->direction != RIGHT)
{
pf->direction = LEFT;
}
else if (key_val(VK_RIGHT) && pf->direction != LEFT)
{
pf->direction = RIGHT;
}
//暂停
else if (key_val(VK_SPACE))
{
Pause();
}
//加速
else if (key_val(VK_F3))
{
if (pf->sleep_time > 80)
{
pf->sleep_time -= 30;
pf->score += 2;
}
}
//减速
else if (key_val(VK_F4))
{
if (pf->score>2)
{
pf->sleep_time += 30;
pf->score -= 2;
}
}
//退出
else if (key_val(VK_ESCAPE))
{
pf->state = EXIT;
}
SnakeMove(pf);
Sleep(200);
} while (pf->state == OK);
}
贪吃蛇的运行程序比较繁杂,接下来我们一起剖析一下。
运行程序开始后,要首先时时判别蛇的运行状态,当蛇的状态为OK时,则一直循环,然后是系统要接受键盘上传入的信息,是上、下、左、右,还是暂停或正常退出,当为上下左右时,则要对蛇身的节点进行依次改变,如果下一节点的坐标与食物的坐标相同,则要增加蛇身的长度,如果下一节点为 自己蛇身的节点或是墙体的节点,那么则会将蛇的状态置为异常,循环结束。
5.3游戏结束
当游戏结束后,对于建立的蛇身的节点要依次释放掉,并给玩家选择,是要退出游戏还是再来一局。
//游戏结束
void GameOver(ppsnake pf)
{
system("cls");
Set_cur_pos(45, 10);
switch (pf->state)
{
case EXIT:
printf("您主动退出,游戏结束");
break;
case KILL_BY_WALL:
printf("您撞到了墙,游戏结束");
break;
case KILL_BY_SELF:
printf("您撞到了自己,游戏结束");
break;
}
psnake cur = pf->snakehead;
while (cur)
{
pf->snakehead = pf->snakehead->next;
cur->x = 0;
cur->y = 0;
free(cur);
cur = pf->snakehead;
}
}
最后,附上贪吃蛇的主程序。
//游戏测试
void test()
{
char ch;
do
{
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
SnakeStart(&snake);
//运行游戏
GameRun(&snake);
//游戏结束(善后工作)
GameOver(&snake);
system("cls");
Set_cur_pos(43, 15);
printf("再玩一局(Y)");
Set_cur_pos(43, 16);
printf("退出游戏(N)");
ch = getchar();
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//设置设备本地化
setlocale(LC_ALL, "");
srand((unsigned)time(NULL));
test();
return 0;
}