1. 游戏任务
使用C语言在Windows环境的控制台中模拟实现小游戏贪吃蛇
游戏中要包含以下功能:
1. 贪吃蛇地图绘制
2. 贪吃蛇上下左右移动和吃食物
3. 蛇撞墙,或撞到自身死亡
4. 计算得分
5. 蛇身加速、减速
6. 暂停游戏
2. Win32 API 介绍
Windows是一种多作业的操作系统(同时进行多个任务进程),它除了协调应用程序的执行、分配内存、管理资源之外。它还是一个很大的服务中心,服务中心有很多函数接口,我们可以通过调用这些函数或者说是服务,来帮助应用程序达到开启视窗、描绘图形、使用周边设备等功能。由于这些服务的对象是应用程序,所以我们把这些服务称为 Application Programming Interface (应用程序编程接口) 简称API函数
2.1 控制台程序(Console)
首先操作系统是win11的朋友们要注意了,我们运行程序的时候弹出的那个黑色窗口不是控制台,而是win11新提供的终端窗口,在终端窗口中是不能实现控制台程序中的一些功能的
上面这个窗口就是终端窗口,下面我们讲解如何改成控制台窗口
鼠标放到下箭头上,然后选择设置
在启动中选择Windows控制台主机,并保存
下次再运行起来的就是控制台窗口了
下面我们介绍两个控制台程序命令:
2.1.1 设置控制台的大小
mode con cols=100 lines=30
当我们把这段命令敲到cmd里头之后就会发现窗口的大小改变了
cols 控制的是列,lines 控制的是行,现在我们就可以根据喜好控制游戏窗口的大小了
2.1.2 设置控制台的名字
title 贪吃蛇
现在可以注意到,控制台窗口的名字变成了贪吃蛇
2.1.3 system()函数
上面我们是在cmd中进行的操作,那么我们如何把这些操作写进C程序中呢,这时就用到了system函数,system函数就相当于帮你把内容输入到控制台中了,这个函数需要引用头文件<stdlib.h>
int system (const char* command);
官网链接:system - C++ Reference
上面我们展示了一下使用的效果,我们将窗口大小和名字都修改了,但是我在最后输入了一个pause暂停的语句,这是因为如果不暂停的话程序就直接结束了,紧接着刚刚输入的这些命令就失效了,那我们就看不到效果了。
2.2 控制台屏幕上的坐标 COORD
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系原点(0,0)位于缓冲区的顶部左侧单元格
COORD的类型声明差不多长这个样子:
为啥说差不多呢,因为在真正的声明中short是大写的,因为它前面给重命名了,但是其实这些我们都不必关注。
下面说一下如何使用这个结构体,首先要引用头文件<Windows.h>,然后搞一个 COORD 类型变量赋值就行
2.3 控制台的操作以及光标控制
2.3.1 GetStdHandle
GetStdHandle 是一个Windows API函数,它用于从一个特定的标准设备 (标准输入、标准输出或标准错误) 中取得一个句柄(用来标识不同设备的数值),使用对应的句柄可以操作对应的设备。下面我们展示一下这个函数的声明:
HANDLE GetStdHandle( DWORD nStdHandle );
官网资料:GetStdHandle 函数 - Windows Console | Microsoft Learn
这个参数的类型 DWORD 看起来很迷,但其实这个参数就3种输入情况
本节我们主要是用这个函数来获取控制台的标准输出句柄,以此来控制控制台上输出的东西,其实说白了就是把光标隐藏掉,因为如果不隐藏的话,光标在那里一直闪,很影响游戏画面的美观性。
当我们使用这个函数的时候要先定义一个HANDLE类型的参数,其实HANDLE就是一个被typedef了的 void* 类型名。当然像这种API函数都要引用头文件<Windows.h>,后面再有用到API函数的时候我就不赘述引用头文件了。
2.3.2 GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
官网资料:GetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该指针接收有关主机游标(光标)的信息。
2.3.3 CONSOLE_CURSOR_INFO
这个结构体包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
官网资料:CONSOLE_CURSOR_INFO 结构 - Windows Console | Microsoft Learn
dwSize 是由光标填充的字符单元格的百分比。此值介于1-100之间。光标外观会发生变化,从0到100是光标从最下面一直上长到最上面,最后填充满整个字符单元格。
bVisible 是游标的可见性,一个布尔类型变量。如果光标可见,此成员为true,不可见为false
我们现在用一下GetConsoleCursorInfo函数,把控制台中的光标信息存放到cursor_info结构体中,观察光标信息cursor_info中的值,其中dwSize是25,对应着光标占25%的字符单元格,bVisible是1,对应着 true 可见的。后面那个圈出来的光标是我自己点出来的,像那样的光标就是dwSize=100的光标。
2.3.4 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
官网资料:SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn
我们可以用访问并改变结构体cursor_info的成员,然后再用SetConsoleCursorInfo,把改变后的信息交给程序。像这里我就把光标的dwSize改成了50,现在它看起来比25的时候高了不少。
但是我们的主要任务是要隐藏光标,所以我们要修改光标的可见性,当然,在使用布尔类型时要注意引用头文件<stdbool.h>
2.3.5 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的光标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
官网资料:SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn
一顿操作之后你就会发现,hello world并不是从左上角的(0,0)开始打印了,而是从我设置好的光标位置开始打印的了。
当然为了后续方便使用,我们可以把这一坨封装到一个函数里头去。
2.3.6 GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(
int vKey
);
官网资料:GetAsyncKeyState function (winuser.h) - Win32 apps | Microsoft LearnGetAsyncKeyState 函数 (winuser.h) - Win32 apps |Microsoft 学习GetAsyncKeyState function (winuser.h) - Win32 apps | Microsoft Learn
将键盘上每个键的虚拟值(vKey)传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState的返回值时short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位short型数据中。如果最高位是1,说明按键的状态是按下;如果最高位是0,说明按键的状态是抬起;如果最低位是1,说明按键被按过;如果最低位是0,说明按键没被按过。
所以我们只需要判断返回值最低位是否为1就能知道这个按键是否被按过。
虚拟键值表:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
下面我们实现检测数字键的功能,就是说我按下哪个数字键,就打印出哪个数字:
解释一下,首先,我定义的这个宏 KEY_PRESS(vk) 让得到的虚拟键的反馈值按位与 1 就能知道最后返回值的最后一位是不是1了,也就是说,能够检测到有没有按这键。然后写一个死循环,一直判断这些虚拟键有没有被按过,如果按过,就把它打印出来。
既然我们能够判断数字键有没有被按过,那么我们就能判断上下左右键有没有被按过,我们想监测哪个键就把对应的码值写上去就好了,如此说来,蛇的移动问题就解决了一半了
3. 贪吃蛇游戏设计与分析
3.1 地图
我们最终的贪吃蛇游戏大概是这个样子的,那我们的地图该如何布置呢?
欢迎界面
操作介绍界面
游戏界面
在游戏地图上,我们打印墙体使用宽字符:,打印蛇身使用宽字符:,打印食物使用宽字符:
普通的字符是占一个字节的,但是宽字符占两个字节,而且这些宽字符在视觉效果上也是一个普通字符的二倍
这里简单讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家使用,C语言最初是美国人发明的,他们的语言中就26个字母,所以可能要使用到的字符非常少,但是其他用语言国家就不一定够用了。所以后来为了使C语言国际化,C语言的标准中不断加入了国际化的支持。比如宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了<locale.h> 头文件,其中提供了允许程序员针对特定地区调整程序行为的函数。
3.1.1 <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分,标准中依赖地区的部分有以下几项:
1. 数字量的格式
2. 货币量的格式
3. 字符集
4. 日期和时间的表示形式
3.1.2 类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的每一个宏,指定一个类项:
LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm()
LC_CTYPE:影响字符处理函数的行为
LC_MONETARY:影响货币的格式
LC_NUMERIC:影响 printf() 的数字格式
LC_TIME:影响时间格式 strftime() 和 wcsftime()
LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境
每个类项的详细说明:setlocale,_wsetlocale | Microsoft Learn
3.1.3 setlocale函数
setlocale 函数用于修改当前的地区,可以针对一个类项修改,也可以LC_ALL修改所有
char* setlocale (int category, const char* locale);
官网资料:setlocale - C++ Reference (cplusplus.com)
setlocale 的第一个参数可以是前面讲到的任何一个类项,区别就是影响哪个类项,或者是全都影响
setlocale 的第二个参数仅定义了两种可能取值:"C" (正常模式) 和 "" (本地模式)。本地模式就是一个空字符串就行了,然后你的Windows是哪国版本就给你上那个地区的模式
当没有专门调用setlocale来控制模式的话,默认是正常模式启动
setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL
setlocale() 可以用来查询当前地区,这时第二个参数设置为NULL就可以了
setlocale() 在我们贪吃蛇项目中的用处就是把程序本地化,然后来让我们使用宽字符
3.1.4 宽字符的打印
宽字符的字面量必须加上前缀 L ,否则C语言会把字面量当作普通字符处理。前缀 L 在单引号前面,表示宽字符,宽字符的打印使用 wprintf() ,打印格式前面也要加上 L ,对应宽字符的占位符是 %lc ,宽字符串占位符是 %ls 。汉字也是宽字符
现在我们就很明显看出来宽字符的宽了,它真的占了两个字符的位置。
那么在控制台的坐标系中一个普通的字符是占一列位置的,那么一个宽字符事实上要占两列位置,但是它们所占的行是一样的,都只占一行
3.1.5 地图坐标
我们假设要实现一个27行58列的棋盘,再围绕它画出墙,如图:
棋盘大小可以根据自己喜好设定,列数最好是行数的两倍,这样差不多能是一个正方形,然后列数最好设计成双数的,因为棋盘的墙还有里头的蛇和食物都是宽字符
3.1.6 蛇身和食物
初始化状态,假设蛇身长度是5,蛇身的每个节点是 ● ,蛇头出现在一个固定的坐标处,比如(24,5) 处开始出现蛇,连续5个节点。
注意:蛇的每一个节点的x坐标必须是2的倍数,否则蛇撞墙的判定会很迷
关于食物,就是在墙体内随机生成一个坐标(列也同样必须是2的倍数),坐标不能和蛇身重合,然后打印★
4. 未完待续······
到此,我们贪吃蛇游戏的前置知识就学完了,下节我们将着手写出这个游戏