文章目录
贪吃蛇
贪吃蛇是一种经典的游戏,玩家操纵一条蛇在一个有边界的区域内移动,通过吃食物来增长长度。
知识储备
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
整体思路
•将游戏拆分成开始、运行和结束三个阶段
•游戏的开始阶段:我们要初始化界面、地图、食物和蛇
•游戏的运行阶段:实现蛇的运动、吃食物、撞墙、吃到自己以及暂停、退出等功能
•游戏的结束阶段:需要根据不同的结束状态给出结束标志,实现循环游玩。
•大体框架如下:
void test()
{
GameStart();
GameRun();
GameEnd();
}
GameStart
•在开始实现各种函数之前,我们需要创建一些全局变量便于函数定义:
enum Direction
{
UP,
DOWN,
RIGHT,
LEFT
};
enum Snake_State
{
OK,
END_NORMAL,
SELFKILL,
KILL_WALL,
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake
{
pSnakeNode _phead;
pSnakeNode _pfood;
int _scroce;
int _foodweight;
enum Direction _direction;
enum Snake_State _state;
int _SleepTime;
int MAX;
}Snake,*pSnake;
•上述声明中,snake作为对整个游戏管理的结构体,存储蛇头的坐标、当前得分、蛇头的方向、食物的坐标、食物对应的分数、蛇的状态、蛇运动间歇以及历史最高分
•注意到蛇身我们选择使用单链表构建节点,因为蛇身本身物理结构就与单链表相似,这样打印蛇身事半功倍。
•要打印开始界面,则需要控制光标的位置,否则每次打印都需要大量的换行符和空格,非常费力,接下来我们就实现控制光标位置。
控制光标位置
•要实现控制光标位置的相关功能则需要了解Win32 API的相关知识,在Windows.h下的一系列库函数实现了相关功能。
如:
HANDLE GetStdHandle(DWORD nStdHandle);该函数能获得特定设备的句柄。
BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);能获得句柄对应光标的相关信息。
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);设置光标信息。
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD pos);设置光标位置。
•具体实现如下:
void SetPos(short x, short y)//设置光标位置
{
COORD pos = { x,y };
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
void GameStart()
{
HANDLE handle = NULL;
handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取显示屏的句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);//获取光标信息
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);//隐藏光标
}
打印初始化界面和地图
•打印初始化界面需要用到上述设置光标位置的函数,并且打印完后程序暂停运行,就需要用到指令pause,调用函数system();
void PrintStart()
{
SetPos(60, 15);
printf("欢迎来到贪吃蛇小游戏!");
SetPos(62, 30);
system("pause");
SetPos(50, 15);
printf("用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
SetPos(60, 16);
printf("加速能获得更高的分数哦!");
SetPos(62, 30);
system("pause");
}
•接下来要打印地图,我们选择用宽字符’□’作为墙体,这样视觉效果更好,既然墙体是宽字符,那么蛇身和食物自然也需要是宽字符。这时我们可以宏定义墙体和蛇身:
#define SNAKENODE L'●'//L是宽字符的声明
#define FOOD L'★'
#define WALL L'□'
仅仅如此还不够,要实现宽字符的打印需要使用setlocale函数,让编译器的C语言功能适应本地环境。
int main()
{
setlocale(LC_ALL, "");
test();
return 0;
}
初始化地图:
void CreaMap()
{
system("cls");
SetPos(0, 0);
int i = 0;
for (; i <= 57; i += 2)
wprintf(L"%c", WALL);
SetPos(0, 26);
for (i=0; i <= 57; 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);
}
}
初始化蛇
•往后的实现中肯定需要大量的链表节点,可以先实现节点的创建函数:
pSnakeNode BuyNode()
{
pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
if (!node)
{
perror("buynode mollc");
return;
}
return node;
}
•创建蛇身就使用链表头插即可。
void CreatSanke(pSnake ps)
{
for (int i = 0; i < 5; i++)
{
pSnakeNode node = BuyNode();
node->next = NULL;
node->x = 10 + 2 * i;
node->y = 5;
if (ps->_phead == NULL)
{
ps->_phead = node;
SetPos(node->x, node->y);
wprintf(L"%c", SNAKENODE);
}
else
{
node->next = ps->_phead;
ps->_phead = node;
SetPos(node->x, node->y);
if (i == 4)
wprintf(L"%c", L'▲');//区分蛇头和蛇身
else
wprintf(L"%c", SNAKENODE);
}
ps->_direction = RIGHT;
ps->_scroce = 0;
ps->_foodweight = 10;
ps->_SleepTime = 200;
ps->_state = OK;
}
}
初始化食物
•食物的生成位置是随机的,这又需要用到我们的老熟人rand()函数。当然,需要先调用srand()函数并且,设置time(NULL)为种子。
•此外,还需注意食物的位置在地图内,并且不能与蛇身重合。
void CreatFood(pSnake ps)
{
pSnakeNode food = BuyNode();
food->next = NULL;
again:
while (1)
{
food->x = rand() % 53 + 2;
food->y = rand() % 25 + 1;
if (food->x % 2 == 0)
break;
}
pSnakeNode pcup= ps->_phead;
while (pcup)
{
if (pcup->x == food->x && pcup->y == food->y)
goto again;
pcup = pcup->next;
}
ps->_pfood = food;
SetPos(food->x,food->y);
wprintf(L"%c", FOOD);
}
那么GameStart函数就完全实现了!
void GameStart(pSnake ps)
{
srand(time(NULL));
HANDLE handle = NULL;
handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取显示屏的句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);//获取光标信息
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);//隐藏光标
//打印欢迎界面
PrintStart();
//打印地图
CreaMap();
//初始化蛇
CreatSanke(ps);
//生成第一个食物
CreatFood(ps);
}
GameRun
•蛇的运行实现的功能不多,主要包括打印一些得分信息,蛇的运动和吃掉食物、死亡以及退出等功能。
打印静态信息
void PrintHelp(pSnake ps)
{
SetPos(0, 27);
printf("操作方式:用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
SetPos(10, 28);
printf("Space:暂停,Esc:退出");
SetPos(5, 29);
printf("游戏开始时间:%s %s",__TIME__, __DATE__);
SetPos(5, 30);
printf("历史最高得分:%d", ps->MAX);
}
打印动态信息
SetPos(64, 10);
printf("当前得分:%d", ps->_scroce);
SetPos(64, 11);
printf("当前食物的分数:%d", ps->_foodweight);
蛇身移动的实现
•在贪吃蛇游戏中,蛇的运动方向是通过控制←、↑、→、↓实现的。那么要实现相关功能,我们就需要一个函数能够获取键盘的按键情况,还是WIN32 API的函数。
•SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
•由此我们就可以实现蛇身的移动。
如下:
do
{
if (KEY_PRESS(VK_UP)&&ps->_direction!=DOWN)
ps->_direction = UP;
else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
ps->_direction = DOWN;
else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
ps->_direction = RIGHT;
else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
ps->_direction = LEFT;
else if (KEY_PRESS(VK_SPACE))
{
do
{
Sleep(10);
} while (!KEY_PRESS(VK_SPACE));
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_state = END_NORMAL;
break;
}
else if (KEY_PRESS(VK_F3) && ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_foodweight += 2;
}
else if (KEY_PRESS(VK_F4) && ps->_foodweight>0)
{
ps->_SleepTime += 30;
ps->_foodweight -= 2;
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_state == OK);
GameEnd
•游戏结束部分,我们要实现结束语、销毁节点,保留历史最高分。
结束界面
void GameEnd(pSnake ps)
{
SetPos(24, 12);
if (ps->_state == END_NORMAL)
printf("你正常退出游戏!");
else if (ps->_state == KILL_WALL)
printf("你撞墙了!");
else if (ps->_state == SELFKILL)
printf("你自杀了!");
pSnakeNode pcur = ps->_phead;
while (pcur)
{
pSnakeNode del = pcur;
pcur = del->next;
free(del);
}
}
保留历史最高分
•要实现保留历史最高分,那肯定要调用外存,所以需要一定文件操作函数。
void GetMax(pSnake ps)//一开始要载入最高得分
{
FILE* pf = fopen("data.txt", "rb");
if (!pf)
{
FILE* pf = fopen("data.txt", "wb+");
}
if (!pf)
{
perror("fopen");
return 1;
}
int i;
fread(&i, sizeof(int), 1, pf);
ps->MAX = i;
if (i < 0)
ps->MAX = 0;
fclose(pf);
pf = NULL;
}
void CopyMax(pSnake ps)//记录最高得分
{
FILE* pf = fopen("data.txt", "wb");
if (!pf)
{
perror("fopen");
return 1;
}
fwrite(&(ps->_scroce), sizeof(int), 1, pf);
fclose(pf);
pf = NULL;
}
整体代码
Snake.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>
#define SNAKENODE L'●'
#define FOOD L'★'
#define WALL L'□'
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
enum Direction
{
UP,
DOWN,
RIGHT,
LEFT
};
enum Snake_State
{
OK,
END_NORMAL,
SELFKILL,
KILL_WALL,
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake
{
pSnakeNode _phead;
pSnakeNode _pfood;
int _scroce;
int _foodweight;
enum Direction _direction;
enum Snake_State _state;
int _SleepTime;
int MAX;
}Snake,*pSnake;
void SetPos(short x, short y);
void PrintStart();
void CreaMap();
void CreatSanke(pSnake ps);
void CreatFood(ps);
void PrintHelp(pSnake ps);
void SnakeMove(pSnake ps);
void EatFood(pSnake ps, pSnakeNode pcup);
void NoFood(pSnake ps, pSnakeNode pcup);
void KillByWall(pSnake ps);
void KillBySelf(pSnake ps);
void GetMax(pSnake ps);
void CopyMax(pSnake ps);
Snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Snake.h"
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
void PrintStart()
{
SetPos(60, 15);
printf("欢迎来到贪吃蛇小游戏!");
SetPos(62, 30);
system("pause");
SetPos(50, 15);
printf("用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
SetPos(60, 16);
printf("加速能获得更高的分数哦!");
SetPos(62, 30);
system("pause");
}
void CreaMap()
{
system("cls");
SetPos(0, 0);
int i = 0;
for (; i <= 57; i += 2)
wprintf(L"%c", WALL);
SetPos(0, 26);
for (i=0; i <= 57; 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);
}
}
pSnakeNode BuyNode()
{
pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
if (!node)
{
perror("buynode mollc");
return;
}
return node;
}
void CreatSanke(pSnake ps)
{
for (int i = 0; i < 5; i++)
{
pSnakeNode node = BuyNode();
node->next = NULL;
node->x = 10 + 2 * i;
node->y = 5;
if (ps->_phead == NULL)
{
ps->_phead = node;
SetPos(node->x, node->y);
wprintf(L"%c", SNAKENODE);
}
else
{
node->next = ps->_phead;
ps->_phead = node;
SetPos(node->x, node->y);
if (i == 4)
wprintf(L"%c", L'▲');
else
wprintf(L"%c", SNAKENODE);
}
ps->_direction = RIGHT;
ps->_scroce = 0;
ps->_foodweight = 10;
ps->_SleepTime = 200;
ps->_state = OK;
}
}
void CreatFood(pSnake ps)
{
pSnakeNode food = BuyNode();
food->next = NULL;
again:
while (1)
{
food->x = rand() % 53 + 2;
food->y = rand() % 25 + 1;
if (food->x % 2 == 0)
break;
}
pSnakeNode pcup= ps->_phead;
while (pcup)
{
if (pcup->x == food->x && pcup->y == food->y)
goto again;
pcup = pcup->next;
}
ps->_pfood = food;
SetPos(food->x,food->y);
wprintf(L"%c", FOOD);
}
void PrintHelp(pSnake ps)
{
SetPos(0, 27);
printf("操作方式:用↑、↓、←、→分别控制蛇的移动,F3为加速,F4为减速\n");
SetPos(10, 28);
printf("Space:暂停,Esc:退出");
SetPos(5, 29);
printf("游戏开始时间:%s %s",__TIME__, __DATE__);
SetPos(5, 30);
printf("历史最高得分:%d", ps->MAX);
}
void EatFood(pSnake ps, pSnakeNode pcup)
{
pcup->next = ps->_phead;
ps->_phead = pcup;
pcup = ps->_phead;
int i = 1;
while (pcup->next)
{
if (i)
{
SetPos(pcup->x, pcup->y);
wprintf(L"%c", L'▲');
i = 0;
}
else
{
SetPos(pcup->x, pcup->y);
wprintf(L"%c", SNAKENODE);
}
pcup = pcup->next;
}
ps->_scroce += ps->_foodweight;
CreatFood(ps);
}
void NoFood(pSnake ps, pSnakeNode pcup)
{
pcup->next = ps->_phead;
ps->_phead = pcup;
pcup = ps->_phead;
int i = 1;
while (pcup->next->next)
{
if (i)
{
SetPos(pcup->x, pcup->y);
wprintf(L"%c", L'▲');
i = 0;
}
else
{
SetPos(pcup->x, pcup->y);
wprintf(L"%c", SNAKENODE);
}
pcup = pcup->next;
}
SetPos(pcup->next->x, pcup->next->y);
printf(" ");
free(pcup->next);
pcup->next = NULL;
}
void KillByWall(pSnake ps)
{
if (ps->_phead->x == 0 ||
ps->_phead->x == 56 ||
ps->_phead->y == 0 ||
ps->_phead->y == 26)
{
ps->_state = KILL_WALL;
return 1;
}
return 0;
}
void KillBySelf(pSnake ps)
{
pSnakeNode pcup = ps->_phead;
pSnakeNode pnext = pcup->next;
while (pnext->next)
{
if (pcup->x == pnext->x && pcup->y == pnext->y)
{
ps->_state = SELFKILL;
return;
}
pnext = pnext->next;
}
}
void SnakeMove(pSnake ps)
{
pSnakeNode pcup = BuyNode();
if (ps->_direction == RIGHT)
{
pcup->x = ps->_phead->x + 2;
pcup->y = ps->_phead->y;
}
else if (ps->_direction == LEFT)
{
pcup->x = ps->_phead->x - 2;
pcup->y = ps->_phead->y;
}
else if (ps->_direction == UP)
{
pcup->x = ps->_phead->x;
pcup->y = ps->_phead->y - 1;
}
else if (ps->_direction == DOWN)
{
pcup->x = ps->_phead->x;
pcup->y = ps->_phead->y + 1;
}
if(pcup->x==ps->_pfood->x&&pcup->y==ps->_pfood->y)
EatFood(ps,pcup);
else
NoFood(ps,pcup);
KillByWall(ps);
KillBySelf(ps);
}
void GetMax(pSnake ps)
{
FILE* pf = fopen("data.txt", "rb");
if (!pf)
{
FILE* pf = fopen("data.txt", "wb+");
}
if (!pf)
{
perror("fopen");
return 1;
}
int i;
fread(&i, sizeof(int), 1, pf);
ps->MAX = i;
if (i < 0)
ps->MAX = 0;
fclose(pf);
pf = NULL;
}
void CopyMax(pSnake ps)
{
FILE* pf = fopen("data.txt", "wb");
if (!pf)
{
perror("fopen");
return 1;
}
fwrite(&(ps->_scroce), sizeof(int), 1, pf);
fclose(pf);
pf = NULL;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Snake.h"
void GameStart(pSnake ps)
{
srand(time(NULL));
system("mode con cols=150 lines=40");
system("title 贪吃蛇");
HANDLE handle = NULL;
handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取显示屏的句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);//获取光标信息
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);//隐藏光标
//获取历史最高得分;
GetMax(ps);
//打印欢迎界面
PrintStart();
//打印地图
CreaMap();
//初始化蛇
CreatSanke(ps);
//生成第一个食物
CreatFood(ps);
}
void GameRun(pSnake ps)
{
//打印静态信息
PrintHelp(ps);
do
{
//打印动态信息
SetPos(64, 10);
printf("当前得分:%d", ps->_scroce);
SetPos(64, 11);
printf("当前食物的分数:%d", ps->_foodweight);
if (KEY_PRESS(VK_UP)&&ps->_direction!=DOWN)
ps->_direction = UP;
else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
ps->_direction = DOWN;
else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
ps->_direction = RIGHT;
else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
ps->_direction = LEFT;
else if (KEY_PRESS(VK_SPACE))
{
do
{
Sleep(10);
} while (!KEY_PRESS(VK_SPACE));
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_state = END_NORMAL;
break;
}
else if (KEY_PRESS(VK_F3) && ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_foodweight += 2;
}
else if (KEY_PRESS(VK_F4) && ps->_foodweight>0)
{
ps->_SleepTime += 30;
ps->_foodweight -= 2;
}
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_state == OK);
}
void GameEnd(pSnake ps)
{
//保留最大得分
if(ps->_scroce>ps->MAX)
CopyMax(ps);
SetPos(24, 12);
if (ps->_state == END_NORMAL)
printf("你正常退出游戏!");
else if (ps->_state == KILL_WALL)
printf("你撞墙了!");
else if (ps->_state == SELFKILL)
printf("你自杀了!");
pSnakeNode pcur = ps->_phead;
while (pcur)
{
pSnakeNode del = pcur;
pcur = del->next;
free(del);
}
}
void test()
{
int ch = 0;
do{
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);
printf("再来?局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'Y' || ch == 'y');
system("cls");
SetPos(60, 20);
printf("汗流浃背了吧,老弟!");
while (1)
Sleep(100);
}
int main()
{
setlocale(LC_ALL, "");
test();
return 0;
}
That`s all!