前言
大家好呀,我是Humble,不知不觉在CSND分享自己学过的C语言知识已经有三个多月了,从开始的C语言常见语法概念说到C语言的数据结构今天用C语言实现贪吃蛇已经有30余篇博客的内容,也希望这些内容可以帮助到各位正在阅读的小伙伴~
本次贪吃蛇项目是Humble继扫雷,通讯录之后分享的第三个项目,大约有五六百行代码的量~
而且不出意外,这应该是HumbleC语言部分的最后一篇博客了,在这之后,Humble未来的规划是分享C++的相关语法内容,C++实现的数据结构与算法,计算机网络,操作系统以及数据库的相关知识,希望未来也请各位多多关照~
好了,因为是一篇具有特殊意义的博客,好像前言说的有些多了(笑),接下来废话不多说,直接进入我们今天的贪吃蛇项目的内容把~
游戏演示
关于贪吃蛇这个游戏,我相信各位并不陌生,小伙伴们也跟我一样在童年的时候都玩过吧~
那么在正式制作之前,先给大家看一下这个即将由我们自己制作的贪吃蛇游戏的演示 ,让各位先对它有个整体的了解之后,之后的制作才会更加容易~
当我们运行起来后,首先看到的是两个初始化界面
然后下面是正式开始游戏的画面
围起来的部分是地图,蛇的初始长度被设置为5个节点,通过按键操作吃掉食物会获得相应得分,同时蛇的节点会增加,其余规则可看界面右方给的文字说明
当我们撞到墙或者自身或者按ESC键自己退出时,会显示游戏结束的界面:
以上就是对这个贪吃蛇小游戏简单的一次总揽,更多细节上的东西我们放在下面再讲
目标
本篇博客的目标:
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇
实现基本的功能:
1.初始化以及地图的创建
2.蛇与食物的初始化
3.蛇吃食物
4.撞墙检测
5.装自身检测
6.蛇的移动,加速,减速
7.暂停功能
8.计算得分
....
技术要点
本次贪吃蛇项目涉及的技术要点主要有一下七点:
C语言函数、枚举、结构体、动态内存管理、链表、Win32 API等
前面5点都是Humble在之前的博客都有分享过的,各位小伙伴可以访问我的主页去阅读哦
今天在讲贪吃蛇这个项目之前,我先对Win32 API 做个介绍 ,对其进行了解才能更丝滑的写接下来的贪吃蛇项目哦~
Win32 API
一.Win32 API概述
Windows这个多作业系统除了协调程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programming Interface,简称API函数
WIN32API也就是Microsoft Windows32位平台的应用程序编程接口
当然,这里大家不要太在乎这个32位平台,我们就把它统称为WIN32 API
二.控制台程序(Console)
控制台程序其实就是平常我们程序运行起来出现的界面
我们可以设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
mode con cols=100 lines=30
也可以通过命令设置控制台窗口的名字:
title 贪吃蛇
这些命令,我们可以调用C语言函数system来执行。
代码如下:
#include <windows.h>
int main()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
return 0;
}
三.控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体(具体内容我们可以不必深究,主要是为下面的几个函数服务的,咱们会用就行~),表示一个字符在控制台屏幕上的坐标
给坐标赋值:COORD pos = { 10, 15 };
四.GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(类似权限),使用这个句柄可以操作设备
它的定义如下:
HANDLE GetStdHandle(DWORD nStdHandle);//返回类型是HANDLE
//括号里只能是标准输入、标准输出或标准错误,这些都有对应的单词,比如标准输出写为STD_OUTPUT_HANDLE
我们直接看一下它怎么用吧~举个例子:
HANDLE hOutput = NULL;//我们用一个HANDLE的变量接收函数返回值
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//括号里是标准输出
好,掌握了GetStdHandle()之后,接下来就很简单了,我们看一下下面的几个函数把,使用它们可以帮我们实现对光标和虚拟键的操作
五.GetConsoleCursorInfo
GetConsoleCursorInfo,它是用来检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
实例:
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
CONSOLE_CURSOR_INFO CursorInfo;//以CONSOLE_CURSOR_INFO类型定义一个变量~
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
这里出现了一个CONSOLE_CURSOR_INFO,是我们前面没出现的,也给大家简单介绍一下~
CONSOLE_CURSOR_INFO,它是结构体,包含有关控制台光标的信息
它的结构如下:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO;
dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条(这个我们今天用不到)
bVisible,游标的可见性。如果光标可见,则此成员为true,我们接下来要实现的贪吃蛇要隐藏光标,所以要将bVisible设置成false,所以要这样写:
CONSOLE_CURSOR_INFO CursorInfo;
CursorInfo.bVisible = false; //隐藏控制台光标
因为上面的对光标的操作只有使用SetConsoleCursorInfo后才能真正被修改,所以下面来看一下SetConsoleCursorInfo,
六.SetConsoleCursorInfo
SetConsoleCursorInfo的作用是设置指定控制台屏幕缓冲区的光标的大小和可见性
它有两个参数,这里也不过多赘述,直接看代码:
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
经过SetConsoleCursorInfo 函数后,上面对光标的隐藏操作也就在控制台上得以实现啦~
七.SetConsoleCursorPosition
SetConsoleCursorPosition:调用这个函数将光标位置设置到指定的位置
代码如下:
COORD pos = { 10, 5};//我们将想要设置的坐标信息放在COORD类型的pos中
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOutput, pos);//设置标准输出上光标的位置为pos
当然,为了之后写贪吃蛇代码时使用的更加方便,我们可以将这段代码封装成一个函数SetPos
这样我们之后想改变光标到我们想要的位置就会更加得心应手了~
void SetPos(int x, int y)
{
COORD pos = { x, y};
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOutput, pos);
}
最后我们再看一个函数GetAsyncKeyState,它是用来实现游戏的交互,即获取按键情况的~
八.GetAsyncKeyState
当我们电脑上的按键被出发时,键盘上每个键的虚拟键值会传递给函数,函数通过返回值来分辨按键的状态
在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中最高位是1,说明按键的状态是按下。如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明该按键被按过,没按过的话则为0
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
我们可以用#define定义:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
当然,关于VK虚拟键的值,在Win API 的官网都有说明,我们可以去翻阅来获得自己想找的值~
说完了Win API及接下来会用到的这些函数的知识,下面我们进入贪吃蛇游戏的设计与分析吧~
贪吃蛇游戏设计与分析
一.地图
前面在演示的时候,我们看过游戏的地图长这个样子~
无论是打印墙体使用的字符□,还是打印蛇使用的字符●,或者打印食物使用的字符★
它们都是名为宽字符的存在
那么什么是宽字符?它与普通字符有什么区别?
其实,从大小上看。普通的字符是占一个字节的,这类宽字符是占用2个字节的
这点很重要,因为我们的地图是建立在坐标上的,宽字符的x轴占了2格,而y轴只占1格,我们要考虑好这点,再去设计比如 :地图的大小以及蛇与食物随机生成的坐标的规定等等
知道了这个,我们看一看宽字符是怎么打印的吧~
宽字符的打印
#include <stdio.h
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");
//这个setlocale函数以及它包含的头文件locale.h是为了将模式切换成本地环境,大家直接用就可以,不用深究~
wchar_t ch1 = L'●';
wchar_t ch2 = L'原';
wchar_t ch3 = L'神';
wchar_t ch4 = L'★';
printf("%c%c\n", 'a', 'b');
wprintf(L"%c\n", ch1);//wprintf是用来打印宽字符的,w即wide,宽的意思
wprintf(L"%c\n", ch2);//宽字符的打印要在%前加上L,别忘了哦~
wprintf(L"%c\n", ch3);
wprintf(L"%c\n", ch4);
return 0;
}
打印结果如下:
我们发现一个普通字符占一个字符的位置.但是打印一个宽字符,占用2个字符的位置
地图坐标
我们假设实现一个棋盘27行,58列的棋盘,再围绕地图画出墙
如下图:
二.蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是 ● ,在固定的一个坐标处开始出现
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印 ★
三.数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点。
每个节点只要记录好蛇身节点在地图上的坐标就行
所以蛇节点结构如下:
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
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//正常结束
};
四.游戏流程设计
关于游戏流程设计,Humble做了一张思维导图供大家参考~:
核心逻辑实现分析
我们打开VS,创建一个Snake的贪吃蛇工程
在其中创建3个文件:test.c测试文件,snake.c源文件,snake.h头文件
在test.c中我们写游戏的主逻辑:
#define _CRT_SECURE_NO_WARNINGS
#include "snake.h"
void test()
{
int ch = 0;
srand((unsigned int)time(NULL));
do
{
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
Snake snake = { 0 };//创建贪吃蛇
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);//设置光标的位置
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'Y');
SetPos(0, 27);
}
int main()
{
//修改适配本地中文环境
setlocale(LC_ALL, "");
test();//贪吃蛇游戏的测试
return 0;
}
GameStart
我们先看游戏开始GameStart这个函数,它包含五个函数:
void GameStart(pSnake ps)
{
WelcomeToGame();//打印欢迎信息
CreateMap(); //绘制地图
InitSnake(ps); //初始化蛇
CreateFood(ps);//创建食物
PrintHelpInfo();//打印右侧帮助信息
}
先看第一个:打印欢迎信息WelcomeToGame
void WelcomeToGame()
{
//欢迎信息
SetPos(35, 10);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(38, 20);
system("pause");
system("cls");//清空之前的界面
//功能介绍信息
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(15, 11);
printf("加速能得到更高的分数");
SetPos(38, 20);
system("pause");
system("cls");
}
接下来是创建地图 CreateMap
#define WALL L'□' //宽字符的定义前面要加上L
void CreateMap()
{
int i = 0;
//上
SetPos(0, 0);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 25);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
下面是初始化蛇InitSnake
蛇最开始为5节,每节对应链表的一个节点,蛇的每一个节点都自己的坐标
创建5个节点,然后将每个节点存放在链表中进行管理创建完蛇身后,将蛇的每一节打印在屏幕上
再设置当前游戏的状态,蛇移动的速度,默认的方向(向右),初始成绩,蛇的状态,每个食物的分数
#define BODY L'●'
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
int i = 0;
//创建蛇身节点,并初始化坐标
//头插法
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 + i * 2;
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"%c", BODY);
cur = cur->next;
}
//初始化贪吃蛇数据
ps->_SleepTime = 200;
ps->_Socre = 0;
ps->_Status = OK; ps->_Dir = RIGHT;
ps->_foodWeight = 10;
}
GameStart包含的第四个子函数是:CreateFood
随机生成食物的坐标
下面有一些限制:
x坐标必须是2的倍数
食物的坐标不能和蛇身每个节点的坐标重复
#define FOOD L'★' //食物打印的宽字符
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
//食物不能和蛇身冲突
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物
if (pFood == NULL)
{
perror("CreateFood::malloc()");
return;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos(pFood->x, pFood->y);
wprintf(L"%c", FOOD);
ps->_pFood = pFood;
}
}
GameStart的最后一个函数是PrintHelpInfo,负责打印文字信息
void PrintHelpInfo()
{
SetPos(64, 15);
printf("不能穿墙,不能咬到自己\n");
SetPos(64, 16);
printf("用↑.↓.←.→分别控制蛇的移动.");
SetPos(64, 17);
printf("F3 为加速,F4 为减速\n");
SetPos(64, 18);
printf("ESC :退出游戏. 空格键:暂停游戏.");
SetPos(64, 20);
}
GameRun
下面来看一下游戏运行的函数GameRun吧~
void pause()//暂停
{
while (1)
{
Sleep(300);
if (KEY_PRESS(VK_SPACE))//空格键
break;
}
}
void GameRun(pSnake ps)
{
do
{
SetPos(64, 10); //每次进来都要对食物分数进行修正
printf("得分:%d ", ps->_Socre);
printf("每个食物得分:%d分", ps->_foodWeight);
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_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status = END_NOMAL;
break;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_SleepTime < 350)
{
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
if (ps->_SleepTime == 350)
{
ps->_foodWeight = 1;
}
}
}
//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);
虽然这个是三个函数最复杂的部分,但我们也是一个一个点,按照设计的流程来~
关于虚拟键检测按键状态,我们封装一个宏~
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
下面是蛇身的移动,这个很关键~
先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下⼀个位置的坐标
确定了下⼀个位置后,判断下一个位置是否是食物:NextIsFood
是食物就做:EatFood,如果不是食物:NoFood
蛇身移动后,判断此次移动是否会造成撞墙:KillByWall或者撞上自己:KillBySelf
void SnakeMove(pSnake ps)
{
//创建下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
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);
}
int NextIsFood(pSnakeNode psn, pSnake ps)
{
return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
void EatFood(pSnakeNode psn, pSnake ps)
{
//头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
ps->_Socre += ps->_foodWeight;
free(ps->_pFood);
CreateFood(ps);
}
//将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,弃掉蛇身的最后⼀个节点
//pSnakeNode psn 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
void NoFood(pSnakeNode psn, pSnake ps)
{
//头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
//最后一个位置打印空格,然后释放节点
SetPos(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;
}
呼,终于将最核心的逻辑搞定了~最后来看看游戏结束的函数GameEnd吧~
GameEnd
游戏状态不再是OK的时候,要告知游戏结束的原因,并且释放蛇身节点
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
SetPos(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);
}
}
到这,我们整个贪吃蛇游戏的功能就全部实现了(鼓掌鼓掌)
最后是Humble的参考代码,分3个文件
参考代码
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "snake.h"
void test()
{
int ch = 0;
srand((unsigned int)time(NULL));
do
{
Snake snake = { 0 };//创建贪吃蛇
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(20, 15);//设置光标的位置
printf("再来一局吗?(Y/N):");
ch = getchar();
getchar();//清理\n
} while (ch == 'Y');
SetPos(0, 27);
}
int main()
{
//修改适配本地中文环境
setlocale(LC_ALL, "");
test();//贪吃蛇游戏的测试
return 0;
}
snake.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <time.h>
//宽字符的定义
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇的初始位置
#define POS_X 24
#define POS_Y 5
//虚拟键
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//游戏状态
enum GAME_STATUS
{
OK,//正常运行
KILL_BY_WALL,//撞墙结束
KILL_BY_SELF,//咬到自己结束
END_NOMAL//按键结束
};
//蛇身节点
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;
//设置光标的坐标
void SetPos(int x, int y);
//欢迎界面
void WelcomeToGame();
//创建地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//打印右侧界面的文字说明
void PrintHelpInfo();
//暂停响应
void pause();
//下一个节点是食物
int NextIsFood(pSnakeNode psn, pSnake ps);
//吃食物
void EatFood(pSnakeNode psn, pSnake ps);
//不吃食物
void NoFood(pSnakeNode psn, pSnake ps);
//撞墙检测
int KillByWall(pSnake ps);
//撞自身检测
int KillBySelf(pSnake ps);
//蛇的移动
void SnakeMove(pSnake ps);
//游戏开始
void GameStart(pSnake ps);
//游戏运行
void GameRun(pSnake ps);
//游戏结束
void GameEnd(pSnake ps);
snake.c
#define _CRT_SECURE_NO_WARNINGS
#include "snake.h"
//设置光标的位置的函数
void SetPos(int x, int y)
{
HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE); //获得设备句柄
COORD pos = { x, y }; //根据句柄设置光标的位置
SetConsoleCursorPosition(hanlde, pos);
}
void WelcomeToGame()
{
//欢迎信息
SetPos(35, 10);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(38, 20);
system("pause");
system("cls");//清空之前的界面
//功能介绍信息
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(15, 11);
printf("加速能得到更高的分数");
SetPos(38, 20);
system("pause");
system("cls");
}
void CreateMap()
{
int i = 0;
//上
SetPos(0, 0);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 25);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
int i = 0;
//创建蛇身节点,并初始化坐标
//头插法
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 + i * 2;
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"%c", BODY);
cur = cur->next;
}
//初始化贪吃蛇数据
ps->_SleepTime = 200;
ps->_Socre = 0;
ps->_Status = OK; ps->_Dir = RIGHT;
ps->_foodWeight = 10;
}
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针
//食物不能和蛇身冲突
while (cur)
{
if (cur->x == x && cur->y == y)
{
goto again;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建食物
if (pFood == NULL)
{
perror("CreateFood::malloc()");
return;
}
else
{
pFood->x = x;
pFood->y = y;
SetPos(pFood->x, pFood->y);
wprintf(L"%c", FOOD);
ps->_pFood = pFood;
}
}
void PrintHelpInfo()
{
SetPos(64, 15);
printf("不能穿墙,不能咬到自己\n");
SetPos(64, 16);
printf("用↑.↓.←.→分别控制蛇的移动.");
SetPos(64, 17);
printf("F3 为加速,F4 为减速\n");
SetPos(64, 18);
printf("ESC :退出游戏. 空格键:暂停游戏.");
SetPos(64, 20);
}
void pause()//暂停
{
while (1)
{
Sleep(300);
if (KEY_PRESS(VK_SPACE))//空格键
break;
}
}
int NextIsFood(pSnakeNode psn, pSnake ps)
{
return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
void EatFood(pSnakeNode psn, pSnake ps)
{
//头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
ps->_Socre += ps->_foodWeight;
free(ps->_pFood);
CreateFood(ps);
}
void NoFood(pSnakeNode psn, pSnake ps)
{
//头插法
psn->next = ps->_pSnake;
ps->_pSnake = psn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%c", BODY);
cur = cur->next;
}
//最后一个位置打印空格,然后释放节点
SetPos(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 pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
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 GameStart(pSnake ps)
{
WelcomeToGame();//打印欢迎信息
CreateMap(); //绘制地图
InitSnake(ps); //初始化蛇
CreateFood(ps);//创建食物
PrintHelpInfo();//打印右侧帮助信息
}
void GameRun(pSnake ps)
{
do
{
SetPos(64, 10);
printf("得分:%d ", ps->_Socre);
printf("每个食物得分:%d分", ps->_foodWeight);
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_SPACE))
{
pause();
}
else if (KEY_PRESS(VK_ESCAPE))
{
ps->_Status = END_NOMAL;
break;
}
else if (KEY_PRESS(VK_F3))
{
if (ps->_SleepTime >= 50)
{
ps->_SleepTime -= 30;
ps->_foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
if (ps->_SleepTime < 350)
{
ps->_SleepTime += 30;
ps->_foodWeight -= 2;
if (ps->_SleepTime == 350)
{
ps->_foodWeight = 1;
}
}
}
//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快
Sleep(ps->_SleepTime);
SnakeMove(ps);
} while (ps->_Status == OK);//游戏状态是OK则继续游戏
}
void GameEnd(pSnake ps)
{
pSnakeNode cur = ps->_pSnake;
SetPos(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);
}
}
结语
好了,今天的这个贪吃蛇游戏的实现分享就到这里了
作为C语言部分的最后一篇博客(大概),虽然心情复杂,但不知道该说些什么
总之还是那句话:在学习编程的道路上Humble与各位同行,加油吧各位!
希望大家动动小手帮我点个免费的赞或者关注(感谢感谢),也欢迎大家订阅我的专栏呀~
让我们在接下来的时间里一起成长,一起进步吧!