目录
C语言贪吃蛇项目
基本功能
需要的基础内容
Win32API
介绍
控制台程序部分指令
设置控制台窗口的长宽
设置控制台的名字
控制台在屏幕上的坐标位置结构体COORD
检索指定标准设备的句柄(标准输入、标准输出或标准错误)
光标信息结构体类型CONSOLE_CURSOR_INFO
检索有关指定控制台屏幕上的光标大小和可见性的信息
指定的控制台屏幕缓冲区设置光标的大小和可见性
设置指定控制台屏幕缓冲区中的光标位置
获取按键状态
宽字符打印与本地化
宽字符
头文件locale.h与本地化
类项
设置本地化
宽字符的打印
贪吃蛇项目设计
游戏流程设计
贪吃蛇界面的打印
打印游戏欢迎界面
打印游戏规则界面
打印游戏地图界面
打印游戏地图右侧的帮助信息
贪吃蛇结构体设计
贪吃蛇进行过程设计
初始化蛇身以及游戏信息
初始化食物
游戏进行部分
贪吃蛇运行结束
贪吃蛇项目实现
使用C语言在Windows环境的控制台中模拟实现贪吃蛇
函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API
Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programming Interface,简称API函数。WIN32API
也就是Microsoft Windows 32位平台的应用程序编程接口
mode con cols=100 lines=30
后按Enter执行(cols
表示列,lines
表示行)如图所示:
参考mode
命令文档:mode | Microsoft Learn
title (需要的名字)
后按Enter执行参考title
命令文档:title | Microsoft Learn
可以在C语言中使用system
函数执行上述命令
使用system
函数需要包含头文件stdlib.h
#define _CRT_SECURE_NO_WARNINGS 1
#include //使用system函数需要包含头文件stdlib.h
#include
int main()
{
//设置控制台窗口的长宽:mode con cols=100 lines=30
system("mode con cols=100 lines=30");//设置列为100列,行为30行
//设置控制台窗口的名称:title 贪吃蛇
system("title 贪吃蛇");
getchar();//防止程序提前自动结束
//等价于使用
system("pause");
return 0;
}
COORD
COORD
是WindowsAPI
中定义的⼀个结构体,表示一个字符在控制台屏幕上显示时的坐标,坐标系(0, 0)的原点位于屏幕的顶部左侧单元格
//COORD结构体原型
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;//对_COORD结构体进行重命名为COORD,以及对_COORD结构体指针重命名为*PCOORD
参考COORD
文档:COORD 结构 - Windows Console | Microsoft Learn
使用COORD
设置字符显示坐标:
使用COORD结构体需要包含头文件windows.h
COORD pos = {10, 15};//将x轴坐标设置为10,y轴坐标设置为15
使用GetStdHandle
函数进行获取。
GetStdHandle
是⼀个WindowsAPI
函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标
准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备
参考GetStdHandle
文档:GetStdHandle 函数 - Windows Console | Microsoft Learn
//函数原型
HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);
//参数说明
nStdHandle [in]标准设备。 此参数的取值可为下列值之一
STD_INPUT_HANDLE((DWORD)-10) 标准输入设备。 最初,这是输入缓冲区 CONIN$ 的控制台
STD_OUTPUT_HANDLE((DWORD)-11) 标准输出设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$
STD_ERROR_HANDLE((DWORD)-12) 标准错误设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$
返回类型
如果函数执行成功,则返回值为指定设备的句柄
如果函数执行失败,则返回值为 INVALID_HANDLE_VALUE
如果应用程序没有关联的标准句柄(例如在交互式桌面上运行的服务),并且尚未重定向这些句柄,则返回值为 NULL
使用GetStdHandle
函数需要包含头文件windows.h
代码实例:
HANDLE hOutput = NULL;//hOutput相当于一个指针类型的变量
//获取标准输出的句柄(用来标识不同的标准输出设备的数值) ,即需要操作的控制台程序
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
对于句柄的简单理解:需要操作(按键响应等)控制台程序时,首先得获取到控制台这个应用,而句柄就是帮助获取控制台应用的一个“把手”
CONSOLE_CURSOR_INFO
//光标信息结构体原型
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;//光标占比大小
BOOL bVisible;//光标是否可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;//将结构体类型重命名为CONSOLE_CURSOR_INFO,并将结构体指针类型重命名为*PCONSOLE_CURSOR_INFO
dwSize
由游标填充的字符单元的百分比。 该值介于 1 到 100 之间。 游标外观各不相同,范围从完全填充单元到显示为单元底部的横线。
bVisible
光标的可见性。 如果光标可见,则此成员为 TRUE,默认为可见
参考CONSOLE_CURSOR_INFO
结构体类型文档:CONSOLE_CURSOR_INFO 结构 - Windows Console | Microsoft Learn
使用CONSOLE_CURSOR_INFO
需要包含头文件windows.h
代码实例
CONSOLE_CURSOR_INFO CursorInfo.bVisible = false;//隐藏光标
使用函数GetConsoleCursorInfo
进行获取
//函数原型
BOOL WINAPI GetConsoleCursorInfo(_In_ HANDLE hConsoleOutput,
_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
//参数说明
第一个参数hConsoleOutput [in],获取指定控制台屏幕缓冲区的句柄。可以通过GetStdHandle函数获取
第二个参数lpConsoleCursorInfo [out],指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台光标的信息
返回类型
如果该函数成功,则返回值为非零值
如果函数失败,则返回值为零
参考GetConsoleCursorInfo
函数文档:GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
使用GetConsoleCursorInfo
函数需要包含头文件windows.h
代码实例
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;//使用光标结构体类型定义变量
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
使用SetConsoleCursorInfo
进行设置
//函数原型
BOOL WINAPI SetConsoleCursorInfo(
_In_ HANDLE hConsoleOutput,
_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
//参数说明
第一个参数hConsoleOutput [in],控制台屏幕缓冲区的句柄
第二个参数pConsoleCursorInfo [in],指向 CONSOLE_CURSOR_INFO 结构的指针,该结构为控制台屏幕缓冲区的光标提供新的参数
//返回类型
如果该函数成功,则返回值为非零值
如果函数失败,则返回值为零
参考SetConsoleCursorInfo
函数文档:SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
使用SetConsoleCursorInfo
函数需要包含windows.h
代码实例
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
使用函数SetConsoleCursorPosition
进行设置
//函数原型
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);
//参数说明
第一个参数为hConsoleOutput [in],获取控制台屏幕缓冲区的句柄
第二个参数为dwCursorPosition [in],指定新光标位置(以字符为单位)的 COORD 结构。 坐标是屏幕缓冲区字符单元的列和行。 坐标必须位于控制台屏幕缓冲区的边界以内
//返回类型
如果该函数成功,则返回值为非零值
如果函数失败,则返回值为零
参考SetConsoleCursorPosition
文档:SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn
代码实例
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
使用GetAsyncKeyState
函数获取
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState
的返回值是short
类型,在上⼀次调用 GetAsyncKeyState
函数后,如果返回的16位的short
数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0
//函数原型
SHORT GetAsyncKeyState([in] int vKey);
//参数说明
[in] vKey,类型: int,虚拟密钥代码。
参考GetAsyncKeyState
函数文档:getAsyncKeyState 函数 (winuser.h) - Win32 apps | Microsoft Learn
参考虚拟密钥代码文档:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
使用GetAsyncKeyState
函数需要包含头文件windows.h
在游戏地图上,打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符(即窄字符)是占1个字节的,而宽字符是占用2个字节
出现窄字符和宽字符区别的原因:
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx,故ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,例如,在法语中,字母上方有注音符号,它就无法用基本的ASCII码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,不同的国家有不同的字母,因此,尽管都使用256个符号的编码方式,代表的字母却不⼀样。例如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel,在俄语编码中代表另⼀个符号。但是所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这⼀段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示256x256=65536个符号
为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。例如:加入宽字符的类型wchar_t
和宽字符的输入wscanf()
和输出函数wprintf()
,并且使用时需要包含头文件locale.h
,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数
locale.h
与本地化locale.h
提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准可以中,依赖地区的部分有以下几项:
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
类项 |
影响内容 |
LC_ALL |
以下列出的所有类别 |
LC_COLLATE |
|
LC_CTYPE |
字符处理函数(不受影响的 |
LC_MONETARY |
|
LC_NUMERIC |
格式化输出例程(例如 |
LC_TIME |
|
使用函数setlocale
函数进行设置
setlocale
函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项
//函数原型
char *setlocale(int category, const char *locale);
//参数说明
第一个参数为category,表示受区域设置影响的分类。
第二个参数为locale,表示区域设置说明符
//返回类型
如果提供了有效的locale和category,函数会返回指向与指定的locale和category关联的字符串的指针。 如果locale参数为NULL,函数会返回当前区域设置。
如果将无效参数传递给任一函数,则返回值为NULL。
setlocale
的第一个参数可以是前面说明的类项中的某一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL
,就会影响所有的类项
C标准给第⼆个参数仅定义了2种可能取值:"C"(即NULL)和""
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");//当地区设置为"C"时,库函数按正常方式执行
需要改变地区时,就只能显示调用setlocale函数。将" "作为第2个参数,调用setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。
例如:切换到我们的本地模式后则支持宽字符(汉字)的输出等
setlocale(LC_ALL, "");//切换到本地环境
函数参考文档:setlocale | Microsoft Learn
代码实例
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int main()
{
printf("当前环境为:%s\n", setlocale(LC_ALL, NULL));
printf("设置后的本地环境为:%s\n", setlocale(LC_ALL, ""));
return 0;
}
输出结果:
当前环境为:C
设置后的本地环境为:Chinese (Simplified)_China.936
在C语言中,宽字符的字面量前必须加上前缀L
,否则C语言会把字面量当作窄字符类型处理。前缀L
在单引号前面,表示宽字符。
宽字符的打印使用函数wprintf()
,对应wprintf()
的占位符为%lc
;打印宽字符串时,对应的占位符为%ls
并且在双引号前,需要加上前缀L
代码实例
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int main()
{
printf("设置后的本地环境为:%s\n", setlocale(LC_ALL, ""));
wchar_t c = L'中';
wchar_t c1 = L'国';
wchar_t c2 = L'■';
wchar_t c3 = L'●';
wchar_t c4 = L'★';
char* string = "ab";
wprintf(L"%lc\n", c);
wprintf(L"%lc\n", c1);
printf("%s\n", string);
wprintf(L"%lc\n", c2);
wprintf(L"%lc\n", c3);
wprintf(L"%lc\n", c4);
return 0;
}
输出结果如下图:
普通字符和宽字符打印的宽度展示如下:
游戏开始界面只需要打印文字,但是需要确定文字的打印位置
void HideCursor();
,防止出现打印完成后光标闪烁问题mode
命令和title
命令void SetPos(int x, int y);
void Welcome();
pause
指令显示“请按任意键继续...”,亦可防止程序提前结束cls
指令清除当前页面打印的内容,方便打印下一界面,实现页面跳转的效果代码示例
//隐藏光标
void HideCursor()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标
CONSOLE_CURSOR_INFO cursor = { 0 };
//先获取光标信息
GetConsoleCursorInfo(hOutput, &cursor);
cursor.bVisible = false;//设置光标为不可见
//再传入新的光标信息
SetConsoleCursorInfo(hOutput, &cursor);
}
//设置光标位置
void SetPos(int x, int y)
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(hOutput, pos);
}
//欢迎界面
void Welcome()
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//定位光标
SetPos(33, 12);
printf("欢迎来到贪吃蛇的世界,尽情游玩吧!");
SetPos(40, 28);
system("pause");
system("cls");
}
游戏规则界面同样只是打印基本文字信息
SetPos
函数在每一次打印过程中设置光标位置void Rules();
pause
指令显示“请按任意键继续...”,亦可防止程序提前结束cls
指令清除当前页面打印的内容,方便打印下一界面,实现页面跳转的效果注意设置光标隐藏函数只需要在程序运行起来后执行一次即可,故在游戏界面打印部分只需要调用一次,不需要放置到Welcome()
函数内
代码示例
//游戏规则介绍界面
void Rules()
{
SetPos(45, 10);
printf("游戏规则");
SetPos(35, 12);
printf("使用WASD或↑↓←→键操作蛇的移动");
SetPos(30, 13);//重新设置坐标位置防止出现文字打印覆盖
printf("按下空格即可加速,按下左alt或右alt取消加速");
SetPos(22, 14);
printf("加速可以加分,加速一次+2分,最高加8分,取消加速-2分,直到减到初始值");
SetPos(40, 28);
system("pause");
system("cls");
}
游戏地图的打印依旧是纯打印字符,但是需要注意打印的是宽字符
参考下图
void Map();
,在该函数中调用SetPos
函数设置每次打印的位置代码示例
//打印游戏地图界面
void Map()
{
//设置地区
setlocale(LC_ALL, "");
//打印地图上部分边界
SetPos(0, 0);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", MAP);
}
//打印地图下部分边界
SetPos(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", MAP);
}
//打印地图左部分边界
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", MAP);
}
//打印地图右部分边界
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);//注意此处是56而不是28,因为宽字符横向占两个宽度
wprintf(L"%lc", MAP);
}
getchar();
}
帮助信息依旧是纯打印文字,只需要设置好打印位置即可
void Help();
//打印游戏地图右侧帮助信息
void Help()
{
SetPos(60, 20);
printf("不能穿墙,不能咬到自己");
SetPos(60, 21);
printf("用WASD或↑↓←→控制蛇的移动方向");
SetPos(60, 22);
printf("按下空格进入加速,按下左alt或右alt取消加速");
SetPos(60, 23);
printf("使用ESC退出游戏,使用P暂停游戏");
SetPos(60, 26);
printf("Copy Right@EPSDA");
}
需要进行设计的结构体类型有:
设计时需要注意,蛇吃掉食物,食物变成自身节点,这一过程相当于食物也是蛇身节点类型
//枚举游戏状态
enum state
{
Normal = 1,//正常状态
Error,//错误状态
KillBySelf,//自己咬到自己
KillbByWall//撞墙
};
//枚举蛇的方向
enum dir
{
Up = 1,
Down,
Left,
Right
};
//定义蛇身节点
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}Snode;
//定义游戏信息结构体
typedef struct GameInfo
{
Snode* pSnake;//指向蛇头的指针
Snode* pFood;//指向食物的指针
int FoodWeight;//一个食物的得分
int score;//当前总得分
enum dir direct;//蛇的移动方向,默认向右
enum state Srun;//游戏状态
int sleepTime;//蛇的移动速度,睡眠时间越短,蛇越快,默认为200
}Sinfo;
//初始化蛇身以及除食物以外的游戏信息
void Init(Sinfo* ps)
{
//确保有蛇对象
assert(ps);
//优先蛇身的尾结点
//通过头插实现蛇身整体
Snode* cur = NULL;
for (int i = 0; i <= 4; i++)
{
//创建蛇节点
cur = (Snode*)malloc(sizeof(Snode));
assert(cur);
cur->x = 24 + 2 * i;
cur->y = 5;
cur->next = NULL;
//将节点连起来
if (ps->pSnake == NULL)
{
ps->pSnake = cur;//使游戏信息中维护蛇身的指针指向蛇尾
}
else
{
cur->next = ps->pSnake;
ps->pSnake = cur;
}
}
//打印蛇(遍历链表)
//使用指向头结点的ps->pSnake指针
Snode* pcur = ps->pSnake;
setlocale(LC_ALL, "");
while (pcur)
{
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
//初始化其他信息
ps->FoodWeight = 10;
ps->sleepTime = 200;
ps->score = 0;
ps->Srun = Normal;
ps->direct = Right;
}
//初始化食物
//食物本质也是蛇身,所以依旧可以使用蛇身结构体
void FoodInit(Sinfo* pf)
{
assert(pf);
//随机生成食物
Snode* food = (Snode*)malloc(sizeof(Snode));
assert(food);
pf->pFood = food;
//设置食物坐标
//但是食物坐标不能与蛇坐标重合
create:
do
{
//确保食物坐标不会超出地图范围
pf->pFood->x = rand() % 53 + 2;
pf->pFood->y = rand() % 24 + 1;
} while (pf->pFood->x % 2 != 0);//确保食物的x坐标为2的倍数
//确保食物所在位置不是蛇的位置,并且蛇身每个节点都需要比较
Snode* cur = pf->pSnake;
while (cur)
{
if (pf->pFood->x == cur->x && pf->pFood->y == cur->y)
{
goto create;
}
cur = cur->next;
}
//打印食物
setlocale(LC_ALL, "");
SetPos(pf->pFood->x, pf->pFood->y);
wprintf(L"%lc", FOOD);
}
//判断下一个坐标是不是食物
bool NextIsFood(Sinfo* snake, Snode* pfood)
{
assert(snake);
assert(pfood);
if (snake->pSnake->x == snake->pFood->x && snake->pSnake->y == snake->pFood->y)
{
return true;
}
else
{
return false;
}
}
//吃食物
void EatFood(Sinfo* snake, Snode* pfood)
{
assert(snake);
assert(pfood);
//是食物进行头插
pfood->next = snake->pSnake;
snake->pSnake = pfood;
//打印新蛇身
Snode* pcur = snake->pSnake;
setlocale(LC_ALL, "");
while (pcur)
{
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
snake->score += snake->FoodWeight;
//释放原来旧的食物
free(snake->pFood);
//重新创建食物
FoodInit(snake);
}
//不是食物则正常移动
void NotEatFood(Sinfo* snake, Snode* pnext)
{
assert(snake);
assert(pnext);
pnext->next = snake->pSnake;
snake->pSnake = pnext;
//不是食物时,蛇移动不会增加身体的长度
Snode* pcur = snake->pSnake;
setlocale(LC_ALL, "");
while (pcur->next->next != NULL)
{
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 Kill_Self(Sinfo* snake)
{
assert(snake);
//不可撞到自身相当于不可以撞到自身的第二个之后的所有节点
Snode* pcur = snake->pSnake->next;
while (pcur)
{
if (pcur->x == snake->pSnake->x && pcur->y == snake->pSnake->y)
{
snake->Srun = KillBySelf;
return;
}
pcur = pcur->next;
}
}
//撞到墙
void Kill_Wall(Sinfo* snake)
{
assert(snake);
//不可以撞到墙相当于头结点不可以等于墙的坐标
Snode* pcur = snake->pSnake;
if (pcur->x == 0 || pcur->x == 56 || pcur->y == 0 || pcur->y == 26)
{
snake->Srun = KillByWall;
return;
}
}
//蛇的移动
void Move(Sinfo* snake)
{
//默认移动方向向右
assert(snake);
//创建新节点用于移动时存储新坐标
Snode* psnake = (Snode*)malloc(sizeof(Snode));
assert(psnake);
psnake->next = NULL;
switch (snake->direct)
{
case Up:
psnake->x = snake->pSnake->x;
psnake->y = snake->pSnake->y - 1;
break;
case Down:
psnake->x = snake->pSnake->x;
psnake->y = snake->pSnake->y + 1;
break;
case Left:
psnake->x = snake->pSnake->x - 2;
psnake->y = snake->pSnake->y;
break;
case Right:
psnake->x = snake->pSnake->x + 2;
psnake->y = snake->pSnake->y;
break;
default:
break;
}
//移动过程中是否遇到食物
if (NextIsFood(snake, psnake))//遇到食物
{
EatFood(snake, psnake);
}
else
{
NotEatFood(snake, psnake);
}
Kill_Self(snake);
Kill_Wall(snake);
}
//游戏暂停
void SleepStatus()
{
while (1)
{
//循环睡眠
Sleep(200);
//同时检测按键
if (KEYPRESSED(0x50))
{
break;
}
}
}
//游戏过程部分
void GameRun(Sinfo* snake)
{
//读取键盘输入控制蛇的移动
do
{
//打印得分情况
SetPos(60, 10);
printf("一个食物%02d分", snake->FoodWeight);
SetPos(60, 11);
printf("当前得分:%02d", snake->score);
//向上走,原方向不能向下
if (KEYPRESSED(VK_UP) && snake->direct != Down)
{
snake->direct = Up;
}
//向下走,原方向不能向上
else if (KEYPRESSED(VK_DOWN) && snake->direct != Up)
{
snake->direct = Down;
}
//向左走,原方向不能向右
else if (KEYPRESSED(VK_LEFT) && snake->direct != Right)
{
snake->direct = Left;
}
//向右走,原方向不能向左
else if (KEYPRESSED(VK_RIGHT) && snake->direct != Left)
{
snake->direct = Right;
}
else if (KEYPRESSED(VK_ESCAPE))
{
snake->Srun = Quit;
break;
}
else if (KEYPRESSED(0x50))
{
SleepStatus();//暂停游戏
}
else if (KEYPRESSED(VK_SPACE))
{
//加速,最多加到320
if (snake->sleepTime < 320)
{
snake->sleepTime += 30;
snake->FoodWeight += 2;
}
}
else if (KEYPRESSED(VK_LMENU) || KEYPRESSED(VK_RMENU))
{
//减速
if (snake->sleepTime > 200)
{
snake->sleepTime -= 30;
snake->FoodWeight -= 2;
}
}
//蛇的移动
Move(snake);
Sleep(snake->sleepTime);
} while (snake->Srun == Normal);
}
//游戏结束
void GameEnd(Sinfo* snake)
{
assert(snake);
//释放开辟过的空间
SetPos(15, 12);
switch (snake->Srun)
{
case Quit:
printf("游戏退出");
break;
case KillByWall:
printf("墙可不是食物嗷(头部出现大包)");
break;
case KillBySelf:
printf("没有闹饥荒嗷,自己不好吃");
break;
}
while (snake->pSnake)
{
Snode* BlockToFree = snake->pSnake;
snake->pSnake = snake->pSnake->next;
free(BlockToFree);
}
free(snake->pFood);
snake = NULL;
}
//测试文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake_game.h"
void gameTest()
{
int ch = 0;
do
{
//游戏开始
GameStart();
//游戏进行
srand((unsigned int)time(NULL));
Sinfo snake = { 0 };
Init(&snake);
//初始化蛇身以及除食物以外的游戏信息
//初始化食物
FoodInit(&snake);
GameRun(&snake);
//游戏结束
GameEnd(&snake);
SetPos(15, 10);
printf("还要继续嘛?(Y/N)");
ch = getchar();
getchar();//清理缓冲区的\n
} while (ch == 'Y' || ch == 'y');
}
int main()
{
gameTest();
SetPos(0, 28);
return 0;
}
//头文件
#pragma once
#include
#include
#include
#include
#include
#include
#include
//定义宏检测按键是否被按过
#define KEYPRESSED(vk) (GetAsyncKeyState(vk) & 0x1 ? 1 : 0)
//定义蛇身图案
#define BODY L'●'
//定义食物图案
#define FOOD L'★'
//定义地图图案
#define MAP L'□'
//枚举游戏状态
enum state
{
Normal = 1,//正常状态
Quit,//退出状态
KillBySelf,//自己咬到自己
KillByWall//撞墙
};
//枚举蛇的方向
enum dir
{
Up = 1,
Down,
Left,
Right
};
//定义蛇身节点
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}Snode;
//定义游戏信息结构体
typedef struct GameInfo
{
Snode* pSnake;//指向蛇头的指针
Snode* pFood;//指向食物的指针
int FoodWeight;//一个食物的得分
int score;//当前总得分
enum dir direct;//蛇的移动方向,默认向右
enum state Srun;//游戏状态
int sleepTime;//蛇的移动速度,睡眠时间越短,蛇越快,默认为200
}Sinfo;
//游戏开始部分
void GameStart();
//隐藏光标
void HideCursor();
//设置光标位置决定打印内容位置
void SetPos(int x, int y);
//打印欢迎界面
void Welcome();
//打印游戏规则介绍界面
void Rules();
//打印游戏地图界面
void Map();
//打印游戏地图右侧帮助信息
void Help();
//游戏进行
void GameRun(Sinfo* snake);
//初始化蛇身以及游戏信息
void Init(Sinfo* ps);
//初始化食物
void FoodInit(Sinfo* pf);
//判断下一个坐标是不是食物
bool NextIsFood(Sinfo* snake, Snode* pfood);
//吃食物
void EatFood(Sinfo* snake, Snode* pfood);
//不是食物则正常移动
void NotEatFood(Sinfo* snake, Snode* pfood);
//游戏暂停
void SleepStatus();
//蛇的移动
void Move(Sinfo* snake);
//撞到自身
void Kill_Self(Sinfo* snake);
//撞到墙
void Kill_Wall(Sinfo* snake);
//游戏结束
void GameEnd(Sinfo* snake);
//项目文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "Snake_game.h"
//隐藏光标
void HideCursor()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标
CONSOLE_CURSOR_INFO cursor = { 0 };
//先获取光标信息
GetConsoleCursorInfo(hOutput, &cursor);
cursor.bVisible = false;//设置光标为不可见
//再传入新的光标信息
SetConsoleCursorInfo(hOutput, &cursor);
}
//设置光标位置
void SetPos(int x, int y)
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x, y };
SetConsoleCursorPosition(hOutput, pos);
}
//欢迎界面
void Welcome()
{
system("mode con cols=105 lines=30");
system("title 贪吃蛇");
//定位光标
SetPos(33, 12);
printf("欢迎来到贪吃蛇的世界,尽情游玩吧!");
SetPos(40, 28);
system("pause");
system("cls");
}
//游戏规则介绍界面
void Rules()
{
SetPos(45, 10);
printf("游戏规则");
SetPos(35, 12);
printf("使用↑↓←→键操作蛇的移动");
SetPos(30, 13);//重新设置坐标位置防止出现文字打印覆盖
printf("按下空格即可加速,按下左alt或右alt取消加速");
SetPos(22, 14);
printf("加速可以加分,加速一次+2分,最高加8分,取消加速-2分,直到减到初始值");
SetPos(40, 28);
system("pause");
system("cls");
}
//打印游戏地图界面
void Map()
{
//设置地区
setlocale(LC_ALL, "");
//打印地图上部分边界
SetPos(0, 0);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", MAP);
}
//打印地图下部分边界
SetPos(0, 26);
for (int i = 0; i < 29; i++)
{
wprintf(L"%lc", MAP);
}
//打印地图左部分边界
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", MAP);
}
//打印地图右部分边界
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);//注意此处是56而不是28,因为宽字符横向占两个宽度
wprintf(L"%lc", MAP);
}
}
//打印游戏地图右侧帮助信息
void Help()
{
SetPos(60, 20);
printf("不能穿墙,不能咬到自己");
SetPos(60, 21);
printf("用↑↓←→控制蛇的移动方向");
SetPos(60, 22);
printf("按下空格进入加速,按下左alt或右alt取消加速");
SetPos(60, 23);
printf("使用ESC退出游戏,使用P暂停游戏");
SetPos(60, 26);
printf("Copy Right@EPSDA");
}
//游戏开始部分
void GameStart()
{
//隐藏光标
HideCursor();
//打印欢迎界面
Welcome();
//打印游戏规则界面
Rules();
//打印游戏地图界面
Map();
//打印游戏地图右侧帮助信息
Help();
}
//初始化蛇身以及除食物以外的游戏信息
void Init(Sinfo* ps)
{
//确保有蛇对象
assert(ps);
//优先蛇身的尾结点
//通过头插实现蛇身整体
Snode* cur = NULL;
for (int i = 0; i <= 4; i++)
{
//创建蛇节点
cur = (Snode*)malloc(sizeof(Snode));
assert(cur);
cur->x = 24 + 2 * i;
cur->y = 5;
cur->next = NULL;
//将节点连起来
if (ps->pSnake == NULL)
{
ps->pSnake = cur;//使游戏信息中维护蛇身的指针指向蛇尾
}
else
{
cur->next = ps->pSnake;
ps->pSnake = cur;
}
}
//打印蛇(遍历链表)
//使用指向头结点的ps->pSnake指针
Snode* pcur = ps->pSnake;
setlocale(LC_ALL, "");
while (pcur)
{
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
//初始化其他信息
ps->FoodWeight = 10;
ps->sleepTime = 200;
ps->score = 0;
ps->Srun = Normal;
ps->direct = Right;
}
//初始化食物
//食物本质也是蛇身,所以依旧可以使用蛇身结构体
void FoodInit(Sinfo* pf)
{
assert(pf);
//随机生成食物
Snode* food = (Snode*)malloc(sizeof(Snode));
assert(food);
pf->pFood = food;
//设置食物坐标
//但是食物坐标不能与蛇坐标重合
create:
do
{
//确保食物坐标不会超出地图范围
pf->pFood->x = rand() % 53 + 2;
pf->pFood->y = rand() % 24 + 1;
} while (pf->pFood->x % 2 != 0);//确保食物的x坐标为2的倍数
//确保食物所在位置不是蛇的位置,并且蛇身每个节点都需要比较
Snode* cur = pf->pSnake;
while (cur)
{
if (pf->pFood->x == cur->x && pf->pFood->y == cur->y)
{
goto create;
}
cur = cur->next;
}
//打印食物
setlocale(LC_ALL, "");
SetPos(pf->pFood->x, pf->pFood->y);
wprintf(L"%lc", FOOD);
}
//判断下一个坐标是不是食物
bool NextIsFood(Sinfo* snake, Snode* pfood)
{
assert(snake);
assert(pfood);
if (snake->pSnake->x == snake->pFood->x && snake->pSnake->y == snake->pFood->y)
{
return true;
}
else
{
return false;
}
}
//吃食物
void EatFood(Sinfo* snake, Snode* pfood)
{
assert(snake);
assert(pfood);
//是食物进行头插
pfood->next = snake->pSnake;
snake->pSnake = pfood;
//打印新蛇身
Snode* pcur = snake->pSnake;
setlocale(LC_ALL, "");
while (pcur)
{
SetPos(pcur->x, pcur->y);
wprintf(L"%lc", BODY);
pcur = pcur->next;
}
snake->score += snake->FoodWeight;
//释放原来旧的食物
free(snake->pFood);
//重新创建食物
FoodInit(snake);
}
//不是食物则正常移动
void NotEatFood(Sinfo* snake, Snode* pnext)
{
assert(snake);
assert(pnext);
pnext->next = snake->pSnake;
snake->pSnake = pnext;
//不是食物时,蛇移动不会增加身体的长度
Snode* pcur = snake->pSnake;
setlocale(LC_ALL, "");
while (pcur->next->next != NULL)
{
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 Kill_Self(Sinfo* snake)
{
assert(snake);
//不可撞到自身相当于不可以撞到自身的第二个之后的所有节点
Snode* pcur = snake->pSnake->next;
while (pcur)
{
if (pcur->x == snake->pSnake->x && pcur->y == snake->pSnake->y)
{
snake->Srun = KillBySelf;
return;
}
pcur = pcur->next;
}
}
//撞到墙
void Kill_Wall(Sinfo* snake)
{
assert(snake);
//不可以撞到墙相当于头结点不可以等于墙的坐标
Snode* pcur = snake->pSnake;
if (pcur->x == 0 || pcur->x == 56 || pcur->y == 0 || pcur->y == 26)
{
snake->Srun = KillByWall;
return;
}
}
//蛇的移动
void Move(Sinfo* snake)
{
//默认移动方向向右
assert(snake);
//创建新节点用于移动时存储新坐标
Snode* psnake = (Snode*)malloc(sizeof(Snode));
assert(psnake);
psnake->next = NULL;
switch (snake->direct)
{
case Up:
psnake->x = snake->pSnake->x;
psnake->y = snake->pSnake->y - 1;
break;
case Down:
psnake->x = snake->pSnake->x;
psnake->y = snake->pSnake->y + 1;
break;
case Left:
psnake->x = snake->pSnake->x - 2;
psnake->y = snake->pSnake->y;
break;
case Right:
psnake->x = snake->pSnake->x + 2;
psnake->y = snake->pSnake->y;
break;
default:
break;
}
//移动过程中是否遇到食物
if (NextIsFood(snake, psnake))//遇到食物
{
EatFood(snake, psnake);
}
else
{
NotEatFood(snake, psnake);
}
Kill_Self(snake);
Kill_Wall(snake);
}
//游戏暂停
void SleepStatus()
{
while (1)
{
//循环睡眠
Sleep(200);
//同时检测按键
if (KEYPRESSED(0x50))
{
break;
}
}
}
//游戏过程部分
void GameRun(Sinfo* snake)
{
//读取键盘输入控制蛇的移动
do
{
//打印得分情况
SetPos(60, 10);
printf("一个食物%02d分", snake->FoodWeight);
SetPos(60, 11);
printf("当前得分:%02d", snake->score);
//向上走,原方向不能向下
if (KEYPRESSED(VK_UP) && snake->direct != Down)
{
snake->direct = Up;
}
//向下走,原方向不能向上
else if (KEYPRESSED(VK_DOWN) && snake->direct != Up)
{
snake->direct = Down;
}
//向左走,原方向不能向右
else if (KEYPRESSED(VK_LEFT) && snake->direct != Right)
{
snake->direct = Left;
}
//向右走,原方向不能向左
else if (KEYPRESSED(VK_RIGHT) && snake->direct != Left)
{
snake->direct = Right;
}
else if (KEYPRESSED(VK_ESCAPE))
{
snake->Srun = Quit;
break;
}
else if (KEYPRESSED(0x50))
{
SleepStatus();//暂停游戏
}
else if (KEYPRESSED(VK_SPACE))
{
//加速,最多加到320
if (snake->sleepTime < 320)
{
snake->sleepTime += 30;
snake->FoodWeight += 2;
}
}
else if (KEYPRESSED(VK_LMENU) || KEYPRESSED(VK_RMENU))
{
//减速
if (snake->sleepTime > 200)
{
snake->sleepTime -= 30;
snake->FoodWeight -= 2;
}
}
//蛇的移动
Move(snake);
Sleep(snake->sleepTime);
} while (snake->Srun == Normal);
}
//游戏结束
void GameEnd(Sinfo* snake)
{
assert(snake);
//释放开辟过的空间
SetPos(15, 12);
switch (snake->Srun)
{
case Quit:
printf("游戏退出");
break;
case KillByWall:
printf("墙可不是食物嗷(头部出现大包)");
break;
case KillBySelf:
printf("没有闹饥荒嗷,自己不好吃");
break;
}
while (snake->pSnake)
{
Snode* BlockToFree = snake->pSnake;
snake->pSnake = snake->pSnake->next;
free(BlockToFree);
}
free(snake->pFood);
snake = NULL;
}