引言:本篇博客中,我将会使用结构体,链表,WIN32 API等一系列知识完成C语言项目——贪吃蛇的实现。在观看此篇博客之前,请将这些知识所熟悉,不然可能会造成理解困难。
更多有关C语言的知识详解可前往个人主页:计信猫
目录
一,贪吃蛇项目的准备
1,三文件操作
2,贪吃蛇有关结构体的定义
3,改变窗口信息与隐藏光标
4,确定光标位置函数
二,游戏开始(GameStart)
1,欢迎界面的设计
2,游戏地图的创建
3,蛇的初始化
4,创建食物
编辑
三,游戏运行(GameRun)
1,打印游戏帮助信息
编辑2,游戏得分的打印与蛇的移动方向的判定
3,蛇的移动(SnakeMove)
4,检测下一个坐标是否为食物
Ⅰ,EatFood
Ⅱ,NoFood
5,判断蛇是否撞墙或者撞到自己
Ⅰ,KillByWall
Ⅱ,KillBySelf
四,游戏结束(GameEnd)
1,根据蛇的状态打印结束语
2,释放蛇的空间
五,游戏的优化设置
六,代码参考
1,test.c
2, snake.c
3, snake.h
一,贪吃蛇项目的准备
1,三文件操作
在贪吃蛇项目中,我们会创建三个文件分别为:test.c,snake.c,snake.h。它们的作用分别为游戏函数的测试,游戏函数的实现,游戏函数的声明和结构体变量的定义。
2,贪吃蛇有关结构体的定义
在snake.h中,我们将会定义与贪吃蛇有关的结构体。
1,贪吃蛇身体的节点:
//定义蛇身体的节点
typedef struct snakenode
{
//坐标
int x;
int y;
struct snakenode* next;//下一个节点
}snakenode,*psnakenode;
2,蛇的方向:
//定义方向
enum direction
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
3,蛇的状态:
//蛇的状态
enum game_state
{
OK,//正常状态
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到了自己
END_NORMAL//正常结束游戏
};
4,贪吃蛇游戏:
//贪吃蛇
typedef struct snake
{
psnakenode _psnake;//蛇头节点
psnakenode _pfood;//食物节点
enum direction _dir;//蛇的方向
enum game_state _sta;//游戏状态
int _food_weight;//一个食物的分数
int _score;//总成绩
int _sleep_time;//蛇的速度
}snake,* psnake;
3,改变窗口信息与隐藏光标
在snake.c中,我们定义一个GameStart函数来包含开始游戏之前的窗口改变,光标隐藏,欢迎界面与地图打印函数。
在前文WIN32 API中我们已经对窗口信息修改和隐藏光标的函数进行过解释,所以我们这里直接使用:
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 = false;//隐藏鼠标
SetConsoleCursorInfo(houtput, &cursorinfo);//设置光标状态
}
4,确定光标位置函数
在欢迎界面与游戏界面中,我们需要将游戏开始信息与游戏信息等打印在指定的位置,这时候我们就需要有特定的函数来确定光标的位置,以此来完成指定位置信息的打印。所以我们如下设计确定光标位置setpos()函数。
void setpos(int x,int y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出句柄
COORD pos = { x,y };//设定坐标
SetConsoleCursorPosition(houtput, pos);//设定光标的坐标
}
二,游戏开始(GameStart)
1,欢迎界面的设计
如图中的界面就是我们想要达到的欢迎界面效果,所以我们在GameStart()函数中设计一个新函数名为welcometogame()函数,函数内容如下:
void welcometogame()
{
setpos(40, 14);
wprintf(L"欢迎来到贪吃蛇游戏\n");
setpos(42, 20);
system("pause");
system("cls");
setpos(25, 14);
wprintf(L"使用 ↑ . ↓ . ← . → 来进行方向移动,f3加速,f4减速\n");
setpos(25, 15);
wprintf(L"加速吃到食物可以增加得分\n");
setpos(42, 20);
system("pause");
system("cls");
}
首先我们使用setpos函数来确定光标的位置,再使用wprintf函数来打印内容,system(“pause”)语句则代表着暂停程序,按任意键表示继续程序,而system(“cls”)语句则代表着将当前控制台的页面全部清空,这样我们就打印出了我们的欢迎界面。
2,游戏地图的创建
在贪吃蛇游戏中,我们将要创建如图所示的游戏地图:
正如我们前一篇博客所讲到的,我们的控制台界面的各个地点都有着对应的坐标,如下:
而我们所想要设计的地图的大小,就可以通过坐标的范围来圈定。所以我们的地图大小应该如下图坐标所示:
有了坐标,我们就知道了地图的上边需要29个宽字符‘▢’,下边也是一样,而左右两边我们则可以使用setpos函数来移动光标进行左右两边边界的打印。所以我们在GameStart()函数中创建一个creatmap()函数,代码如下:
void creatmap()
{
int i = 0;
//上
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
//下
setpos(0, 26);//移动光标以便打印下标
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
//左
for (i = 1; i <= 25; i++)
{
setpos(0, i);//移动光标
wprintf(L"%lc", L'□');
}
//右
for (i = 1; i <= 25; i++)
{
setpos(56, i);//移动光标
wprintf(L"%lc", L'□');
}
system("pause");//按任意键继续
}
代码一走,则地图创建成功!
3,蛇的初始化
我们将蛇想象成我们以前学到的链表,刚开始的蛇身长度为5个节点,所以我们便可以使用头插的办法来完成蛇身的初始化。
但是,在初始化蛇身的时候,我们一定需要注意蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中, 另外⼀半在墙外的现象,坐标不好对齐。如下图所示:
而对于蛇的身体,我们则使用宽字符‘●‘进行表示。为了方便以后更改蛇身体的坐标,墙和蛇的图标,于是我们选择在snake.h中使用define定义:
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
最后,我们还不要忘记对蛇的结构体中的其他成员变量的初始化。所以在GameStart()函数中,我们继续定义一个InitSnake()函数来进行对蛇进行初始化。
void InitSnake(psnake ps)
{
int i = 0;
psnakenode cur = NULL;
for (i = 0; i < 5; i++)
{
cur = (psnakenode)malloc(sizeof(snakenode));//创建节点
if (cur == NULL)//判断是否申请成功
{
perror("InitSnake()::malloc()");
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)
{
setpos(cur->x, cur->y);//设定光标位置
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置蛇的其他属性
ps->_dir = RIGHT;
ps->_food_weight = 10;
ps->_score = 0;
ps->_sleep_time = 200;//单位是毫秒
ps->_sta = OK;
getchar();//暂停程序观察效果,不需要观察效果后可将该语句删除
}
4,创建食物
当蛇的初始化完成后,我们就需要完成食物的初始化。但在初始化食物的时候,我们需要注意到以下三点:
1,食物必须出现在墙体之内
2,食物的x坐标一定为2的倍数
3,食物坐标不可以与蛇身的坐标重复
在该函数中,我们也给食物下定义:
#define FOOD L'※'
为了达到坐标随机生成的目的,我们将在主函数中使用srand((unsigned int)time);(需要包含<time.h>)。所以我们的创建食物函数CreatFood()函数的实现如下:
void CreatFood(psnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;//使横坐标在2~54之内
y = rand() % 24 + 1;//使纵坐标在1~25之内
} while (x % 2 != 0);//若生成的x为奇数,那么就重新生成
psnakenode cur = ps->_psnake;//遍历链表,防止食物与蛇身重合
while (cur)
{
if (x == cur->x && y == cur->y)//如果食物的坐标与蛇身重合
{
goto again;//重新生成
}
cur = cur->next;
}
//打印食物标号
psnakenode food = (psnakenode)malloc(sizeof(snakenode));
if (food == NULL)//防止申请内存失败
{
perror("CreatFood()::malloc()");
return;
}
food->x = x;
food->y = y;
food->next = NULL;
setpos(food->x, food->y);
wprintf(L"%lc", FOOD);
ps->_pfood = food;
getchar();//观察食物打印效果,之后若不需要观察则可以删除该语句
}
代码一走,食物生成成功!
三,游戏运行(GameRun)
1,打印游戏帮助信息
在正式开始游戏时,我们需要在游戏地图之外打印一系列信息,以帮助用户了解游戏的玩法。所以我们在GameRun()函数中封装一个PrintHelpInfo()函数来进行游戏帮助信息的打印。
//打印游戏帮助信息
void PrintHelpInfo()
{
//打印提⽰信息
setpos(64, 15);
printf("不能穿墙,不能咬到自己\n");
setpos(64, 16);
printf("⽤↑.↓.←.→分别控制蛇的移动\n");
setpos(64, 17);
printf("F3 为加速,F4 为减速\n");
setpos(64, 18);
printf("ESC :退出游戏.space:暂停游戏.");
}
代码一走,效果如图:
2,游戏得分的打印与蛇的移动方向的判定
首先,我们实现一个KEY_PRESS宏来判断某个按键是否被按下:
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
所以,我们便可以在GameStart()函数中使用do-while循环来打印游戏的得分与了解按键按下的情况,以便控制蛇的移动方向。
//游戏的运行
void GameRun(psnake ps)
{
//打印游戏帮助信息
PrintHelpInfo();
do
{
//打印游戏得分信息
setpos(64, 10);
printf("总得分:%d\n", ps->_score);
setpos(64, 11);
printf("当前一个食物的分数:%2d\n", ps->_food_weight);
//判断按键以控制蛇的移动
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//VK_UP为↑的虚拟键码,同时蛇的当前方向不可以向下
{
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_SPACE))//空格为暂停
{
Pause();//Pause函数执行暂停操作
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_sta = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_sleep_time < 300)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);//蛇走一步
Sleep(ps->_sleep_time);
} while (ps->_sta == OK);
}
其中Pause()函数如下:
//游戏暂停操作
void Pause()
{
while (1)
{
Sleep(200);//死循环休眠则视为暂停
if (KEY_PRESS(VK_SPACE))
{
break;//再次按下空格则跳出循环
}
}
}
3,蛇的移动(SnakeMove)
当我们在前面获取了按键按下的情况,即蛇的状态信息之后,我们就可以使用SnakeMove()函数来对蛇的移动进行控制。
在该函数中,我们就可以再创建一个节点(pNextNode)表示蛇头即将到达的节点位置,而该新节点的位置与当前蛇头的坐标x,y有关:
于是新节点的位置设置如下:
//蛇走一步
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;
pNextNode->y = ps->_psnake->y;
break;
}
}
4,检测下一个坐标是否为食物
当我们的蛇头移动到下一个坐标的时候,就会产生两种情况:1,下一个坐标为食物。2,下一个坐标不为食物。两种情况应该不同方式来处理,所以我们在SnakeMove()函数中的switch循环之后再定义两个函数来处理不同情况:
//判断下一个坐标是否为食物
int NextIsFood(psnakenode pNextNode, psnake ps)
{
return (pNextNode->x == ps->_pfood->x && pNextNode->y == ps->_pfood->y);
}
//检测下一个坐标是否为食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
Ⅰ,EatFood
该函数用于处理下一个坐标为食物的情况。该情况下,我们就可以直接将食物节点变为蛇的头节点,然后打印蛇,加分,最后再次创建食物:
//下一个坐标为食物
void EatFood(psnakenode pNextNode, psnake ps)
{
//使用头插,将食物节点变为蛇头
ps->_pfood->next = ps->_psnake;
ps->_psnake = ps->_pfood;
//释放掉pNextNode
free(pNextNode);
pNextNode = NULL;
//打印出新蛇
psnakenode cur = ps->_psnake;
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//加分
ps->_score += ps->_food_weight;
//再次创建食物
CreatFood(ps);
}
Ⅱ,NoFood
该函数用于处理下一个坐标不为食物的情况。该情况下,我们则需要做到的任务如下:
1,使用头插法将pNextNode变为新的蛇头
2,把最后一个坐标打印为两个空格
3,释放掉尾节点
4,将倒数第二个节点的next置空
所以我们画图解释:
于是我们使用cur遍历数组,当cur->next->next为NULL时,那么就证明cur来到了倒数第二个节点,此时我们就跳出while循环,之后将cur->next指向的节点处打印两个空格(以覆盖先前打印而留下的蛇身),再将尾节点释放掉,最后将cur->next置空。
//下一个坐标不是食物
void NoFood(psnakenode pNextNode, psnake ps)
{
//头插法
pNextNode->next = ps->_psnake;
ps->_psnake = pNextNode;
//遍历链表
psnakenode cur = ps->_psnake;
while (cur->next->next)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//出了循环,cur刚好停在倒数第二个节点,所以先打印两个空格
setpos(cur->next->x, cur->next->y);
printf(" ");
//释放掉尾节点
free(cur->next);
//将倒数第二个节点的next置空
cur->next = NULL;
}
这样,一个贪吃蛇游戏就初步完成了。
5,判断蛇是否撞墙或者撞到自己
在GameRUN()函数中,我们也需要检测蛇是否在行动的过程中撞到了墙或者撞到了自己。于是我们可以在设计两个函数KillByWall()函数和KillBySelf()函数来进行判断。
Ⅰ,KillByWall
关于蛇是否撞墙的判断十分简单。我们只需要判断蛇头的坐标是否与墙的x或者y坐标重合即可。若重合,则撞墙;反之,则未撞墙。
//检测是否撞墙
void KillByWall(psnake ps)
{
if (ps->_psnake->x == 0 || ps->_psnake->x == 56 ||
ps->_psnake->y == 0 || ps->_psnake->y == 26)
{
ps->_sta = KILL_BY_WALL;
}
}
Ⅱ,KillBySelf
在该函数中,我们只需要从蛇头之后的一个节点遍历蛇身,假如某段蛇身的坐标等于蛇头的坐标,那么就是撞到了自己;反之,则没有撞到自己。
//检测是否撞到自己
void KillBySelf(psnake ps)
{
psnakenode cur = ps->_psnake->next;//从蛇头之后的一个节点开始遍历
while (cur)
{
if (cur->x == ps->_psnake->x && cur->y == ps->_psnake->y)
{
ps->_sta = KILL_BY_SELF;
break;//撞到了自己,退出循环
}
cur = cur->next;
}
}
四,游戏结束(GameEnd)
1,根据蛇的状态打印结束语
有了前文所获取的蛇的结束状态,那我们就可以在GameEnd()函数中打印对应的结束语。
void GameEnd(psnake ps)
{
setpos(24, 12);//设置光标位置,打印结束信息
switch (ps->_sta)
{
case END_NORMAL:
printf("游戏正常结束\n");
break;
case KILL_BY_WALL:
printf("您撞到了墙,游戏结束\n");
break;
case KILL_BY_SELF:
printf("您撞到了自己,游戏结束\n");
break;
}
}
2,释放蛇的空间
当然,当我们的游戏结束后,我们使用的蛇身节点也需要被释放,而释放方法非常简单,就是我们之前在链表操作函数中学到的销毁链表操作。
//游戏结束
void GameEnd(psnake ps)
{
//释放蛇身
psnakenode cur = ps->_psnake;
while (cur)
{
psnakenode del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
如此,我们的贪吃蛇的主体函数就完成了!!
五,游戏的优化设置
在这里,我们将实现游戏的多次使用和正常退出,以及项目结束的信息的位置的设置。这一切的操作,都在test.c中进行。
#include"snake.h"
void test()
{
int ch = 0;
do
{
snake ps = { 0 };
GameStart(&ps);
GameRun(&ps);
GameEnd(&ps);
setpos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'Y' || ch == 'y');
setpos(0, 27);
}
int main()
{
srand((unsigned int)time);
setlocale(LC_ALL, "");//设置本地化以打印宽字符
test();
return 0;
}
六,代码参考
1,test.c
#include"snake.h"
void test()
{
int ch = 0;
do
{
snake ps = { 0 };
GameStart(&ps);
GameRun(&ps);
GameEnd(&ps);
setpos(20, 15);
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'Y' || ch == 'y');
setpos(0, 27);
}
int main()
{
srand((unsigned int)time);
setlocale(LC_ALL, "");//设置本地化以打印宽字符
test();
return 0;
}
2, snake.c
#include"snake.h"
void setpos(int x,int y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出句柄
COORD pos = { x,y };//设定坐标
SetConsoleCursorPosition(houtput, pos);//设定光标的坐标
}
void welcometogame()
{
setpos(40, 14);
wprintf(L"欢迎来到贪吃蛇游戏\n");
setpos(42, 20);
system("pause");
system("cls");
setpos(25, 14);
wprintf(L"使用 ↑ . ↓ . ← . → 来进行方向移动,f3加速,f4减速\n");
setpos(25, 15);
wprintf(L"加速吃到食物可以增加得分\n");
setpos(42, 20);
system("pause");
system("cls");
}
void creatmap()
{
int i = 0;
//上
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
//下
setpos(0, 26);//移动光标以便打印下标
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", L'□');
}
//左
for (i = 1; i <= 25; i++)
{
setpos(0, i);//移动光标
wprintf(L"%lc", L'□');
}
//右
for (i = 1; i <= 25; i++)
{
setpos(56, i);//移动光标
wprintf(L"%lc", L'□');
}
}
void InitSnake(psnake ps)
{
int i = 0;
psnakenode cur = NULL;
for (i = 0; i < 5; i++)
{
cur = (psnakenode)malloc(sizeof(snakenode));//创建节点
if (cur == NULL)//判断是否申请成功
{
perror("InitSnake()::malloc()");
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)
{
setpos(cur->x, cur->y);//设定光标位置
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置蛇的其他属性
ps->_dir = RIGHT;
ps->_food_weight = 10;
ps->_score = 0;
ps->_sleep_time = 200;//单位是毫秒
ps->_sta = OK;
}
void CreatFood(psnake ps)
{
int x = 0;
int y = 0;
again:
do
{
x = rand() % 53 + 2;//使横坐标在2~54之内
y = rand() % 24 + 1;//使纵坐标在1~25之内
} while (x % 2 != 0);//若生成的x为奇数,那么就重新生成
psnakenode cur = ps->_psnake;//遍历链表,防止食物与蛇身重合
while (cur)
{
if (x == cur->x && y == cur->y)//如果食物的坐标与蛇身重合
{
goto again;//重新生成
}
cur = cur->next;
}
//打印食物标号
psnakenode food = (psnakenode)malloc(sizeof(snakenode));
if (food == NULL)//防止申请内存失败
{
perror("CreatFood()::malloc()");
return;
}
food->x = x;
food->y = y;
food->next = NULL;
setpos(food->x, food->y);
wprintf(L"%lc", FOOD);
ps->_pfood = food;
//getchar();//观察食物打印效果,之后若不需要观察则可以删除该语句
}
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 = false;//隐藏鼠标
SetConsoleCursorInfo(houtput, &cursorinfo);//设置光标状态
//打印欢迎界面
welcometogame();
//打印地图
creatmap();
//初始化蛇
InitSnake(ps);
//创建食物
CreatFood(ps);
}
//游戏暂停操作
void Pause()
{
while (1)
{
Sleep(200);//死循环休眠则视为暂停
if (KEY_PRESS(VK_SPACE))
{
break;//再次按下空格则跳出循环
}
}
}
//打印游戏帮助信息
void PrintHelpInfo()
{
//打印提⽰信息
setpos(64, 15);
printf("不能穿墙,不能咬到自己\n");
setpos(64, 16);
printf("用↑.↓.←.→分别控制蛇的移动\n");
setpos(64, 17);
printf("F3 为加速,F4 为减速\n");
setpos(64, 18);
printf("ESC :退出游戏.space:暂停游戏.");
}
//判断下一个坐标是否为食物
int NextIsFood(psnakenode pNextNode, psnake ps)
{
return (pNextNode->x == ps->_pfood->x && pNextNode->y == ps->_pfood->y);
}
//下一个坐标为食物
void EatFood(psnakenode pNextNode, psnake ps)
{
//使用头插,将食物节点变为蛇头
ps->_pfood->next = ps->_psnake;
ps->_psnake = ps->_pfood;
//释放掉pNextNode
free(pNextNode);
pNextNode = NULL;
//打印出新蛇
psnakenode cur = ps->_psnake;
while (cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//加分
ps->_score += ps->_food_weight;
//再次创建食物
CreatFood(ps);
}
//下一个坐标不是食物
void NoFood(psnakenode pNextNode, psnake ps)
{
//头插法
pNextNode->next = ps->_psnake;
ps->_psnake = pNextNode;
//遍历链表
psnakenode cur = ps->_psnake;
while (cur->next->next)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//出了循环,cur刚好停在倒数第二个节点,所以先打印两个空格
setpos(cur->next->x, cur->next->y);
printf(" ");
//释放掉尾节点
free(cur->next);
//将倒数第二个节点的next置空
cur->next = NULL;
}
//检测是否撞墙
void KillByWall(psnake ps)
{
if (ps->_psnake->x == 0 || ps->_psnake->x == 56 ||
ps->_psnake->y == 0 || ps->_psnake->y == 26)
{
ps->_sta = KILL_BY_WALL;
}
}
//检测是否撞到自己
void KillBySelf(psnake ps)
{
psnakenode cur = ps->_psnake->next;//从蛇头之后的一个节点开始遍历
while (cur)
{
if (cur->x == ps->_psnake->x && cur->y == ps->_psnake->y)
{
ps->_sta = KILL_BY_SELF;
break;
}
cur = cur->next;
}
}
//蛇走一步
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;
pNextNode->y = ps->_psnake->y;
break;
}
//检测下一个坐标是否为食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测是否撞墙
KillByWall(ps);
//检测是否撞到自己
KillBySelf(ps);
}
//游戏的运行
void GameRun(psnake ps)
{
//打印游戏帮助信息
PrintHelpInfo();
do
{
//打印游戏得分信息
setpos(64, 10);
printf("总得分:%d\n", ps->_score);
setpos(64, 11);
printf("当前一个食物的分数:%2d\n", ps->_food_weight);
//判断按键以控制蛇的移动
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)//VK_UP为↑的虚拟键码,同时蛇的当前方向不可以向下
{
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_SPACE))//空格为暂停
{
Pause();//Pause函数执行暂停操作
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_sta = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80)
{
ps->_sleep_time -= 30;
ps->_food_weight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_sleep_time < 300)
{
ps->_sleep_time += 30;
ps->_food_weight -= 2;
}
}
SnakeMove(ps);//蛇走一步
Sleep(ps->_sleep_time);
} while (ps->_sta == OK);
}
//游戏结束
void GameEnd(psnake ps)
{
setpos(24, 12);//设置光标位置,打印结束信息
switch (ps->_sta)
{
case END_NORMAL:
printf("游戏正常结束\n");
break;
case KILL_BY_WALL:
printf("您撞到了墙,游戏结束\n");
break;
case KILL_BY_SELF:
printf("您撞到了自己,游戏结束\n");
break;
}
//释放蛇身
psnakenode cur = ps->_psnake;
while (cur)
{
psnakenode del = cur;
cur = cur->next;
free(del);
del = NULL;
}
}
3, snake.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'※'
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)
//定义蛇身体的节点
typedef struct snakenode
{
//坐标
int x;
int y;
struct snakenode* next;//下一个节点
}snakenode,*psnakenode;
//定义方向
enum direction
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
enum game_state
{
OK,//正常状态
KILL_BY_WALL,//撞到墙
KILL_BY_SELF,//撞到了自己
END_NORMAL//正常结束游戏
};
//贪吃蛇
typedef struct snake
{
psnakenode _psnake;//蛇头节点
psnakenode _pfood;//食物节点
enum direction _dir;//蛇的方向
enum game_state _sta;//游戏状态
int _food_weight;//一个食物的分数
int _score;//总成绩
int _sleep_time;//蛇的速度
}snake,* psnake;
//设置光标位置
void setpos(int x, int y);
//开始游戏
void GameStart(psnake ps);
//打印欢迎界面
void welcometogame();
//设置光标坐标
void setpos(int x, int y);
//打印游戏地图
void creatmap();
//蛇的初始化
void InitSnake(psnake ps);
//创建食物
void CreatFood(psnake ps);
//游戏的运行
void GameRun(psnake ps);
//打印游戏帮助信息
void PrintHelpInfo();
//游戏暂停操作
void Pause();
//蛇走一步
void SnakeMove(psnake ps);
//判断下一个坐标是否为食物
int NextIsFood(psnakenode pNextNode, psnake ps);
//下一个坐标为食物
void EatFood(psnakenode pNextNode, psnake ps);
//下一个坐标不是食物
void NoFood(psnakenode pNextNode, psnake ps);
//检测是否撞墙
void KillByWall(psnake ps);
//检测是否撞到自己
void KillBySelf(psnake ps);
//游戏结束
void GameEnd(psnake ps);
快去享受游戏吧!!!