文章目录
- 一、贪吃蛇项目需要实现的基本功能
- [二、Win32 API介绍](#二、Win32 API介绍)
-
- [2.1 控制台](#2.1 控制台)
- [2.2 部分控制台命令及调用函数](#2.2 部分控制台命令及调用函数)
-
- [mode 和 title 命令](#mode 和 title 命令)
- [COORD 命令](#COORD 命令)
- GetStdHandle(获取数据)
- GetConsoleCursorInfo(获取光标数据)
- [SetConsoleCursorInfo (设置光标属性)](#SetConsoleCursorInfo (设置光标属性))
- GetAsyncKeyState(获取按键情况)
- 三、贪吃蛇项目游戏设计和分析
-
- [3.1 本地化中宽字符的概念及应用](#3.1 本地化中宽字符的概念及应用)
- [3.2 蛇身及食物等初始化结构设计](#3.2 蛇身及食物等初始化结构设计)
- [3.3 游戏流程设计](#3.3 游戏流程设计)
一、贪吃蛇项目需要实现的基本功能
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
二、Win32 API介绍
本次实现贪吃蛇会使用到的一些Win32 API知识,接下来我们就学习一下。
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称AP|函数。WIN32API也就是Microsoft Windows32位平台的应用程序编程接口。
2.1 控制台
我们这次的贪吃蛇项目需要用到控制台来运行,所以在什么空项目调用时应该出现的时调试控制台,就像下图这样
如果出现的不是 调试控制台 ,就右键调试栏,选择 属性 后找到终端,将 默认终端应用程序 改成 让Windows决定 然后确定,重新调试就行了
2.2 部分控制台命令及调用函数
mode 和 title 命令
我们平常调试出来的黑框其实就是控制台,对于这个控制台我们可以改变长宽,也可以改变名字,这就要用到 mode 和 title 命令,但是在调试时我们需要借用 system 来调用它们
c
//system 函数可以用来执行系统命令
int main()
{
设置控制台相关属性
system("mode con cols=100 lines=30");//cols-行,lines-列
system("title 贪吃蛇");
//getchar(); 停止运行
system("pause");//程序暂停
return 0;
}
这里最后需要放上 getchar() 或 system("pause") 来暂停程序,否则因为程序运行好就停止了,无法向控制台一样,直接更改控制台名字。调试结果如下
COORD 命令
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。关于控制台的坐标轴大家可以看成下图
这是 COORD 命令的声明,里面包含了坐标的x和y坐标,需要注意的是这里调用需要添加头文件 <Windows.h>
c
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, * PCOORD;
GetStdHandle(获取数据)
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
c
//函数声明
HANDLE GetStdHandle(DWORD nStdHandle);
利用这个调用函数,我们可以设定某点的坐标,比如这样
c
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
return 0;
}
我们观察 HANDlE ,发现它是重命名 void * 的一个指针变量,所以当我们不打算给某点一个确定的坐标值时,可以将它设为 NULL
GetConsoleCursorInfo(获取光标数据)
GetConsoleCursorInfo 是用来检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
c
//声明
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
//PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
关于 CONSOLE_CURSOR_INFO ,这个结构体,包含有关控制台光标的信息,它里面的内容是这样
c
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, * PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
- bVisible,游标的可见性。如果光标可见,则此成员为 TRUE。我们也可以设置为 FALSE 让它变得不可见
举个例子,这样我们就可以获取和houtput句柄相关的控制台上的光标信息,存放在 cursor_info 中了
c
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
return 0;
}
SetConsoleCursorInfo (设置光标属性)
SetConsoleCursorInfo 可以设置指定控制台屏幕缓冲区的光标的大小和可见性。
c
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo
);
比如我们将光标的填充变成100(默认为25)
c
int main()
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };
//获取和houtput句柄相关的控制台上的光标信息,存放在cursor_info中
GetConsoleCursorInfo(houtput, &cursor_info);
//修改光标占比
cursor_info.dwSize = 100;
//设置和houtput句柄相关的控制台上的光标信息
SetConsoleCursorInfo(houtput, &cursor_info);
system("pause");
return 0;
}
我们可以发现确实发生了变化
既然这样,我们就可以设计一个封装方法来改变光标的位置,就不用每次改变光标位置时都写这么都行了
c
void set_pos(int x, int y)
{
//获得标准输出的设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
int main()
{
set_pos(10, 20);
printf("hehe\n");
set_pos(10, 10);
printf("hehe\n");
return 0;
}
GetAsyncKeyState(获取按键情况)
它将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
关于虚拟键码,大家可以点击这个链接查看 link:虚拟键码
GetAsyncKeystate 的返回值是short类型,在上一次调用 GetAsyncKeystate 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。即将它与1按位与得出结果
因此我们可以设计出这样一个宏来实现它
c
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//VK即按键的虚拟键码
中间一列就是虚拟键码
讲到这里差不多就是贪吃蛇项目所需的所有API了,但是控制台所拥有的API函数远不止如此,有兴趣的朋友可以点开下面链接 link:API函数
三、贪吃蛇项目游戏设计和分析
大家看,如果不进行处理我们地图根据坐标和宽度、高度打印出来就会是这样
很明显高大于宽,这样我们就不好确定贪吃蛇的每个节点,也不方便创建,
所以就需要用到宽字符
3.1 本地化中宽字符的概念及应用
简单来说,C语言设计时只考虑了英语,所以最开始只有8位二进制来存储数据,但是相对于中文的十万汉字等基数极大的语言就非常不够用,因此 本地化 应运而生,宽字符 就是其中之一。
关于本地化,也进行一个简单介绍
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。在标准中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏指定一个类项:
- LC COLLATE:影响字符串比较函数strcoll()和strxfrm()。
- LC CTYPE:影响字符处理函数的行为。
- LC MONETARY:影响货币格式。
- LC_NUMERIC:影响 printf()的数字格式。
- LC TIME:影响时间格式 strftime()和 wcsftime()。
- LC ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。
更多: link
为了修改当前地区我们需要用到 setlocale函数
c
char* setlocale (int category, const char* locale);
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和""(本地模式)。也可以用"NULL"获取当前的默认模式
宽字符的打印
宽字符的字面量必须加上前缀"L",否则C语言会把字面量当作窄字符类型处理。前缀"乚"在单引号前面,表示宽字符,对应 wprintf()的占位符为 %lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为 %ls。
这样就能打印出来了
c
#include <stdio.h>
#include<locale.h>
int main()
{
setlocale(LC_ALL, "");
wchar_t ch1 = L'●';
wchar_t ch2 = L'比';
wchar_t ch3 = L'特';
wchar_t ch4 = L'★';
printf("%c%c\n", 'a', 'b');
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
return 0;
}
3.2 蛇身及食物等初始化结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行
关于蛇身我们只需要确定蛇头的位置坐标,然后利用链表就可以找到后面的蛇身直至蛇尾
不仅如此,我们对整条蛇还需要进行一个封装,来确定方向、状态等
所以我们先利用枚举罗列方向和状态
c
//蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
OK,//正常状态
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞到自己
END_NORMAL//正常退出
};
然后再进行整条蛇的封装,为了方便去下一个节点的坐标及对整条蛇的实参进行改变,我们使用指针来重命名各个结构体
c
//蛇身的节点类型
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 _score;//总成绩
int _sleep_time;//休息时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
由此一来蛇的基本结构就设置完毕了
3.3 游戏流程设计
整体流程放在下面,具体游戏实现请看下回分解😊