在过去几个月,我陆续学习了简单的数据结构,为了能更好的知识学以致用,在此我们设计一款基于开发台的贪吃蛇小游戏,其基于Win32 API设计。贪吃蛇游戏的总体设计思路为 初始化游戏->进行游戏->结束游戏,接着我们就先从初始化游戏开始讲。
初始化游戏
作为一个贪吃蛇游戏,他应该包括进入的介绍,一些辅助提醒的话,初始化的地图,初始化的蛇身,以及初始化的一个食物。这些包含若干个函数,我便一个功能一个功能的进行介绍。
初始化游戏框图
首先,我们需要限制游戏框图大小,利用system函数就相当于在控制台里面进行命令,根据需要把它限制在了长100,宽30,这刚好符合我们的需求。并且给游戏框图取个名字,就叫他贪吃蛇。
接着,我们便开始设置界面,首先,我们要先获得标准输入流的句柄,创建了HANDLE类型的houtput,并从标准设备中获取句柄。
接着,我们利用GetConsoleCursorInfo函数去改变控制台屏幕缓冲区的光标大小和可见性。先创建一个对应类型的结构体,接着接受标准设备传过来的屏幕,然后将结构体中的bVisible改为0或者false,这个结构体内容对应的是光标的虚实,1为实,0为虚。如果改为false则需要添加stdbool头文件。接着将数据设置回去。
游戏启动函数
void GameStart(pSnake 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 = 0;
SetConsoleCursorInfo(houtput, &CursorInfo);
WelcomeToGame();
CreatMap();
InitSnack(ps);
Creat_Food(ps);
}
再接下来,便会打印第一级界面,运行WelcomeToGame函数,在这个函数中,又包含了一个SetPos函数,SetPos函数中包含一个结构体类型COORD,其声明仅包括两个short类型的数据,这个结构体用于储存光标想要防止的坐标。接着再定义一个HANDLE类型的数据Houtput,将其置为空指针,并在获得设备句柄以后将COORD类型的pos返回回去,这样,坐标就变到想要的位置了。
设置光标函数
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE Houtput = NULL;
Houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(Houtput, pos);
}
欢迎界面打印函数
void WelcomeToGame()
{
SetPos(40, 15);
printf("欢迎贪吃蛇小游戏!");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(25, 12);
printf("用↑.↓.←.→.来控制小蛇的移动\n");
SetPos(25, 13);
printf("加速分数更多哦\n");
SetPos(40, 25);
system("pause");
system("cls");
}
在 欢迎界面打印函数中,我们还在system函数中输入了pause和cls命令,其分别对应着暂停和清空屏幕,在玩家点击任意键后继续。在欢迎界面打印完成后,立刻创建地图并打印地图。在这个环节中我们需要引入宽字符,宽字符是除了常见的西文字符以外的字符,而且分不同地区。默认为c语言的字符,我们可以利用clocale来将定位定到中国,那么就可以使用中国的宽字符了。在setlocale中,第一个实参填要改变的数据,第二个实参填改变的地区,若填C则为C语言默认,若不填,它会按照你目前系统的国家来给你设置宽字符地区。
设置完成以后,我们便定义一下墙,食物和蛇身的样子,因为是宽字符,所以在定义时得加上L在前面。在具体的CreatMap函数中,我们打印出墙体。若要打印宽字符,需要用wprintf,并在打印的内容前加一个L。在控制台中,一个单位长1个单位,宽2单位,所以在打印地图的时候,想要一个正方形的地图,行只需要是列的一半即可。
宽字符设置相关
#include <clocale>
setlocale(LC_ALL, "");
创建地图函数
#define Wall L'□'
#define Food L'★'
#define Body L'●'
void CreatMap()
{
int i = 0;
SetPos(0, 0);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%c", Wall);
}
SetPos(0,26);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%c", Wall);
}
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%c", Wall);
}
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%c", Wall);
}
}
初始化蛇函数
在初始化蛇之前,我们先定义几个结构体和枚举类型,首先是枚举类型DIRC,它代表的是蛇的移动方向,总共有上升,下降,左转,右转。接着是枚举类型的游戏Game_Status,其包含游戏正常,蛇咬到自己了,蛇撞墙了,还有玩家正常。接着是蛇的身体,蛇的身体是一种链条,其包含x,y,next,x和y表示该节身体的位置,next用于连接下一节身体。再接下来是Snake,其包含的第一个是蛇首节点,接着是食物的位置,食物节点和蛇身公用一套结构体,只不过next一般置为空。接着是蛇的方向,然后是游戏的状态,接着是该局游戏的总分,然后是每个食物所占的分数,最后一个的蛇在移动间的睡眠时间,其代表了蛇的速度。
在真正的初始化中,主要是类似于打印墙,只不过需要先利用malloc创建好每个节点,然后利用头插法先把节点穿起来,形成链,接着打印出最初的5节身体。接着把最初的睡眠时间置为200ms,把最初的总分置为0,状态为OK,方向向右,每个石头的权重为10。
enum DIRC
{
Up = 1,
Down,
Left,
Right
};
enum Game_Status
{
OK,
Kill_By_Self,
Kill_By_Wall,
END_NOMAL
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* Next;
}SnakeNode,*pSnakeNode;
typedef struct Snake
{
pSnakeNode PSnake;
pSnakeNode PFood;
enum DIRC dir;
enum Game_Status status;
int Score;
int Food_weght;
int Sleep_time;
}Snake,*pSnake;
void InitSnack(pSnake ps)
{
pSnakeNode cur = NULL;
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc");
return;
}
cur->x = Pos_X + i * 2;
cur->y = Pos_Y;
if (ps->PSnake == NULL)
{
ps->PSnake = cur;
cur->Next = NULL;
}
else
{
cur->Next = ps->PSnake;
ps->PSnake = cur;
}
}
cur = ps->PSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", Body);
cur = cur->Next;
}
ps->Sleep_time = 200;
ps->Score = 0;
ps->status = OK;
ps->dir = Right;
ps->Food_weght = 10;
}
创建食物函数
在创建食物函数中,我们想要在地图内生成一个随机的食物节点,要求是位置不能和蛇的身体一样,还有必须在墙内。为了随机,我们利用rand函数,关于rand函数,可以在前文看到,因为x的范围是2到54,y的范围是1到25,rand返回的数据不定,因此,x和y做如下处理。而x不可以是2的倍数,利用do while循环实现。并在下文将蛇首节点赋值给cur,在while中不断判断该坐标是否与蛇身相同,若相同,则回到创建食物坐标的位置。当判断食物坐标位置正确后,利用malloc函数创建食物节点,把对应坐标赋给该节点,将该节点打印出来并将该节点赋给Snake中的PFood。
void Creat_Food(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->PSnake;
while (cur)
{
if (x == cur->x && y == cur->y)
{
goto again;
}
cur = cur->Next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood");
return;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos(pFood->x, pFood->y);
wprintf(L"%c", Food);
ps->PFood = pFood;
}
}
游戏运行
在游戏运行函数中,我们先提供一些辅助的信息给玩家,接着进入游戏的do——while循环,能够无限循环的条件便是游戏状态为OK。在每次循环的开始,将坐标设置在(64,10),并打印总得分与当前每个食物的权重。接着便是检查按键,检查按键调用了一个定义,该定义调用了GetAsyncKeyState函数,该函数会检查当前按下的按键是否是实参的按键,若是,则返回1,若否,则返回0。每一个if语句里面都必须保证按下了按键并且当前蛇不是朝着按键的反方向行进,当满足条件,那么就改变蛇的dir,如果按下的按键是空格,那么便进入pause函数,pause函数会暂停当前蛇的运动,只有当再次按下空格,蛇才会继续行动。如果按下的是esc键,那么会直接退出游戏,结束break,并且将状态改为END_NORMAL,如果分别按下的是ctrl或者alt键,则分别会加速或者减速,并且使得食物权重增加或者减少,当然,速度是有上限和下限的。当键盘的检测结束以后,就让蛇停止对应的时间,接着便是蛇的运动函数
游戏运行函数
#define Key_Press(VK) ((GetAsyncKeyState(VK) & 0x01)? 1:0)
void GameRun(pSnake ps)
{
PrintHelpInfo();
do
{
SetPos(64, 10);
printf("得分:%d", ps->Score);
printf("每食物得分:%2d", ps->Food_weght);
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_RIGHT) && ps->dir != Left)
{
ps->dir = Right;
}
else if (Key_Press(VK_LEFT) && ps->dir != Right)
{
ps->dir = Left;
}
else if (Key_Press(VK_SPACE))
{
pause();
}
else if (Key_Press(VK_ESCAPE))
{
ps->status = END_NOMAL;
break;
}
else if (Key_Press(VK_MENU))
{
if (ps->Sleep_time < 350)
{
ps->Sleep_time += 30;
ps->Food_weght -= 1;
}
}
else if (Key_Press(VK_CONTROL))
{
if (ps->Sleep_time > 50)
{
ps->Sleep_time -= 30;
ps->Food_weght += 1;
}
}
Sleep(ps->Sleep_time);
SnakeMove(ps);
} while (ps->status == OK);
}
游戏辅助信息函数
void PrintHelpInfo()
{
SetPos(64,15);
printf("别穿墙,别自咬");
SetPos(64, 16);
printf("用↑.↓.←.→.来控制小蛇的移动\n");
SetPos(64, 17);
printf("ctrl键为加速,alt键为减速");
SetPos(64, 18);
printf("ESC:退出游戏.space:暂停游戏");
SetPos(64, 20);
printf("阿煜制作!");
}
游戏暂停函数
void pause()
{
while (1)
{
Sleep(200);
if (Key_Press(VK_SPACE))
{
break;
}
}
}
蛇运动函数
首先先创建一个节点,这个节点的x,y取决于前面蛇的方向,根据不同的方向进行x和y对应的相加减。如果是左右移动,那么x的变化便是2格2格的变化,在这个过程中就已经把蛇首的下一个位置的坐标赋值给该节点。随后判断该节点是否为食物节点,若是食物节点,执行EatFood函数,若不是,执行No_food函数,然后判断蛇首有没有撞墙或者是撞到自己的现象。
void SnakeMove(pSnake ps)
{
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove:malloc");
return;
}
switch (ps->dir)
{
case Up:
{
pNextNode->x = ps->PSnake->x;
pNextNode->y = ps->PSnake->y - 1;
}
break;
case Down:
{
pNextNode->x = ps->PSnake->x;
pNextNode->y = ps->PSnake->y + 1;
}
break;
case Left:
{
pNextNode->x = ps->PSnake->x - 2;
pNextNode->y = ps->PSnake->y;
}
break;
case Right:
{
pNextNode->x = ps->PSnake->x + 2; //这里我们的列全部是 + 2的哦
pNextNode->y = ps->PSnake->y;
}
break;
}
if (Next_is_food(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
No_food(pNextNode, ps);
}
Kill_by_self(ps);
Kill_by_Wall(ps);
}
判断是否为食物相关函数
首先先判断是否为食物节点,若是,返回1,进入EatFood函数。不是则返回零,进入No_food函数。在EatFood函数之中,我们将食物节点和蛇函数传过去,使用头插法将食物头插到蛇的蛇首并将它变为新的蛇首。将蛇身重新打印出来,并且对应的在总分里面加当前的分数权重。接着重新生成一个食物。
如果前面不是食物,那么前面一部分还是一样,在打印蛇这一环节时,不打印最后一个节点,并在该节点处打印两个空格,防止出现拖影。在全部打印完成后,释放掉最后一个节点的地址,并将最后一个节点的next指针置为空。
int Next_is_food(pSnakeNode psn, pSnake ps)
{
return(psn->x == ps->PFood->x && psn->y == ps->PFood->y);
}
void EatFood(pSnakeNode psn, pSnake ps)
{
psn->Next = ps->PSnake;
ps->PSnake = psn;
pSnakeNode cur = ps->PSnake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", Body);
cur = cur->Next;
}
ps->Score += ps->Food_weght;
Creat_Food(ps);
}
void No_food(pSnakeNode psn, pSnake ps)
{
psn->Next = ps->PSnake;
ps->PSnake = psn;
pSnakeNode 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;
}
非自然死亡函数
非自然死亡包括撞墙和自杀,其代码本质基本差不多。撞墙是判断蛇首的坐标与墙的关系,自杀是通过遍历判断蛇首与身体的关系。在这之后,都要改变蛇的状态,将其变为Kill_By_Wall或者Kill_by_self。
int Kill_by_Wall(pSnake ps)
{
if (ps->PSnake->x == 56 || ps->PSnake->x == 0 || ps->PSnake->y == 0 || ps->PSnake->y == 26)
{
ps->status = Kill_By_Wall;
return 1;
}
else
return 0;
}
int Kill_by_self(pSnake ps)
{
pSnakeNode cur = ps->PSnake->Next;
while (cur)
{
if (ps->PSnake->x == cur->x && ps->PSnake->y == cur->y)
{
ps->status = Kill_By_Self;
return 1;
}
cur = cur->Next;
}
return 0;
}
游戏结束
在上一步跳出循环以后,进入游戏结束函数。刚开始先创建一个节点等于蛇的蛇首。接着根据游戏最后蛇的状态,打印出游戏的结束方式。接着一步一步的将蛇销毁掉。
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->PSnake;
SetPos(24, 12);
switch (ps->status)
{
case END_NOMAL:
printf("您主动退出游戏\n");
break;
case Kill_By_Self:
printf("您撞上自己了,游戏结束!\n");
break;
case Kill_By_Wall:
printf("您撞墙了,游戏结束!\n");
break;
}
while (cur)
{
pSnakeNode del = cur;
cur = cur->Next;
free(del);
}
}
文件主函数
在文件主函数中,我们使用了GAME函数。GAME函数先定义了一个int类型的ch,接着进行随机数的初始化,然后进入一个do-while循环,在这个循环中先后经历了开始-运行-结束函数,当游戏结束以后,询问是否在来一局,若玩家选择了y或Y,则重新开始游戏,若没有,则彻底结束游戏。
#include "game.h"
#include <clocale>
void GAME()
{
int ch = 0;
srand((unsigned int)time(NULL));
do
{
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("再来一局?(Y/N)");
ch = getchar();
getchar();
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
setlocale(LC_ALL, "");
GAME();
return 0;
}