0.前言:
贪吃蛇小游戏我们可以分为三个阶段来分别实现各个阶段的功能,第一个阶段为:游戏的开始,第二个阶段为:游戏的运行,第三个阶段为:游戏的结束。
1.游戏的开始
游戏的开始又分为7个步骤:
1.1设置游戏窗口的大小
游戏窗口我们用的是系统的终端,所以这里我用系统命令就可以实现设置窗口的大小。system函数里面的参数相当于你在终端里面命令所以要严格终端命令的语法
1.2设置窗口的名字
同理设置窗口的名字我们也用的是系统命令。
1.3隐藏屏幕光标
当我们要隐藏掉屏幕/终端上的光标时,这时我们要先获取到屏幕/终端,使用 GetStdHandle函数可以获取到屏幕/终端对象,然后用CONSOLE_CURSOR_INFO类型创建光标的结构体来保存光标的信息,最后用SetConsoleCursorInfo函数来重新设置光标的参数。如果不用SetConsoleCursorInfo函数来重新设置,只是改了光标结构体里面的值的话是不生效的。
//隐藏光标
void HideCursor() {
//获取到屏幕/终端
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//创建光标类型
CONSOLE_CURSOR_INFO cursor;
//获取光标信息
GetConsoleCursorInfo(houtput, &cursor);
//修改光标信息
cursor.bVisible = false;
//把新的光标信息重新设置
SetConsoleCursorInfo(houtput, &cursor);
}
1.4打印欢迎界面
如果没有设置光标的坐标,默认会在坐标(0,0)处,终端窗口的长代表x,宽代表y。
所以要想在中间打印出欢迎信息的话,需要先把光标的坐标信息设置成指定坐标位置,SetConsoleCursorPosition函数可以设置光标坐标信息,它的参数:
第一个我们之前见过了是获取屏幕的对象,第二个参数为存储光标坐标的结构体。
这里的结构体和前面的不一样,前面是存储光标信息的,这个是存储坐标信息的, 所以创建光标坐标的结构体,然后可以直接修改里面的x,y值,最后调用SetConsoleCursorPosition函数把修改好的光标结构体和屏幕对象传入就设置好了光标的坐标。
//修改光标的位置
void SetPos(int x, int y) {
//获取到屏幕/终端
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//创建光标坐标结构体
COORD pos = { x,y };
//设置光标坐标
SetConsoleCursorPosition(houtput, pos);
}
最终打印出来的效果为:
当然你打印出来的界面要想看的到话需要先让程序暂停,这里用到一个命令:
system("pause");
1.5创建地图
这里其实就和上面的打印欢迎界面一个思路,修改光标到你指定的地方然后打印你想要的符号信息。这里我就个效果展示了,要想要什么样的地图看你们的创意了。因为你也就会了指定地方的打印了。
可以看到我用的是一个一个的正方型来打印的地图,那么也可以用其他符号,但是我这里的符号是占二个宽度的,c语言里的符号没有,所以我们要改变一下环境,为本地模式。
//本地化
setlocale(LC_ALL, "");
打印宽符号需要使用wprintf函数:
#define WALL L'■'
#define BODY L'◆'
#define FOOD L'★'
wprintf(L"%lc", WALL);
wprintf(L"%lc", BODY);
wprintf(L"%lc", FOOD);
1.6初始化蛇身
首先我们创建一个蛇的结构体里面来管理游戏和蛇的各种状态
//移动状态
typedef enum Dir{
UP ,
DOWN ,
LEFT,
RIGHT
}Dir;
//游戏状态
typedef enum State {
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NORMAL,
}State;
//蛇身
typedef struct SnakeNode {
int x;
int y;
struct SnakeNode* next;
}SnakeNode,* pSnakeNode;
//蛇
typedef struct Snake {
//管理蛇的身体的结构体指针
pSnakeNode _pSnakeHead;
//管理食物的结构体指针
pSnakeNode _pFoodHead;
//管理蛇的方向的状态
Dir _Dir;
//管理游戏的状态
State _State;
//管理游戏的分数
int _Scores;
//管理吃一个食物获得的分数
int _FoodScores;
//管理蛇速度
int _SleepTime;
}Snake ,*pSnake;
我们使用的是链表来维护蛇的身体,使用可以先创建5个结点用一个头结点来串起来,蛇身体的结点用坐标数据和指向下一个结点指针,这里的坐标信息x需要为偶数 ,因为我们使用的符号是二个宽度的使用它打印在屏幕也需要二个宽度。
y坐标还是占一格,但是x坐标占了两格。这里我把蛇的起始点设为(24,5),初始化完后我们可以打印看一下效果
//打印蛇身
void PrintSnakeBody(pSnake ps) {
pSnakeNode tmp = ps->_pSnakeHead;
while (tmp) {
SetPos(tmp->x, tmp->y);
wprintf(L"%lc", BODY);
tmp = tmp->next;
}
}
//6.初始化蛇身
void InitSnake(pSnake ps) {
//创建蛇身
ps->_pSnakeHead = NULL;
for (int i = 0; i < 5; i++) {
int x = 24 + i * 2;
int y = 5;
pSnakeNode tmp = CreateNode(x, y);
//头插
tmp->next = ps->_pSnakeHead;
ps->_pSnakeHead = tmp;
}
//初始化蛇的各种状态
ps->_Dir = RIGHT;
ps->_State = OK;
ps->_Scores = 0;
ps->_FoodScores = FOODSCORES;
ps->_SleepTime = SLEEPTIME;
//打印蛇身
PrintSnakeBody(ps);
}
1.7创建食物
创建食物和创建蛇身都是一样的结构,蛇身是一个链表,而食物是一个所以,创建一个结点就可以维护食物了,结构体和蛇身的结构体一样,存储的是食物的x,y坐标信息,下一个结点的指针就让它指向NULL。
这里要注意一下的就是食物的坐标问题,食物要随机产生并且x坐标只能为偶数,和前面创建蛇身符号一个原因,也不能为蛇身和墙体的坐标,所以随机数产生的坐标要检验。创建完我们也打印看看
//检测食物坐标是否与蛇身坐标重合
int Decide(int x, int y, pSnake ps) {
pSnakeNode tmp = ps->_pSnakeHead;
while (tmp) {
if (tmp->x == x && tmp->y == y) {
return 0;
}
tmp = tmp->next;
}
return 1;
}
//创建食物
void CreateFood(pSnake ps) {
pSnakeNode tmp = CreateNode1();
while (1) {
//不能超出墙体 我的墙体长为29个 宽为27个
int x = rand() % 53 + 2;//2-54 0-52+2
int y = rand() % 25 + 1;//1-25 0-24+1
if (x % 2 == 0 && Decide(x, y,ps)) {
tmp->x = x;
tmp->y = y;
tmp->next = NULL;
ps->_pFoodHead = tmp;
break;
}
}
//打印食物
SetPos(tmp->x, tmp->y);
wprintf(L"%lc", FOOD);
}
2.游戏的运行
游戏的运行又分为4个步骤:
过程2.2到2.4循环,直到游戏状态不为ok
2.1打印帮助信息
打印信息和上面的打印地图和欢迎信息是一个样的,都是先修个到你要打印信息的地方,然后打印,这里我用的都是宽字符来打印的
//1.右侧打印帮助信息
void PrintHelpInfo() {
SetPos(62, 12);
wprintf(L"不能穿墙,不能咬到自己");
SetPos(62, 13);
wprintf(L"用↑.↓.←.→分别控制蛇的移动");
SetPos(62, 14);
wprintf(L"F3为加速,F4为减速");
SetPos(62, 15);
wprintf(L"ESC:退出游戏。空格:暂停游戏。");
}
2.2打印分数和每个食物分数
为什么不一块打印呢,主要是这里的分数不是死的,而是用结构体维护了的,因为不止是打印一遍,蛇每走一步就要刷新一下分数,所以单独创建一个函数来打印分数和每个食物分数
//2.打印当前已获得分数和每个食物的分数
void PrintScores(pSnake ps) {
SetPos(62, 7);
wprintf(L"得分:%2d", ps->_Scores);
SetPos(62, 8);
wprintf(L"每个食物得分:%2d", ps->_FoodScores);
}
2.3获取按键情况
如果检测按过某个键函数会返回一个16位的短整型数,这个数的第一位如果位1的话就说明这个键被按过,如果为0说明没被按过。所以我们可以写一个宏来封装获取按键的情况。
#define KEYSTROKE(key) GetAsyncKeyState(key) & 1 ? 1 : 0
接下来就是获取按键的情况来修改蛇的状态,还有游戏的状态。
//3.获取按键情况
void KEY_PRESS(pSnake ps) {
//向上移动,方向修改为上,此时的方向不能向下,不然就撞自己了。
if (KEYSTROKE(VK_UP) && ps->_Dir != DOWN) {
ps->_Dir = UP;
}
//向下移动,方向修改为下,此时的方向不能向上,不然就撞自己了。
else if (KEYSTROKE(VK_DOWN) && ps->_Dir != UP) {
ps->_Dir = DOWN;
}
//向左移动,方向修改为左,此时的方向不能向右,不然就撞自己了。
else if (KEYSTROKE(VK_LEFT) && ps->_Dir != RIGHT) {
ps->_Dir = LEFT;
}
//向右移动,方向修改为右,此时的方向不能向左,不然就撞自己了。
else if (KEYSTROKE(VK_RIGHT) && ps->_Dir != LEFT) {
ps->_Dir = RIGHT;
}
//按下F3修改休眠时间,如果休眠的时间修短,那么屏幕上显示的越快,效果就是速度变快了
else if (KEYSTROKE(VK_F3)) {
if (ps->_SleepTime > RULES) {
ps->_SleepTime -= TIME;
ps->_FoodScores += AWARDSCORES;
}
}
//按下F3修改休眠时间,如果休眠的时间修长,那么屏幕上显示的越慢,效果就是速度变慢了
else if (KEYSTROKE(VK_F4)) {
if (ps->_FoodScores > SLOWSCORES) {
ps->_SleepTime += TIME;
ps->_FoodScores -= SLOWSCORES;
}
}
//按下空格暂停
else if (KEYSTROKE(VK_SPACE)) {
SetPos(0, 28);
system("pause");
system("cls");
CreateMap();
PrintHelpInfo();
//打印食物
SetPos(ps->_pFoodHead->x, ps->_pFoodHead->y);
wprintf(L"%lc", FOOD);
}
//按下ESC退出
else if (KEYSTROKE(VK_ESCAPE)) {
ps->_State = END_NORMAL;
}
}
2.4根据按键来移动蛇
2.4又分为5个步骤来实现移动
2.4.1根据蛇头的坐标和方向计算下一个节点的坐标
首先先确定下一个节点的坐标然后在来判断是否为食物,如果蛇的状态方向为上:那么下一个节点坐标就应该为x坐标不变y坐标减1,为什么呢?你可以把蛇头看成一个节点中心,它要上移,那么上移后的坐标就是x坐标不变y坐标减1,这里的x,y是对于终端来讲的。所以同理其他方向的坐标也就出来了,我就不列举了。
//4.1根据蛇头的坐标和方向,计算下一个结点的坐标
void coor(int* x, int* y, pSnake ps) {
if (ps->_Dir == UP) {
*x = ps->_pSnakeHead->x;
*y = ps->_pSnakeHead->y - 1;
}
else if (ps->_Dir == DOWN) {
*x = ps->_pSnakeHead->x;
*y = ps->_pSnakeHead->y + 1;
}
else if (ps->_Dir == LEFT) {
*x = ps->_pSnakeHead->x - 2;
*y = ps->_pSnakeHead->y;
}
else if (ps->_Dir == RIGHT) {
*x = ps->_pSnakeHead->x + 2;
*y = ps->_pSnakeHead->y;
}
}
2.4.2判断下一个节点是否是食物
当有了下一个节点的坐标但是我们不知道它是否是食物的坐标还不是食物的坐标,所以这时候我们就应该判断一下,这里可以直接用食物坐标和节点坐标进行比较就行。
int NextlsFood(pSnakeNode pn,pSnake ps) {
return pn->x == ps->_pFoodHead->x && pn->y == ps->_pFoodHead->y;
}
2.4.3是食物就头插新节点,不是就删除尾部节点在头插新节点
如果是食物的话,那么我们就可以直接把这个新的节点头插到我们的蛇身的链表上,并且在重新生成一个食物节点。如果不是的话我们不久要插入这个新的节点,我们还要把原来的旧的尾节点删除,应该我们没吃食物,不能增长,所以要保持原来的5个节点,删除后还需要把尾节点处坐标的符号给清理,这里我们可以直接打印空白字符来覆盖原符号就行。
//4.3是食物,吃掉食物
void EatFood(pSnakeNode pn,pSnake ps) {
pn->next = ps->_pSnakeHead;
ps->_pSnakeHead = pn;
ps->_Scores += ps->_FoodScores;
//创建食物
CreateFood(ps);
}
//4.3不是食物
void NoFood(pSnakeNode pn, pSnake ps) {
pSnakeNode tmp = ps->_pSnakeHead;
while (tmp->next->next) {
tmp = tmp->next;
}
pSnakeNode del = tmp->next;
SetPos(del->x, del->y);
printf(" ");
tmp->next = NULL;
free(del);
del = NULL;
pn->next = ps->_pSnakeHead;
ps->_pSnakeHead = pn;
}
2.4.4判断是否撞墙
判断是否撞墙和判断食物一样,直接用新节点的坐标和墙节点比较即可。
//4.4判断是否撞墙
void KillByWall(pSnake ps) {
if ( ps->_pSnakeHead->y == 0
|| ps->_pSnakeHead->y == 26
|| ps->_pSnakeHead->x == 0
|| ps->_pSnakeHead->x == 56) {
ps->_State = KILL_BY_WALL;
}
}
2.4.5判断是否撞上自己
判断是否撞自己我们可以从头节点的下一个节点开始遍历,一个一个的和新节点坐标比较即可。
//4.5判断是否撞上自己
void KillBySelf(pSnake ps) {
pSnakeNode tmp = ps->_pSnakeHead->next;
while (tmp) {
if (tmp->x == ps->_pSnakeHead->x && tmp->y == ps->_pSnakeHead->y) {
ps->_State = KILL_BY_SELF;
break;
}
tmp = tmp->next;
}
}
3.游戏结束
游戏结束分为2个阶段:
3.1游戏结束原因
游戏结束我们可以直接判断游戏状态即可。
//胜利分数
#define VICTORY 1000
//游戏结束
void GameEnd(pSnake ps) {
system("cls");
SetPos(40, 15);
if (ps->_State == KILL_BY_WALL) {
wprintf(L"蛇撞到墙,结束游戏!!!");
}
else if (ps->_State == KILL_BY_SELF) {
wprintf(L"蛇撞到自己,结束游戏!!!");
}
else if (ps->_State == END_NORMAL) {
wprintf(L"正常退出游戏!!!");
}
else if (ps->_Scores == VICTORY) {
wprintf(L"恭喜通关!!!");
}
SetPos(40, 16);
wprintf(L"是否再来一局(Y/N)");
//释放节点
while (ps->_pSnakeHead) {
pSnakeNode del = ps->_pSnakeHead;
ps->_pSnakeHead = ps->_pSnakeHead->next;
free(del);
del = NULL;
}
free(ps->_pFoodHead);
ps->_pFoodHead = NULL;
}
3.2释放蛇身节点
释放节点一个一个遍历链表即可,但是释放前一个时要先记住下个节点。
while (ps->_pSnakeHead) {
pSnakeNode del = ps->_pSnakeHead;
ps->_pSnakeHead = ps->_pSnakeHead->next;
free(del);
del = NULL;
}
free(ps->_pFoodHead);
ps->_pFoodHead = NULL;
4.结束语:
为什么会用sleep函数来调节速度和显示蛇动样子呢?如果不用的话不程序不休眠,那么程序会直接运行到程序结束为止,那么你就看不到中间移动的过程,它只会给看到程序结束的样子,如果休眠的时间长,那么它显示出来的效果就是程序运行间隔就变长,那么展示出的效果是速度变慢了,反之速度就会加快,这就是全部了。最后运行的效果: