在开始游戏之前先介绍几个游戏中需要用到的Windows API函数:
BOOL SetConsoleCursorPosition(
HANDLE hConsoleOutput, // handle to screen buffer
COORD dwCursorPosition // new cursor coordinates
);
设置标准输出上光标的位置为pos
HANDLE GetStdHandle(
DWORD nStdHandle // input, output, or error device
);
获取标准输出的句句柄(⽤用来标识不不同设备的数值)
使⽤用范例
void SetPos(int x, int y)
{
COORD pos = { 0}; //用于存储坐标(x,y)的结构体
HANDLE hOutput = NULL;
pos.X = x;
pos.Y = y;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE); // 获取标准输出的句句柄 ( ⽤用来标识不不同设备的数值 )
SetConsoleCursorPosition(hOutput, pos); // 设置标准输出上光标的位置为 pos
}
GetAsyncKeyState():是一个⽤用来判断函数调⽤用时指定虚拟键的状态,确定用户当前是否按下了了键盘上的一个键的函数
首先蛇身用一个链表来实现,蛇身的每一个片段都是一个结点,当蛇吃到了食物就相当于给链表头插了一个结点
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
整个游戏的维护也用一个结构体来维护
enum Direction
{
UP=1,
DOWN,
LEFT,
RIGHT
};
enum GameStatus
{
OK,
NORMAL_END,
KILL_BY_WALL,
KILL_BY_SELF
};
typedef struct Snake
{
pSnakeNode _pSnake; //用于维护头结点
pSnakeNode _pFood; //维护食物
int _TotalScore; //游戏总得分
int _AddScore; //当前食物的增加分数
int _SleepTime; //游戏睡眠时间,用于调节蛇的移动速度
enum Direction _Dir; //蛇的方向状态
enum GameStatus _Status;//游戏的状态
}Snake,*pSnake;
整个游戏分为三个阶段:
GameStart();//游戏准备阶段
GameRun(); //玩游戏阶段
GameEnd(; //游戏结束
游戏准备阶段又可以分为以下几个阶段
void GameStart()
{
WelcomeToGame(); //欢迎界面的打印
CreateMap(); //地图的打印
InitSnake(ps); //蛇的初始化创建以及打印
CreateFood(ps); //食物的创建以及打印
}
由于游戏中需要频繁的对光标进行定位,所以我们需要使用上面所提到的函数
void SetPos(int x, int y)
{
COORD pos = {0};
HANDLE handle = NULL;
pos.X = x;
pos.Y = y;
handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
计算机屏幕的布局按以下方式进行划分,所以利用SetPos()可以将光标定位在指定位置,然后作相应的打印操作,这点在地图的打印中会明显体现
//欢迎界面的打印
void WelcomeToGame()
{
system("mode con cols=100 lines=30");//改变控制台窗口大小
SetPos(38, 14);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(40, 28);
system("pause");//需要分屏打印,按任意键打印下一页界面
system("cls");//清屏
SetPos(33, 14);
printf("使用↑、↓、←、→键来控制蛇的的移动\n");
SetPos(35, 15);
printf("F1可以加快蛇的移动速度,F2可减速\n");
SetPos(38, 16);
printf("提示:速度越快得分越高\n");
SetPos(40, 28);
system("pause");
system("cls");
}
墙体的打印是把墙分成上、下、左、右四部分分开打印,每一部分借助循环和SetPos()多次定位,在每一次定位时打印一个事先定义好的图案,作为墙体#define WALL "■"
void CreateMap()
{
int i = 0;
//上边界
for (i=0; i<=58; i+=2)
{
SetPos(i, 0);
printf(WALL);
}
//下边界
for (i=0; i<=58; i+=2)
{
SetPos(i, 29);
printf(WALL);
}
//左边界
for (i=1; i<=28; i++)
{
SetPos(0, i);
printf(WALL);
}
//右边界
for (i=1; i<=28; i++)
{
SetPos(58, i);
printf(WALL);
}
}
蛇在这里的创建是使用一个带头结点的链表来维护的,因为需要频繁创建结点,所以我们先定义一个函数来用于次操作
pSnakeNode BuyNode()
{
pSnakeNode NewNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (NULL == NewNode)
{
perror("BuyNode::malloc()");
exit(EXIT_FAILURE);
}
NewNode->x = 0;
NewNode->y = 0;
NewNode->next = NULL;
return NewNode;
}
蛇身的创建是创建出一个有五个结点的链表,第一个结点为头结点
实现定义了一个初始地址,用于定位蛇的最开始位置#define INIT_X 20
#define INIT_Y 10
void InitSnake(pSnake ps)
{
pSnakeNode first = BuyNode();
pSnakeNode cur = NULL;
int i = 0;
first->x = INIT_X;
first->y = INIT_Y;
for (i=0; i<4; i++)
{
cur = BuyNode();
cur->x = first->x + 2;
cur->y = first->y;
cur->next = first;
first = cur;
}
cur = first;
while (cur)
{
SetPos(cur->x, cur->y);
printf(FOOD);//初始化创建完后循环打印整条蛇
cur = cur->next;
}
printf("\n");
ps->_pSnake = first;//将整条蛇交给 ps->_pSnake 维护,即将头结点地址传过去
}
食物的打印也是通过创建结点的方式来实现的,在地图内部,借助rand()创建两个随机值用于食物的坐标,然后在指定位置创建结点并进行打印
void CreateFood(pSnake ps)
{
pSnakeNode FoodNode = BuyNode();
FoodNode->y = rand()%28 + 1;
do
{
FoodNode->x = rand()%55 + 2;
}while (FoodNode->x % 2 != 0 );
SetPos(FoodNode->x, FoodNode->y);
printf(FOOD);
ps->_pFood = FoodNode;//将食物传给ps维护
}
玩游戏的整体框架使用一个循环来完成,只要游戏的状态为OK则继续游戏,游戏状态发生变化,在GameEnd()中做相应提示,退出游戏
void GameRun(pSnake ps)
{
do
{
if (GetAsyncKeyState(VK_UP) && ps->_Dir != DOWN)
{
ps->_Dir = UP;
}
else if (GetAsyncKeyState(VK_DOWN) && ps->_Dir != UP)
{
ps->_Dir = DOWN;
}
else if (GetAsyncKeyState(VK_LEFT) && ps->_Dir != RIGHT)
{
ps->_Dir = LEFT;
}
else if (GetAsyncKeyState(VK_RIGHT) && ps->_Dir != LEFT)
{
ps->_Dir = RIGHT;
}
else if (GetAsyncKeyState(VK_SPACE))
{
Pause();
}
else if (GetAsyncKeyState(VK_ESCAPE))
{
ps->_Status = NORMAL_END;
}
else if (GetAsyncKeyState(VK_F1))
{
if (ps->_SleepTime >= 40)
{
ps->_AddScore += 2;
ps->_SleepTime -= 20;
}
}
else if (GetAsyncKeyState(VK_F2))
{
if (ps->_SleepTime <= 360 )
{
if (ps->_SleepTime <= 260)
{
ps->_AddScore -= 2;
ps->_SleepTime += 20;
}
else
{
ps->_AddScore = 1;
ps->_SleepTime += 20;
}
}
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
KillByWall(ps);
KillBySelf(ps);
}while (ps->_Status == OK);
}
循环中用GetAsyncKeyState()检测当前键盘按下的键
因为是蛇头走在最前面,用于改变方向,蛇不能立即反向,所以还要检测蛇的当前方向状态
检测到VK_UP且当前状态不为DOWN:ps->_Dir = UP
检测到VK_DOWN且当前状态不为UP:ps->_Dir = DOWN
检测到VK_LEFT且当前状态不为RIGHT:ps->_Dir = LEFT
检测到VK_RIGHT且当前状态不为LEFT:ps->_Dir = RIGHT
检测到VK_ESCAPE:ps->_Status = NORMAL_END
检测到VK_SPACE:游戏暂停(有一个死循环让游戏一直处于睡眠状态,当接下来再检测到VK_SPACE,跳出循环游戏继续)
void Pause()
{
while (1)
{
Sleep(100);
if (GetAsyncKeyState(VK_SPACE))
{
break;
}
}
}
检测到VK_F1:游戏加速(睡眠时间缩短,食物得分增加)
检测到VK_F2:游戏减速(睡眠时间增长,食物得分减少)
根据蛇当前的方向状态,做出不同的方向处理
我们把蛇每走一步都当作是一次链表的头插,如果下一个结点的位置刚好是食物,那我我们就新插入结点打印,即蛇身增长以截儿,如果下一个位置不是食物,依然打印新插入结点,但将蛇尾的结点先用空格打印后释放掉,相当于是蛇向前走了一步
void SnakeMove(pSnake ps)
{
HelpInfor(ps);
switch(ps->_Dir)
{
case UP:
{
pSnakeNode pNextNode = BuyNode();
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
if (IsFood(ps, pNextNode))
{
EatFood(ps, pNextNode);
}
else
{
EatSpace(ps, pNextNode);
}
}
break;
case DOWN:
{
pSnakeNode pNextNode = BuyNode();
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
if (IsFood(ps, pNextNode))
{
EatFood(ps, pNextNode);
}
else
{
EatSpace(ps, pNextNode);
}
}
break;
case LEFT:
{
pSnakeNode pNextNode = BuyNode();
pNextNode->x = ps->_pSnake->x - 2;
pNextNode->y = ps->_pSnake->y;
if (IsFood(ps, pNextNode))
{
EatFood(ps, pNextNode);
}
else
{
EatSpace(ps, pNextNode);
}
}
break;
case RIGHT:
{
pSnakeNode pNextNode = BuyNode();
pNextNode->x = ps->_pSnake->x + 2;
pNextNode->y = ps->_pSnake->y;
if (IsFood(ps, pNextNode))
{
EatFood(ps, pNextNode);
}
else
{
EatSpace(ps, pNextNode);
}
}
break;
default:
break;
}
}
void HelpInfor(pSnake ps)
{
SetPos(69,7);
printf("游戏的总得分为:%d ", ps->_TotalScore);
SetPos(66,8);
printf("当前游戏的食物得分为:%d ", ps->_AddScore);
SetPos(62,12);
printf("使用↑、↓、←、→键来控制蛇的的移动\n");
SetPos(64,13);
printf("F1可以加快蛇的移动速度,F2可减速\n");
SetPos(69,14);
printf("按下SPACE可以暂停游戏\n");
SetPos(70,15);
printf("按下ESC可以退出游戏\n");
}
int IsFood(pSnake ps, pSnakeNode pn)
{
return ((ps->_pFood->x == pn->x) && (ps->_pFood->y == pn->y));
}
void EatSpace(pSnake ps, pSnakeNode pn)
{
pSnakeNode cur = NULL;
pn->next = ps->_pSnake;
ps->_pSnake = pn;
cur = ps->_pSnake;
SetPos(cur->x, cur->y);
printf(FOOD);//打印新结点
//找到最后一个结点的位置
while (cur->next->next)
{
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");//用空格覆盖最后一个结点
free(cur->next);//释放最后一个结点
cur->next = NULL;
}
void EatFood(pSnake ps, pSnakeNode pn)
{
pSnakeNode cur = NULL;
pn->next = ps->_pSnake;
ps->_pSnake = pn;
cur = ps->_pSnake;
SetPos(cur->x, cur->y);
printf(FOOD);
cur = cur->next;
ps->_TotalScore += ps->_AddScore;
CreateFood(ps);//食物被吃后创建食物
}
检测蛇头是否与墙重合,若重合,改变游戏的当前状态
void KillByWall(pSnake ps)
{
if (ps->_pSnake->x == 0
|| ps->_pSnake->x == 58
|| ps->_pSnake->y == 0
|| ps->_pSnake->y == 29)
{
ps->_Status = KILL_BY_WALL;
}
}
检测蛇头是否与蛇身任意结点重合(遍历整个结点与蛇头结点比较),若重合,改变当前游戏状态
void KillBySelf(pSnake ps)
{
pSnakeNode cur = NULL;
cur = ps->_pSnake->next;
while (cur)
{
if ((ps->_pSnake->x == cur->x) && (ps->_pSnake->y == cur->y))
{
ps->_Status = KILL_BY_SELF;
}
cur = cur->next;
}
}
在本函数内先对游戏结束状态做出相应的提示
所有的结点(蛇身和食物)都是通过同台内存开辟出来的,所以在程序结束之前必须要对动态开辟出的内存进行释放,避免内存泄漏
void GameEnd(pSnake ps)
{
pSnakeNode del = NULL;
if (ps->_Status == NORMAL_END)
{
SetPos(23,14);
printf("退出游戏\n");
SetPos(70, 28);
return;
}
else if (ps->_Status == KILL_BY_WALL)
{
SetPos(23,14);
printf("很不幸!你撞墙而亡\n");
}
else if (ps->_Status == KILL_BY_SELF)
{
SetPos(23,14);
printf("很不幸!你追尾了\n");
}
SetPos(70, 28);
while (ps->_pSnake)
{
del = ps->_pSnake;
ps->_pSnake = ps->_pSnake->next;
free(del);
del = NULL;
}
ps->_pSnake = NULL;//ps->_pSnake置空,避免访问已经释放的堆空间
free(ps->_pFood);//释放未来得及吃的食物
ps->_pFood = NULL;
}