引言
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。这一章是需要具备的前置知识,技术要点里有不会的可以在我的主页里面搜索对应的知识点。
游戏画面:
贪吃蛇游戏展示
需要的技术要点:
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等。
Win32PI下面会详细介绍这次需要用到的函数。
一、Win32API介绍
Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启 视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便 称之为 Application Programming Interface,简称API函数。WIN32API也就是MicrosoftWindows 32位平台的应用程序编程接口。
1.控制台程序
平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列、
mode con cols=100 lines=30
通过命令设置控制台窗⼝的名字:
title 贪吃蛇
这些能在控制台窗口执行的命令,可以调用C语言函数system来执行:
int main()
{//设置控制台窗口的长宽system("mode con cols=100 lines=30");//设置cmd窗口名称system("title 贪吃蛇");system("pause");//暂停程序return 0;
}
2.控制台屏幕上的坐标COORD
COORD是WindowsAPI中定义的⼀个结构体,表示一个字符在控制台屏幕上的坐标
typedef struct _COORD {SHORT X;SHORT Y;} COORD, *PCOORD;
赋值:
COORD pos = { 10, 15 };
3.GetStdHandle
GetStdHandle 函数 - Windows Console | Microsoft Learn
GetStdHandle是一个WindowsAPI函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
语法
HANDLE WINAPI GetStdHandle( _In_ DWORD nStdHandle );
参数:
在贪吃蛇游戏中使用 STD_OUTPUT_HANDLE 来控制标准输出设备(即控制台窗口)
返回值
HANDLE是一个指针类型(void*指针)
获取控制台句柄:
//获取标准输出的句柄(通过这个句柄来对控制台进行操作)
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
4.GetConsoleCursorInfo
Console 翻译:控制台
cursor 翻译:光标
GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
语法
BOOL WINAPI GetConsoleCursorInfo(_In_ HANDLE hConsoleOutput, //句柄_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo //CONSOLE_CURSOR_INFO 结构的地址);
需要搭配 CONSOLE_CURSOR_INFO结构体来使用。
4.1CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
成员:
dwSize:由游标填充的字符单元的百分比。 该值介于 1 到 100 之间。 游标外观各不相同,范围从完全填充单元到显示为单元底部的横线。
bVisible:游标的可见性。 如果游标可见,则此成员为 TRUE。
//获取标准输出的句柄(通过这个句柄来对控制台进行操作)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO curInfo; //存有光标信息的结构体//获得和hOutput句柄相关的控制台上的光标信息,存放在curInfo中GetConsoleCursorInfo(hOutput, &curInfo); printf("%d ", curInfo.dwSize); //打印结构体里面的信息
获得了当前光标信息的结构体后,可以对结构体里面的内容进行修改,但是还需要通过SetConsoleCursorInfo 函数 ,来设置指定控制台屏幕缓冲区的光标的大小和可见性。
5.SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
语法(和GetConsoleCursorInfo类似):
BOOL WINAPI SetConsoleCursorInfo(_In_ HANDLE hConsoleOutput, //句柄_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo //光标信息的结构体地址
);
例如:隐藏光标
//获取标准输出的句柄(通过这个句柄来对控制台进行操作)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO curInfo; //存有光标信息的结构体//获得和hOutput句柄相关的控制台上的光标信息,存放在curInfo中GetConsoleCursorInfo(hOutput, &curInfo); curInfo.bVisible = false; //修改光标信息//设置获得和hOutput句柄相关的控制台上的光标信息SetConsoleCursorInfo(hOutput, &curInfo);
6.SetConsoleCursorPosition
SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
语法:
BOOL WINAPI SetConsoleCursorPosition(_In_ HANDLE hConsoleOutput, //句柄_In_ COORD dwCursorPosition //COORD类型的变量
);
例如:
int main(){//获取标准输出的句柄(通过这个句柄来对控制台进行操作)HANDLE hOutput = NULL;hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { 10, 29 };SetConsoleCursorPosition(hOutput, pos);getchar();}
封装一个设置光标位置的函数:
void set_pos(int x, int y)
{//获取标准输出的句柄(通过这个句柄来对控制台进行操作)HANDLE hOutput = NULL;hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { x, y };SetConsoleCursorPosition(hOutput, pos);
}
7.GetAsyncKeyState
获取按键情况
语法:
SHORT GetAsyncKeyState([in] int vKey
);
参数: [in] int vKey 虚拟键代码(不是ASCII)
参考:Virtual-Key 代码 (Winuser.h) - Win32 apps | Microsoft Learn
这里给出了部分:
作用:将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。 GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
在游戏中,我们只需要判断一个数是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。(被按过:最低位为1 ,未被按过:最低位为0)
参考代码:
当我们按下哪个键,屏幕上就输出哪个键
//定义一个检测按键是否被按过的宏
#define KEY_PRESS(kv) ((GetAsyncKeyState(kv)&1) ? 1:0)
//最低位是1,就返回1,
//最低位是0,就返回0int main()
{while (1){if (KEY_PRESS(0x60)) //0{printf("0\n");}else if (KEY_PRESS(0x61)){printf("1\n");}else if (KEY_PRESS(0x62)){printf("2\n");}else if (KEY_PRESS(0x63)){printf("3\n");}else if (KEY_PRESS(0x64)){printf("4\n");}else if (KEY_PRESS(0x65)){printf("5\n");}else if (KEY_PRESS(0x66)){printf("6\n");}else if (KEY_PRESS(0x67)){printf("7\n");}else if (KEY_PRESS(0x68)){printf("8\n");}else if (KEY_PRESS(0x69)){printf("9\n");}}return 0;
}
二、C语言的国际化特性相关的知识
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印⻝物使用宽字符★ 普通的字符是占⼀个字节的,宽字符是占用2个字节。
简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。 C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低 7 位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符 号,它就无法用ASCII码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel( ),在俄语编码中又会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是⼀样的,不⼀样的只是128--255的这⼀段。 至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号, 肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入宽字符的类型wchar_t 和宽字符的输入和输出函数,加入<localel.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
<locale.h>本地化(为了打印宽字符)
<locale.h> 提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。在标准中,依赖地区的部分有以下几项:
• 数字量的格式
• 货币量的格式
• 字符集
• 日期和时间的表示形式
1类项:
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏, 指定⼀个类项:
• LC_COLLATE
• LC_CTYPE
• LC_MONETARY
• LC_NUMERIC
• LC_TIME
• LC_ALL-针对所有类项修改
2setlocale 函数
setlocale - C++ Reference (cplusplus.com)
原型
char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前面说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值: C 和 ""。
当第二个参数为C时,库函数按正常方式执行。
setlocale(LC_ALL,"C"); //按照正常方式执行
当第二个参数为""时,就进行本地化,切换到本地模式,这样就可以打印宽字符了
setlocale(LC_ALL,""); //按照本地方式执行
setlocale函数返回值是一个char*(可以理解为字符串),即当前是什么模式。
第二个参数是NULL时,可以查看当前的模式;
int main()
{char* ret = setlocale(LC_ALL,NULL);//获取当前模式printf("%s\n", ret);ret = setlocale(LC_ALL, "");//设置为本地模式printf("%s\n", ret);return 0;
}
3打印宽字符
对宽字符的打印有自己的规则:
1.宽字符类型:wchar_t 表示宽字符类型
2.单引号('')对应的占位符是:%lc,双引号("")对应的占位符是:%ls;
3.在赋值时,单引号或者双引号前面需要加上 L,表示是宽字符类型。
4.打印宽字符用的函数是:wprintf();语法和printf()类似。但是:站位符双引号前""需要加L。
看代码:
int main()
{//设置本地化setlocale(LC_ALL, "");char a = 'a';char b = 'b';printf("%c%c\n", a, b);wchar_t wc1 = L'★';wchar_t wc2 = L'□';wchar_t wc3 = L'●';wprintf(L"%lc\n", wc1);wprintf(L"%lc\n", wc2);wprintf(L"%lc\n", wc3);return 0;
}
从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置 但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。
三、前置总结
OK啦,看到这里,前面的知识点一定要搞懂,特别是Win32API的句柄和各个函数之间的关系。下一章,正式开始贪吃蛇游戏的设计和代码的实现。