一.基本信息
实现目标:使用C语言在Windows环境的控制台中实现贪吃蛇游戏
游戏运行:
- 地图绘制
- 基本玩法
- 提示信息
- 游戏的开始与结束
基本玩法:
- 通过上下左右键控制蛇的移动
- 蛇可以加速减速
- 吃掉食物可以得分并增加蛇的长度
- 可以自动暂停
游戏结束:
- 撞墙
- 撞到自己
- 主动选择退出
涉及到的知识点:
C语言函数,枚举,结构体,单链表,动态内存管理,预处理指令,Win32API等
Windows控制面板的设置(VS2022):
效果:
(ps:system函数可以用来执行系统命令,头文件<stdlib.h> )
回想一下贪吃蛇的游戏面板,其中蛇应该出现在界面中央,而食物的位置是随机的,这就涉及以下几个问题
1.屏幕光标的坐标设置。
2.如何去操控光标呢?又如何去设置光标的坐标呢?
3.游戏进行过程中并不会看到光标,也就是说,要隐藏光标
4.如果通过按键的输入来控制蛇的移动
至于如何解决这些问题,就要用到Win32API的知识了,下面我就来简单介绍一下。
二.Win32API:
WIN32 API简介:
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。
控制台上的屏幕坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
COORD原型说明:
typedef struct _COORD
{
SHORT X;
SHORT Y;
}COORD;
COORD pos ={10,15};
2.WIN32API函数
2.1.GetStdHandle函数
作用:
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。(句柄类似于玩游戏时操控的手柄)
函数原型声明:
HANDLE GetStdHandle(DWORD nStdHandle);
参数:
对于我们今天要实现的贪吃蛇代码来说,只需用到第二个参数就可以了
返回值:
如果该函数成功,则返回值为指定设备的句柄,(或为由先前对 SetStdHandle 的调用设置的重定向句柄。)
如果函数失败,则返回值为 INVALID_HANDLE_VALUE。
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄
2.2. CONSOLE_CURSOR_INFO
这是windows自带的一个结构体(可以直接拿来用),包含了控制台光标的信息
原型声明:
typedef struct _CONSOLE_CURSOR_INFO
{
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO;
• dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
• bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
实例:
CONSOLE_CURSOR_INFO CursorInfo;
CursorInfo.bVisible = false;
2.3 GetConsoleCursorInfo 函数
作用:
检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。
语法:
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
参数含义:
hConsoleOutput
控制台屏幕缓冲区的句柄。lpConsoleCursorInfo
指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息
返回值:
如果该函数成功,则返回值为非零值。
如果函数失败,则返回值为零。
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息
2.4 SetConsoleCursorInfo 函数
作用:
设置制定控制台光标大小和可见性
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
参数
hConsoleOutput
控制台屏幕缓冲区的句柄。lpConsoleCursorInfo [in]
指向 结构的指针CONSOLE_CURSOR_INFO,该结构为控制台屏幕缓冲区的光标提供新的规范。
返回值
如果该函数成功,则返回值为非零值。
如果函数失败,则返回值为零。
实例:
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//检索有关制定控制台屏幕缓冲区的光标大小和可见性信息
CursorInfo.bVisible = false;
SetConsoleCursorInfo(houtput,&CursorInfo);//设置控制台光标的状态
2.5 SetConsoleCursorPosition 函数
作用:
设置指定控制台屏幕缓冲区中的光标位置。
语法:
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD dwCursorPosition
);
参数含义:
hConsoleOutput
控制台屏幕缓冲区的句柄。dwCursorPosition
指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内。
实例:
COORD pos = { 10,15 };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输入的句柄
SetConsoleCursorPosition(houtput, pos);//设置光标位置
2.6 GetAsyncKeyState 函数
作用:
确定调用函数时键是向上还是向下,以及上次调用 GetAsyncKeyState 后是否按下了该键。
语法:
SHORT GetAsyncKeyState(
int vKey
);
参数:
vkey:虚拟键码
以下是需要用到的虚拟键码
VK_ESCAPE
0x1B ESC 键 VK_SPACE
0x20 空格键 VK_LEFT
0x25 LEFT ARROW 键 VK_UP
0x26 UP ARROW 键 VK_RIGHT
0x27 RIGHT ARROW 键 VK_DOWN
0x28 DOWN ARROW 键
VK_F3
0x72 F3 键 VK_F4
0x73 F4 键
返回值:
GetAsyncKeyState 的返回值是short类型,在上一次调用函数GetAsyncKeyState 后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位位置为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
总结:
GetStdHandle 函数——获得设备的控制手柄
GetConsoleCursorInfo 函数——获得光标信息
SetConsoleCursorInfo 函数——隐藏光标
SetConsoleCursorPosition 函数——设置光标位置
GetAsyncKeyState 函数 ——检测按键情况
这些函数本质上和库函数无甚区别,知道这些函数的返回值类型和参数后,就可以直接使用了。
三.贪吃蛇游戏的设计与分析
1.布置地图
1.1 控制台窗口的坐标:
横向为x轴,从左到右依次增长,纵向为y轴,从上到下,依次增长
坐标原点(0,0)在最左上角
x轴和y轴的单位长度不同,y轴的单位长度要大于x轴
1.2 地图元素:
墙体:□
蛇身:●
食物:★
以上字符,我采用宽字符的方式打印
普通字符占一个字节,宽字符占两个字节,汉字就是宽字符
1.3.宽字符的打印
背景介绍: C语言在不断适应国际化的过程中,加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
1.3.1setlocale函数
C语言的标准输出是英文模式,即所有字符都只占一个字节,这显然无法满足所有国家(例如中国)的输出要求,因为光汉字数量就多打10万个,更别提字符了,所以C语言提供了本地模式。
setlocale函数用于修改当前地区,可以针对一个内容修改,也可针对不同内容修改。
原型声明:
char* setlocale (int category, const char* locale);
setlocale 的第一个参数如果是LC_ALL,就会影响所有的内容。C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");//标准模式
参数:
category
Portion of the locale affected. It is one of the following constant values defined as macros in <clocale>:
value Portion of the locale affected LC_ALL The entire locale. LC_COLLATE Affects the behavior of strcoll and strxfrm. LC_CTYPE Affects character handling functions (all functions of <cctype>, except isdigit and isxdigit), and the multibyte and wide character functions. LC_MONETARY Affects monetary formatting information returned by localeconv. LC_NUMERIC Affects the decimal-point character in formatted input/output operations and string formatting functions, as well as non-monetary information returned by localeconv. LC_TIME Affects the behavior of strftime. locale
C string containing the name of a C locale. These are system specific, but at least the two following locales must exist:
locale name description "C"
Minimal "C" locale ""
Environment's default locale If the value of this parameter is
NULL
, the function does not make any changes to the current locale, but the name of the current locale is still returned by the function.
而我们只需用到 LC_ALL和""
实例:
setlocale(LC_ALL, "");//切换到本地环境
1.3.2.打印函数
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应wprintf() 的占位符为 %ls
演示:
#include<stdio.h>
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");//切换到本地环境
wchar_t ch1 = L'□';
wchar_t ch2 = L'吃';
wprintf(L"%lc\n", ch1);
printf("%c\n", 'a');
wprintf(L"%lc\n", ch2);
return 0;
}
效果:
普通字符和宽字符的打印宽度:
1.4.地图坐标
实现一个27行(y轴),58列(x轴)的棋盘(读者可以自行修改)
设置光标位置
//设置光标位置
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
墙体坐标:
y = 0 , x: 0~56
y = 26, x: 0~56
x = 0 , y: 1~26
x = 56, y: 1~26
代码逻辑:
打印墙体的两种情况:
横行:先设置光标位置,再通过循环依次打印剩下的墙体
纵列:在循环体里设置光标位置,再打印墙体
函数代码:
#define WALL L'□'
//创建地图
void CreatMap()
{
setlocale(LC_ALL, "");//设置为本地模式
int i = 0;
//一个墙体占两个字节,
//y = 0 , x: 0~56
SetPos(0, 0);
for (i = 0; i < 58; i += 2)//一个墙体占两个字节
{
wprintf(L"%lc", WALL);
}
//y = 26, x: 0~56
SetPos(0,26);
for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节
{
wprintf(L"%lc", WALL);
}
//x = 0 , y: 1~26
for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//x = 56, y: 1~26
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
但是我们测试代码时会出现这种情况:
最下面一行的墙体被遮挡了,那时因为循环结束后,光标在(0,26)这个位置
在代码末尾手动将光标坐标置为(27,0)就可以了
2.数据结构体设计
2.1蛇身和食物链表
结构体中应包含蛇的位置坐标和指向下一个位置的指针
代码:
//贪吃蛇蛇身的节点
typedef struct SnakeNode
{
int x;//x轴坐标
int y;//y轴坐标
struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;
2.2贪吃蛇游戏的结构体
回想一下贪吃蛇游戏,有哪些元素呢?
蛇,食物,食物的分数,蛇走的方向,当下的得分,蛇的速度,游戏状态(死亡or正常),食物的分值
那结构体的元素应包含:
1.指向蛇和食物的指针
2.蛇头的方向:上下左右,可以用枚举体
3.当下的得分
4.游戏状态,也可以用枚举
5.蛇的速度
6.食物的分值
这里就出现了一个问题了,蛇的速度如何设置呢?这里介绍一个方法,通过调节程序休眠的时间(即蛇每走一步,程序就暂停一段时间)间隔,来控制速度
代码:
//蛇头移动的方向
enum DIRECTION
{
UP,//上
DOWN,//下
LEFT,//左
RIGHT//右
};
//游戏状态
enum GAME_STATUS
{
NORMAL,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞到自己了
END_NORMAL//主动退出
};
//贪吃蛇
typedef struct Snake
{
SnakeNode* psnake;//指向蛇的指针
SnakeNode* pfood;//指向食物的指针
enum DIRECTION dir;//蛇头走的方向
enum GAME_STATUS status;//当前游戏的状态
int food_score;//食物的分数
int score;//当下的得分
int sleep_time;//程序休眠时间
}Snake;
3.游戏流程设计
3.1.游戏开始
3.1.1设置窗口大小和名字
3.1.2隐藏光标
3.1.2创建欢迎界面
3.1.3创建地图,蛇,食物
3.2 游戏运行
3.2.1打印提示信息
3.2.2蛇的移动和速度
3.2.3食物的分值(蛇速度越快,食物分值越高)
3.2.4计算当下的得分
3.2.5判断当下的游戏状态(蛇是否死亡)
3.2.6蛇是否吃到食物了(吃到食物,蛇身加长)
3.2.7通过按键情况移动蛇身(蛇头移动方向的下一个位置是蛇身的新节点,尾结点删除)
3.3游戏结束
3.3.1给出结束原因
3.3.2销毁蛇身
3.3.3给出提示信息,要不要继续玩
四.游戏主逻辑的实现
1.游戏开始GameStart:完成游戏的初始化
2.游戏进行GameRun:完成游戏逻辑的实现
3.游戏结束GameEnd:销毁蛇身,释放内存
1.GameStart
1.1初始化游戏数据
1.1.1创建蛇身链表
创建蛇身节点,(蛇的坐标默认是不变的,大概位于控制台控制台屏幕中央)
1.1.2设置贪吃蛇游戏的数据(即给Snake结构体的各个变量赋值)
蛇头方向,游戏状态,当下的得分,食物的分值,以及默认程序休眠时间
1.1.3食物链表
由于食物的创建涉及到创建节点和设置随机坐标两方面,所以将其单独封装成一个函数
设置坐标,申请节点,打印食物
食物的坐标可以完全随机吗?显然不是,它是有一定限制的
食物坐标的位置是有范围限定的
1.不可与蛇身重叠
2.不能在墙体之外
3.食物的x轴坐标必须为2的倍数
为什么呢?
蛇身占两个字节,它往前挪动一步也是两个字节。
第二个问题就来了,为什么不能一个字节一个字节的移动呢?
因为蛇身的下一个位置,就是新的链表节点所在,所以食物的坐标x必须为2的倍数,这样蛇才能吃到食物
代码
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
//初始化贪吃蛇数据
void InitSnake(Snake* ps)
{
setlocale(LC_ALL, "");//本地化
//初始化蛇身
int i = 0;
SnakeNode* pcur = NULL;
for(i = 0;i<5;i++)
{
pcur = (SnakeNode*)malloc(sizeof(SnakeNode));//申请内存空间
if (pcur == NULL)
{
perror("InitSnake():malloc");//打印错误信息
exit(1);//非正常退出
}
//设置蛇的位置坐标
pcur->x = POS_X+i*2;//一个蛇身占两个字节
pcur->y = POS_Y;
pcur->next = NULL;
if (ps->psnake == NULL)//蛇身为空
{
ps->psnake = pcur;
}
else//头插法
{
pcur->next = ps->psnake;
ps->psnake = pcur;
}
}
pcur = ps->psnake;//pcur指向蛇头
//打印蛇身
while (pcur)
{
//设置光标位置
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
//初始化其他数据
ps->dir = RIGHT;//初始状态向右走
ps->food_score = 10;//食物的分值为10
ps->score = 0;
ps->sleep_time = 200;//单位是毫秒
ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{
setlocale(LC_ALL, "");
//1.创建食物坐标
//食物坐标范围x:2~54,y:1~25
int x = 0;
int y = 0;
//蛇身的大小是两个字节,蛇每次整体向前移动一步也是两个字节
//食物的坐标必须是2的倍数,这样蛇才能吃到食物
again:
do
{
x = rand() % 52 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
SnakeNode* pcur = ps->psnake;
while (pcur)//食物不能和蛇身重合
{
if (x == ps->psnake->x && y == ps->psnake->y)
{
goto again;//重新生成坐标
}
pcur = pcur->next;
}
//2.创建食物节点
SnakeNode* pf = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pf == NULL)
{
perror("CreatFood():malloc");//打印错误信息
exit(1);
}
//3.给食物节点的变量赋值
pf->x = x;
pf->y = y;
pf->next = NULL;
ps->pfood = pf;//pfood指针指向新创建的节点
//4.打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
效果:
1.2.创建地图
该函数在前面已经实现了,即CreatMap函数
1.3打印欢迎界面
首先,语句的出现位置一定是在界面中央,即要先设置光标位置
其次,提示游戏规则
代码
//欢迎界面
void WelComeToGame()
{
//1.打印第一个界面
SetPos(40, 15);//设置光标位置
printf("%s", "欢迎来到贪吃蛇小游戏!");
SetPos(40, 25);//让“按任意键继续”的出现好看点
system("pause");//程序暂停
system("cls");//清理屏幕信息
//2.打印提示游戏规则的界面
SetPos(25, 12);//设置光标位置
printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
SetPos(40, 25);//让“按任意键继续”的出现好看点
system("pause");
system("cls");
}
效果:
1.4.在游戏开始界面右边的空白处打印提示信息
打印游戏规则提示语句,但是我们发现,食物的分值和得分是会随着游戏的进行而变化的,所以不妨把这两个信息放在GameRun函数中去实现。
代码:
//打印提示信息
void PrintInfo(Snake* ps)
{
SetPos(64, 11);
printf("↑ 上 .↓ 下 .← 左 .→ 右\n");
SetPos(64, 12);
printf(" F3为加速,F4为减速\n");
SetPos(64, 14);
printf("不能穿墙,不能咬到自己");
//SetPos(64, 15);
//printf("食物分值:%d,得分:%d", ps->food_score, ps->score);
SetPos(64,17);
printf("按ECS退出,按空格暂停");
}
GameStart函数代码:
//游戏开始
void GameStart()
{
//欢迎界面
WelComeToGame();
srand((unsigned int)time(NULL));
//初始化贪吃蛇数据
Snake snake = { 0 };
InitSnake(&snake);
//创建食物
CreatFood(&snake);
//创建地图
CreatMap();
//打印提示信息
PrintInfo(&snake);
}
2.GameRun
2.1接收按键信息
在前面有介绍,如果 GetAsyncKeyState 函数返回值的16位short数据中,最低位为1则说明,该按键被按过,否则为0。如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1.
那么,如何检测最低位呢?将返回值按位与1,即可得到最低位。
游戏过程中,游戏的有效按键有8个,(上下左右,加速减速,退出暂停),那就需要考虑八种情况。如果每一种情况都写一遍判断语句,太过麻烦,不妨将其分装成一个宏。
#define KEY_PRESS(VK) (GetAsyncKeyState(VK)&1)?1:0
//最低位为1表达式为真,宏为1,反之,宏为0
2.1.1.改变蛇头的方向
在贪吃蛇的结构体中,我们写了一个枚举变量dir,用来表示蛇头的方向。蛇每向前走一步,我们就判断一次玩家的按键情况,然后修改枚举变量的值。
这时候问题来了,假设记录的上一个蛇头方向是向前,那么蛇能往后走吗?显然是不能的(不然不就咬到自己了嘛),所以在代码中还要进行if语句判断。
2.1.2.改变蛇的运动速度
按F3,F4键后,蛇会加速,减速,相应地,食物的分值也会增加和上升,假设每减速一次,休眠时间增加30ms,食物的分值减少2分,那么程序休眠时间最多增加到320s,因为食物的分值必须大于0。为了保持一致性,我将休眠时间的下限设置为80ms(当然读者也可以不设置该下限,但至少休眠时间必须保证大于0)
2.1.3.游戏暂停或退出
玩家按了ESC键后,整个游戏退出,按空格键,游戏暂停,这里就要写一个函数——暂停函数,当玩家再次按了空格键后,暂停状态结束。
2.2蛇移动函数
2.2.1创建新的节点
2.2.2根据蛇头的位置和方向确定下一个节点的坐标
蛇头方向向上或向下:x轴坐标不变,y轴坐标-1,+1
向左或向右:y轴坐标不变,x轴坐标-2,+2,
然后链接新节点
2.2.3.根据蛇有无吃到食物,来决定尾结点是否保留
没吃到食物,尾结点释放;吃到食物,尾结点保留,相当于蛇身的长度增加一节,将其写成不同的两个函数
在两个函数中,都要对新创建的蛇身进行打印,不同的是未吃到食物的函数中,需要在原来蛇尾的地方打印两个空格,用来清理蛇身。且在遍历蛇身的过程中,只遍历到倒数第二个节点,并释放尾结点;吃到食物的函数中,要对得分进行修改,并销毁原食物节点,创建新的食物
2.3.4判断游戏的状态
撞墙否,咬到自己否,并据此设置贪吃蛇结构体中的游戏状态变量
3.GameEnd
打印游戏结束的原因:正常退出or撞墙or咬到自己
在测试文件中,还可以加上提示语句:是否再来一局
到这里,代码的整体逻辑基本就写完了,但还有一些善后的工作,例如光标还未隐藏,控制台的窗口名字还未设置,不妨将其加到Gamestart函数中去。
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);//设置控制台光标的状态
附录:
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)&1)?1:0)
//最低位为1表达式为真,宏为1,反之,宏为0
//蛇头移动的方向
enum DIRECTION
{
UP,//上
DOWN,//下
LEFT,//左
RIGHT//右
};
//游戏状态
enum GAME_STATUS
{
NORMAL,//正常
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞到自己了
END_NORMAL//主动退出
};
//贪吃蛇蛇身的节点
typedef struct SnakeNode
{
int x;//x轴坐标
int y;//y轴坐标
struct SnakeNode* next;//指向下一个节点的指针
}SnakeNode;
//贪吃蛇
typedef struct Snake
{
SnakeNode* psnake;//指向蛇的指针
SnakeNode* pfood;//指向食物的指针
enum DIRECTION dir;//蛇头走的方向
enum GAME_STATUS status;//当前游戏的状态
int food_score;//食物的分数
int score;//当下的得分
int sleep_time;//程序休眠时间
}Snake;
//设置光标位置
void SetPos(short x,short y);
//创建地图
void CreatMap();
//游戏开始函数
void GameStart(Snake* ps);
//初始化贪吃蛇数据
void InitSnake(Snake* ps);
//创建食物
void CreatFood(Snake* ps);
//欢迎界面
void WelComeToGame();
//打印提示信息
void PrintInfo(Snake* ps);
//游戏进行函数
void GameRun(Snake* ps);
//暂停
void Pause();
//蛇移动
void SnakeMove(Snake* ps);
//判断下一个节点是否是食物
int NextIsFood(Snake* ps);
//吃掉食物
void EatFood(Snake* ps);
//未吃掉食物
void NoFood(Snake* ps);
//是否撞墙
int KillBYWall(Snake* ps);
//是否咬到自己
int KillBYSelf(Snake* ps);
//游戏结束
void GameEnd(Snake* ps);
snake.c文件
#include"snake.h"
//设置光标位置
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(houtput, pos);
}
//创建地图
void CreatMap()
{
setlocale(LC_ALL, "");//设置为本地模式
int i = 0;
//一个墙体占两个字节,
//y = 0 , x: 0~56
SetPos(0, 0);
for (i = 0; i < 58; i += 2)//一个墙体占两个字节
{
wprintf(L"%lc", WALL);
}
//y = 26, x: 0~56
SetPos(0,26);
for (i = 0; i < 58; i += 2)//x轴的一个单位值是一个字节
{
wprintf(L"%lc", WALL);
}
//x = 0 , y: 1~26
for (i = 1; i < 26; i++)//y轴的一个单位值是两个字节
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//x = 56, y: 1~26
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
SetPos(0, 27);
}
//初始化贪吃蛇数据
void InitSnake(Snake* ps)
{
setlocale(LC_ALL, "");//本地化
//初始化蛇身
int i = 0;
SnakeNode* pcur = NULL;
for(i = 0;i<5;i++)
{
pcur = (SnakeNode*)malloc(sizeof(SnakeNode));//申请内存空间
if (pcur == NULL)
{
perror("InitSnake():malloc");//打印错误信息
exit(1);//非正常退出
}
//设置蛇的位置坐标
pcur->x = POS_X+i*2;//一个蛇身占两个字节
pcur->y = POS_Y;
pcur->next = NULL;
if (ps->psnake == NULL)//蛇身为空
{
ps->psnake = pcur;
}
else//头插法
{
pcur->next = ps->psnake;
ps->psnake = pcur;
}
}
pcur = ps->psnake;//pcur指向蛇头
//打印蛇身
while (pcur)
{
//设置光标位置
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
//初始化其他数据
ps->dir = RIGHT;//初始状态向右走
ps->food_score = 10;//食物的分值为10
ps->score = 0;
ps->sleep_time = 200;//单位是毫秒
ps->status = NORMAL;
}
//创建食物
void CreatFood(Snake* ps)
{
setlocale(LC_ALL, "");
//1.创建食物坐标
//食物坐标范围x:2~54,y:1~25
int x = 0;
int y = 0;
//蛇身的大小是两个字节,蛇每次整体向前移动一步也是两个字节
//食物的坐标必须是2的倍数,这样蛇才能吃到食物
again:
do
{
x = rand() % 52 + 2;
y = rand() % 25 + 1;
} while (x % 2 != 0);
SnakeNode* pcur = ps->psnake;
while (pcur)//食物不能和蛇身重合
{
if (x == ps->psnake->x && y == ps->psnake->y)
{
goto again;//重新生成坐标
}
pcur = pcur->next;
}
//2.创建食物节点
SnakeNode* pf = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pf == NULL)
{
perror("CreatFood():malloc");//打印错误信息
exit(1);
}
//3.给食物节点的变量赋值
pf->x = x;
pf->y = y;
pf->next = NULL;
ps->pfood = pf;//pfood指针指向新创建的节点
//4.打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
//欢迎界面
void WelComeToGame()
{
//1.打印第一个界面
SetPos(40, 15);//设置光标位置
printf("%s", "欢迎来到贪吃蛇小游戏!");
SetPos(40, 25);//让“按任意键继续”的出现好看点
system("pause");//程序暂停
system("cls");//清理屏幕信息
//2.打印提示游戏规则的界面
SetPos(25, 12);//设置光标位置
printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
SetPos(40, 25);//让“按任意键继续”的出现好看点
system("pause");
system("cls");
}
//打印提示信息
void PrintInfo(Snake* ps)
{
SetPos(64, 11);
printf("↑ 上 .↓ 下 .← 左 .→ 右\n");
SetPos(64, 12);
printf(" F3为加速,F4为减速\n");
SetPos(64, 14);
printf("不能穿墙,不能咬到自己");
//SetPos(64, 15);
//printf("食物分值:%d,得分:%d", ps->food_score, ps->score);
SetPos(64,17);
printf("按ESC退出,按空格暂停");
SetPos(0, 27);
}
//游戏开始
void GameStart(Snake* 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();
srand((unsigned int)time(NULL));
//初始化贪吃蛇数据
InitSnake(ps);
//创建食物
CreatFood(ps);
//创建地图
CreatMap();
//打印提示信息
PrintInfo(ps);
}
//暂停
void Pause()
{
while (1)
{
Sleep(300);//休眠
if (KEY_PRESS(VK_SPACE))//再按了一次暂停键,暂停结束
{
break;
}
}
}
//判断下一个节点是否是食物
int NextIsFood(Snake* ps)
{
//蛇头的下一个位置坐标与食物坐标重合
return (ps->psnake->x == ps->pfood->x && ps->psnake->y == ps->pfood->y);
//表达式为真,返回1,为假,返回0
}
//吃掉食物
void EatFood(Snake* ps)
{
setlocale(LC_ALL, "");
SnakeNode* pcur = ps->psnake;
//打印蛇身
while (pcur)
{
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
ps->score += ps->food_score;
free(ps->pfood);
CreatFood(ps);
}
//未吃掉食物
void NoFood(Snake* ps)
{
setlocale(LC_ALL, "");
SnakeNode* pcur = ps->psnake;
//打印蛇身
while (pcur->next->next)//找到倒数第二个节点
{
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
//清理屏幕上原来的蛇尾
SetPos(pcur->next->x, pcur->next->y);
printf(" ");//在原来蛇尾的位置打印两个空格
free(pcur->next);
pcur->next = NULL;
}
//蛇移动
void SnakeMove(Snake* ps)
{
//1.创建新的节点
SnakeNode* pcur = (SnakeNode*)malloc(sizeof(SnakeNode));
if (pcur == NULL)
{
perror("SnakeMove():malloc");
exit(1);
}
//根据蛇头的位置和方向确定下一个节点的坐标
switch(ps->dir)
{
case UP://上
pcur->x = ps->psnake->x;//x轴坐标不变
pcur->y = ps->psnake->y - 1;//y轴坐标-1
break;
case DOWN://下
pcur->x = ps->psnake->x;
pcur->y = ps->psnake->y + 1;
break;
case LEFT://左
pcur->x = ps->psnake->x - 2;//x轴坐标要-2
pcur->y = ps->psnake->y;
break;
case RIGHT://右
pcur->x = ps->psnake->x + 2;
pcur->y = ps->psnake->y;
break;
}
//3.将新节点与蛇身链接起来
pcur->next = ps->psnake ;
ps->psnake = pcur;
if (NextIsFood(ps))
{
EatFood(ps);
}
else
{
NoFood(ps);
}
}
//是否撞墙
int KillBYWall(Snake* ps)
{
return (ps->psnake->x == 0 || ps->psnake->x == 56 || ps->psnake->y == 0 || ps->psnake->y == 26);
//表达式为真,返回1,为假,返回0
}
//是否咬到自己
int KillBYSelf(Snake* ps)
{
//判断蛇头是否和蛇身重叠
SnakeNode* pcur = ps->psnake->next;
while (pcur)
{
if (ps->psnake->x == pcur->x && ps->psnake->y == pcur->y)
{
return 1;
}
pcur = pcur->next;
}
return 0;//没有重合,返回0
}
//游戏进行函数
void GameRun(Snake* ps)
{
do
{
//打印提示信息
SetPos(64, 15);
printf("食物分值:%d,得分:%d", ps->food_score, ps->score);
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))//ESC键
{
ps->status = END_NORMAL;
break;
}
else if (KEY_PRESS(VK_F3))//加速键
{
if(ps->sleep_time>=80)//速度上限
{
ps->sleep_time -= 30;
ps->food_score += 2;
}
}
else if (KEY_PRESS(VK_F4))// 减速键
{
if (ps->sleep_time <= 320)//速度下限
{
ps->sleep_time += 30;
ps->food_score -= 2;//食物的分数不能小于等于0
}
}
Sleep(ps->sleep_time);
SnakeMove(ps);//蛇移动
if (KillBYWall(ps))
{
ps->status = KILL_BY_WALL;
}
else if (KillBYSelf(ps))
{
ps->status = KILL_BY_SELF;
}
} while (ps->status == NORMAL);
}
//游戏结束
void GameEnd(Snake* ps)
{
//1.打印游戏退出信息
if (ps->status == END_NORMAL)
{
SetPos(24,12);
printf("您已退出,游戏结束!");
}
else if (ps->status == KILL_BY_SELF)
{
SetPos(24, 12);
printf("咬到自己了,游戏结束!");
}
else if (ps->status == KILL_BY_WALL)
{
SetPos(24, 12);
printf("撞墙了,游戏结束!");
}
//释放蛇身的节点
SnakeNode* pcur = ps->psnake;
while (pcur)
{
SnakeNode* del = pcur;
pcur = pcur->next;
free(del);
}
ps->psnake = NULL;
//释放食物节点
free(ps->pfood);
ps->pfood = NULL;
}
test.c文件
#include"snake.h"
int main()
{
char ch;
do
{
Snake snake = { 0 };
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
SetPos(24, 15);
printf("是否再来一局?(Y/N):");
scanf("%c", &ch);
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
return 0;
}