准备工作:打印得分信息
在进行GameStart之前,我们需要在地图的右侧打印帮助信息,以及目前玩家的得分情况和一个食物在当前速度下的得分情况(加速的状态下按比例增加食物的分数,减速的状态下则相反),至于打印的方法,在上两篇文章中已经介绍完毕,这里我们直接给出实现代码:
void GameIntroduction()
{
Set_Pos(65, 10);
wprintf(L"↑ ↓ ← →进行移动\n");
Set_Pos(65, 11);
wprintf(L"退出游戏请按Esc\n");
Set_Pos(65, 12);
wprintf(L"暂停请按空格\n");
Set_Pos(65, 13);
wprintf(L"小键盘1,2键加减速\n");
}
void ScoreStat(pSnake ps)
{
REPOSITION(63, 8);
wprintf(L"当前总分数%d", ps->_Socre);
REPOSITION(63, 7);
wprintf(L"当前速度一个食物分数为%d", ps->_foodWeight);
}
void GameRun(pSnake ps)
{
GameIntroduction();
do
{
ScoreStat(ps);
} while (ps->_Status == OK);
}
打印获得的成绩放在循环里面,则是因为每次玩家按完加减速之后,得分的权重都会改变,要实时更新。而我们的介绍信息则只需要打印一遍。
一,键位检测的实现
在此之前,我们要先定义一个宏:
cpp
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
这里我们只需要知道它能检测键位是否被按过即可,不做过多介绍,如果想更为细节的了解,可以参考官网的解释:getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learn
接下来我们用多个if-else语句来实现玩家按下不同键位时的检测:
cpp
void GameRun(pSnake ps)
{
GameIntroduction();
do
{
ScoreStat(ps);
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_NUMPAD1))
{
if (ps->_SleepTime > 100)
{
ps->_SleepTime -= 50;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_NUMPAD2))
{
if (ps->_SleepTime < 500)
{
ps->_SleepTime += 50;
ps->_foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status = END_NOMAL;
break;
}
} while (ps->_Status == OK);
}
这里当玩家按上键时调整蛇的方向为上,但如果此时方向朝下调整方向为上,就会使蛇自己咬住自己。左右方向也是同理。下面为实现的暂停函数(Pause):
cpp
void Pause()
{
while (!KEY_PRESS(VK_SPACE))
{
Sleep(200);
}
}
当玩家在此按下空格键时,我们结束暂停的状态。
当然,在每次检测完后,用Sleep函数暂停一下,至于暂停的时间,其实就是根据蛇的移动速度来设置:
cpp
Sleep(ps->_SleepTime);
二,蛇的移动函数的实现
2.1下一个位置节点的初始化
我们这里使用SnakeMove来命名我们的移动函数。由于我们的链表需要使用malloc开辟,所以我们需要用malloc来开辟蛇的下一个位置的节点,当然在开辟完成后,我们还需要检验开辟是否成功:
cpp
pSnakeNode NextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (NextNode == NULL)
{
perror("malloc():SnakeMove:NextNode");
return;
}
我们已经知道,控制台的长宽之比为1 :2,所以,如果接下来蛇往左移动,则他的x坐标则需要增加两个字符位置,向上向下则只需要移动y一个字符位置即可:
cpp
NextNode->x = ps->_pSnake->x;
NextNode->y = ps->_pSnake->y;
NextNode->next = NULL;
switch (ps->_Dir)
{
case RIGHT:
{
NextNode->x += 2;
NextNode->y += 0;
break;
}
case LEFT:
{
NextNode->x -= 2;
NextNode->y += 0;
break;
}
case UP:
{
NextNode->x += 0;
NextNode->y += 1;
break;
}
case DOWN:
{
NextNode->x += 0;
NextNode->y -= 1;
break;
}
}
2.2判断下一个位置是否为食物
接下来如果下一个位置为食物,或不为食物,我们也需要检测,首先我们设置一个函数NestIsFood来检验下一个位置是否为食物:
cpp
int NextIsFood(pSnakeNode psn, pSnake ps)
{
return(psn->x == ps->_pFood->x && psn->y == ps->_pFood->y);
}
直接返回值即可,如果下一个位置为食物,我们用EatFood函数将蛇的长度加一,否则我们使用NoFood来让蛇移动一格:
cpp
void EatFood(pSnake ps, pSnakeNode psn)
{
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
while (cur)
{
Set_Pos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_Socre += ps->_foodWeight;
free(ps->_pFood);
FoodInit(ps);
}
cpp
void NoFood(pSnake ps, pSnakeNode psn)
{
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
while (cur->next->next)
{
Set_Pos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
REPOSITION(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
这里我们说明一下NoFood函数中打印两个空格的原因,因为我们的蛇是通过打印移动的,所以当我们把NextNode节点接到蛇头上时,我们需要把最后一个节点删除,但仔细思考一下,上回打印蛇身的字符是否还会保留,这会使它遗留在屏幕上。所以我们要打印两个空格(因为我们的蛇身为宽字符占两个字节)将其从屏幕上抹除。
2.3判断蛇是否咬住自身或撞墙
这里我们用两个函数KillByWall,KillBySelf来分别表示蛇是否撞墙,蛇是否咬住自己。是否撞墙我们只需要判断蛇头位置是否与墙体位置重合,而是否咬住自己只需要判断蛇头位置是否与自身的其他部分重合:
cpp
int KillBySelf(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;
}
int KillByWall(pSnake ps)
{
if ((ps->_pSnake->x == 0) || (ps->_pSnake->x == 56) || (ps->_pSnake->y == 0) || (ps->_pSnake->y == 26))
{
ps->_Status = KILL_BY_WALL;
return 1;
}
return 0;
}
其实设置为void类型也可以,不过为了区分情况,方便起见,我用返回1或0来区分是否撞墙(咬自己)。
2.4GameRun部分代码
cpp
void GameRun(pSnake ps)
{
GameIntroduction();
do
{
ScoreStat(ps);
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_NUMPAD1))
{
if (ps->_SleepTime > 100)
{
ps->_SleepTime -= 50;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_NUMPAD2))
{
if (ps->_SleepTime < 500)
{
ps->_SleepTime += 50;
ps->_foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status = END_NOMAL;
break;
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
}
2.5SnakeMove部分代码
cpp
void SnakeMove(pSnake ps)
{
pSnakeNode NextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (NextNode == NULL)
{
perror("malloc():SnakeMove:NextNode");
return;
}
NextNode->x = ps->_pSnake->x;
NextNode->y = ps->_pSnake->y;
NextNode->next = NULL;
switch (ps->_Dir)
{
case RIGHT:
{
NextNode->x += 2;
NextNode->y += 0;
break;
}
case LEFT:
{
NextNode->x -= 2;
NextNode->y += 0;
break;
}
case UP:
{
NextNode->x += 0;
NextNode->y += 1;
break;
}
case DOWN:
{
NextNode->x += 0;
NextNode->y -= 1;
break;
}
}
if (NextIsFood(NextNode,ps))
{
EatFood(ps, NextNode);
}
else
{
NoFood(NextNode,ps);
}
KillByWall(ps);
KillBySelf(ps);
}
三,GameEnd部分
这一部分其实没有什么好说的,因为我们上面的代码已经将蛇的状态信息设置好了,所以我们这里只需要根据上面储存进去的信息来打印我们目前的游戏状态即可:
cpp
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
Set_Pos(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);
}
}
四,完整游戏代码
4.1Snake.h
cpp
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <windows.h>
#include <stdlib.h>
#include <locale.h>
#include <time.h>
#define WALL L'□'
#define FOOD L'★'
#define BODY L'●'
#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
typedef struct Snake
{
pSnakeNode _pSnake;//维护整条蛇的指针
pSnakeNode _pFood;//维护⻝物的指针
enum DIRECTION _Dir;//蛇头的⽅向,默认是向右
enum GAME_STATUS _Status;//游戏状态
int _Socre;//游戏当前获得分数
int _foodWeight;//默认每个⻝物10分
int _SleepTime;//每⾛⼀步休眠时间
}Snake, * pSnake;
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum GAME_STATUS
{
OK,//正常运⾏
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到⾃⼰
END_NOMAL//正常结束
};
void REPOSITION(short x, short y);//调整输入的光标位置
void WelcomeMenu();//设置欢迎与介绍菜单
void GameMap();//设置游戏地图
void GameIntroduction();//设置在游戏过程中的提醒
void GameStart(pSnake ps);//游戏的初始化
void SnakeInit(pSnake ps);//蛇身的初始化
void FoodInit(pSnake ps);//初始化食物
void GameRun(pSnake ps);//游戏主体运行部分
void ScoreStat(pSnake ps);//分数统计
void Pause();//空格暂停
void SnakeMove(pSnake ps);//控制蛇移动的函数
int NextIsFood(pSnakeNode psn, pSnake ps);//判断下一个位置是否为食物
void EatFood(pSnake ps, pSnakeNode psn);//是食物的情况下吃掉食物
void NoFood(pSnakeNode psn, pSnake ps);//非食物的情况下进行移动
int KillBySelf(pSnake ps);
int KillByWall(pSnake ps);
void GameEnd(pSnake ps);
4.2Snake.c
这里面的设置位置函数是REPOSITION(这是我第一遍做成功的代码,至于文章里面Set_Pos是我为了写文章又重新写了一遍代码)。
cpp
#include "snake.h"
void REPOSITION(short x, short y)
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput,pos);
}
void WelcomeMenu()
{
REPOSITION(35, 10);
wprintf(L"欢迎来到贪吃蛇小游戏");
REPOSITION(35, 20);
system("pause");
system("cls");
REPOSITION(35, 10);
wprintf(L"按 ↑ ↓ ← →键操控贪吃蛇移动\n");
REPOSITION(35, 11);
wprintf(L"按小键盘'1'或'2'键加速或减速\n");
REPOSITION(35, 12);
wprintf(L"Tips:加速吃食物有额外分数加成");
REPOSITION(35, 20);
system("pause");
system("cls");
}
void GameMap()
{
REPOSITION(0, 0);
int i = 0;
for (; i < 58; i += 2)
{
wprintf(L"%lc", WALL);
}
REPOSITION(0, 26);
for (i = 0; i < 58; i += 2)
{
wprintf(L"%lc", WALL);
}
for (i = 1; i < 26; i++)
{
REPOSITION(0, i);
wprintf(L"%lc", WALL);
}
for (i = 1; i < 26; i++)
{
REPOSITION(56, i);
wprintf(L"%lc", WALL);
}
}
void GameIntroduction()
{
REPOSITION(65, 10);
wprintf(L"↑ ↓ ← →进行移动\n");
REPOSITION(65, 11);
wprintf(L"退出游戏请按Esc\n");
REPOSITION(65, 12);
wprintf(L"暂停请按空格\n");
REPOSITION(65, 13);
wprintf(L"小键盘1,2键加减速\n");
}
void GameStart(pSnake ps)
{
srand((unsigned int)time(NULL));
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo = { 25,false };
SetConsoleCursorInfo(houtput, &CursorInfo);
WelcomeMenu();
GameMap();
SnakeInit(ps);
FoodInit(ps);
}
void SnakeInit(pSnake ps)
{
int i = 0;
pSnakeNode cur = NULL;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("SnakeInit():malloc()cur:");
return;
}
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
if (ps->_pSnake == NULL)
{
ps->_pSnake = cur;
}
else
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
cur = ps->_pSnake;
while (cur)
{
REPOSITION(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_Dir = RIGHT;
ps->_foodWeight = 10;
ps->_SleepTime = 300;
ps->_Socre = 0;
ps->_Status = OK;
}
void FoodInit(pSnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand()%53 + 2;
y = rand()%24 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;
while (cur)
{
if (cur->x == x && cur->y == y)
goto again;
cur = cur->next;
}
pSnakeNode cur2 = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur2 == NULL)
{
perror("malloc():FoofInit:cur2:");
return;
}
cur2->next = NULL;
cur2->x = x;
cur2->y = y;
REPOSITION(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood = cur2;
cur2 = NULL;
}
void ScoreStat(pSnake ps)
{
REPOSITION(63, 8);
wprintf(L"当前总分数%d", ps->_Socre);
REPOSITION(63, 7);
wprintf(L"当前速度一个食物分数为%d", ps->_foodWeight);
}
void Pause()
{
while (!KEY_PRESS(VK_SPACE))
{
Sleep(200);
}
}
int NextIsFood(pSnakeNode psn, pSnake ps)
{
return(psn->x == ps->_pFood->x && psn->y == ps->_pFood->y);
}
void EatFood(pSnake ps,pSnakeNode psn)
{
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
while (cur)
{
REPOSITION(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_Socre += ps->_foodWeight;
free(ps->_pFood);
FoodInit(ps);
}
void NoFood(pSnakeNode psn,pSnake ps)
{
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
while (cur->next->next)
{
REPOSITION(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
REPOSITION(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
}
int KillByWall(pSnake ps)
{
if ((ps->_pSnake->x == 0)|| (ps->_pSnake->x == 56)|| (ps->_pSnake->y == 0)|| (ps->_pSnake->y == 26))
{
ps->_Status = KILL_BY_WALL;
return 1;
}
return 0;
}
int KillBySelf(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 SnakeMove(pSnake ps)
{
pSnakeNode NextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (NextNode == NULL)
{
perror("malloc():SnakeMove:NextNode");
return;
}
NextNode->x = ps->_pSnake->x;
NextNode->y = ps->_pSnake->y;
NextNode->next = NULL;
switch (ps->_Dir)
{
case RIGHT:
{
NextNode->x += 2;
NextNode->y += 0;
break;
}
case LEFT:
{
NextNode->x -= 2;
NextNode->y += 0;
break;
}
case UP:
{
NextNode->x += 0;
NextNode->y += 1;
break;
}
case DOWN:
{
NextNode->x += 0;
NextNode->y -= 1;
break;
}
}
if (NextIsFood(NextNode,ps))
{
EatFood(ps, NextNode);
}
else
{
NoFood(NextNode,ps);
}
KillByWall(ps);
KillBySelf(ps);
}
void GameRun(pSnake ps)
{
GameIntroduction();
do
{
ScoreStat(ps);
if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
{
ps->_Dir = DOWN;
}
else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
{
ps->_Dir = UP;
}
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_NUMPAD1))
{
if (ps->_SleepTime > 100)
{
ps->_SleepTime -= 50;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_NUMPAD2))
{
if (ps->_SleepTime < 500)
{
ps->_SleepTime += 50;
ps->_foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_SPACE))
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status = END_NOMAL;
break;
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
}
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
REPOSITION(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);
}
}
4.3test.c
cpp
#include "snake.h"
void test()
{
int ch = 0;
do
{
Snake snake = { 0 };
GameStart(&snake);//游戏初始化
GameRun(&snake);//游戏运行
GameEnd(&snake);//游戏的善后处理
REPOSITION(20, 15);
printf("再来一局吗?(y/n):");
ch = getchar();
getchar();
} while (ch == 'y');
system("cls");
}
int main()
{
setlocale(LC_ALL, "");
test();
return 0;
}
贪吃蛇的内容到这里就完结了,至于下一次更新要到7月10号左右了(一是过暑假了想摸会鱼,二是在c语言准备开数据结构的新坑),我们下篇文章见。