一、游戏背景及其功能
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。
在这里我们将使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇。
实现基本的功能:
- 贪吃蛇地图绘制
- 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞⾃⾝死亡
- 计算得分
- 蛇⾝加速、减速
- 暂停游戏
在编写这个游戏之前,需要我们掌握C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等相关知识,在我的博客中除了Win32 API以外其余知识都有详细的讲解,在这里我们将来学习Win32 API的知识。
二、Win32 API介绍
Win32 API对我们来说是一个全新的内容,因此将在这里对其进行全面讲解。
1、Win32 API
Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便称之为Application Programming Interface,简称API函数。
Win32 API 也就是Microsoft Windows 32位平台的应⽤程序编程接⼝。
2、控制台程序
平常我们运⾏起来的⿊框程序其实就是控制台程序。
- 我们可以输入cmd命令来设置控制台窗⼝的⻓和宽。
例如:将控制台窗口的⼤⼩设置为30⾏,100列。
mode con cols=100 lines=30
参考:mode命令
- 也可以通过cmd命令来设置控制台窗⼝的名字。
title 贪吃蛇
如图:
参考:title命令
- 这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数
system
来执⾏。
例如:
#include<stdio.h>
int main()
{
//设置控制台的长宽
system("mode con cols=30 lines=30");
//设置控制台名称
system("title 贪吃蛇");
return 0;
}
运行结果:
3、定位坐标(COORD)
COORD
是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台上的坐标,坐标(0,0)位于缓冲区的顶部左侧单元格。
如图:
注意: 使用Windows API 中的内容时需要包含头文件#include<windows.h>
。
COORD类型的声明:
typedef struct _COORD
{
SHORT X;
SHORT Y;
}COORD, * PCOORD;
- 使用COORD来表示一个字符在控制台上的坐标。
COORD pos = { 10, 15 };
4、获得句柄(GetStdHandle)
GetStdHandle
是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄,使⽤这个句柄可以操作设备。
函数原型:
HANDLE GetStdHandle(DWORD nStdHandle);
举例:获得输出句柄。
int main()
{
//获得输出句柄
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
return 0;
}
5、获得光标属性(GetConsoleCursorInfo)
GetConsoleCursorInfo
也是一个Windows API 函数,用来获取控制台光标的属性(光标大小、可见性等等)。
函数原型:
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
注:
PCONSOLE_CURSOR_INFO
是指向CONSOLE_CURSOR_INFO
结构的指针,该结构接收有关光标的信息。
函数的使用:
int main()
{
//获得输出句柄
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//获取光标信息
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
return 0;
}
1)描述光标属性(CONSOLE_CURSOR_INFO)
CONSOLE_CURSOR_INFO
是 Windows API 中的一个结构体,用于描述控制台光标的属性。
结构体的声明:
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, * PCONSOLE_CURSOR_INFO;
- dwSize:光标在屏幕上的大小。此值介于1到100之间,是光标高度占字符单元格高度的百分比。
- bVisible:光标的可见性。如果光标可⻅为TRUE,不可见为FALSE。
CursorInfo.bVisible = FALSE; //隐藏控制台光标
6、设置光标属性(SetConsoleCursorInfo)
SetConsoleCursorInfo
是 Windows API 中的一个函数,用于设置控制台光标的属性(大小和可见性)。它一般与 GetConsoleCursorInfo
配合使用。
函数原型:
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo
);
函数的使用:
int main()
{
//获得输出句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//获取光标信息
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
//设置光标状态
CursorInfo.bVisible = FALSE; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);
}
运行结果:
可以看到此时光标已经被隐藏了。
7、设置光标位置(SetConsoleCursorPosition )
SetConsoleCursorPosition
是 Windows API 中的一个函数,用于设置控制台屏幕缓冲区中光标的位置。
函数原型:
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
函数的使用:
int main()
{
//坐标
COORD pos = { 10, 5 };
//获得输出句柄
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置光标的位置为坐标pos
SetConsoleCursorPosition(hOutput, pos);
return 0;
}
运行结果:
可以看到此时光标的位置已经被设置到(10,5)这个坐标了。
1)封装函数SetPos
- 如果想要让光标出现在指定的坐标位置,那么我们就可以将上述代码封装为一个函数
SetPos
。
void SetPos(short x, short y)
{
//取得坐标
COORD pos = { x, y };
//获得输出句柄
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置光标位置
SetConsoleCursorPosition(hOutput, pos);
}
8、检测按键状态(GetAsyncKeyState)
GetAsyncKeyState
是 Windows API 中的一个函数,用于检测某个按键或鼠标按钮的当前状态。
函数原型:
SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState
的返回值是short类型,在调⽤ GetAsyncKeyState
函数后,如果返回的16位short数据中,如果最低位被置为1则说明,该按键被按过,否则为0。
- 因此如果我们要判断⼀个键是否被按过,可以检测
GetAsyncKeyState
函数的返回值最低位是否为1。将其定义为一个宏。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
参考:虚拟键码
实例: 检测数字键
#include <stdio.h>
#include <windows.h>
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
int main()
{
while (1)
{
if (KEY_PRESS(0x30))
{
printf("0\n");
}
else if (KEY_PRESS(0x31))
{
printf("1\n");
}
else if (KEY_PRESS(0x32))
{
printf("2\n");
}
else if (KEY_PRESS(0x33))
{
printf("3\n");
}
else if (KEY_PRESS(0x34))
{
printf("4\n");
}
else if (KEY_PRESS(0x35))
{
printf("5\n");
}
else if (KEY_PRESS(0x36))
{
printf("6\n");
}
else if (KEY_PRESS(0x37))
{
printf("7\n");
}
else if (KEY_PRESS(0x38))
{
printf("8\n");
}
else if (KEY_PRESS(0x39))
{
printf("9\n");
}
}
return 0;
}
运行结果:
当我们按下数字键后就会在控制台上面显示出来,而按到其他键时则不会显示。
三、贪吃蛇游戏设计与分析
1、地图
我们最终的贪吃蛇界面应该实现下面的效果,那我们的地图该如何布置呢?
如果想在控制台窗⼝中的指定位置输出信息,我们得知道
该位置的坐标。
控制台窗⼝的坐标如下所⽰,横向的是X轴,纵向是Y轴。
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★。
普通的字符是只占1个字节,而上述这类宽字符需要占2个字节。
这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。
C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。
后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型wchar_t
和宽字符的输⼊和输出函数,加⼊了<locale.h>
头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
1)<locale.h>本地化
<locale.h>
提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
在标准中,依赖地区的部分有以下⼏项:
- 数字量的格式
- 货币量的格式
- 字符集
- ⽇期和时间的表⽰形式
2)类项
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯不同的宏指定不同的类项:
- LC_COLLATE:影响字符串⽐较函数
strcoll()
和strxfrm()
。 - LC_CTYPE:影响字符处理函数的⾏为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响
printf()
的数字格式。 - LC_TIME:影响时间格式
strftime()
和wcsftime()
。 - LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
3)setlocale函数
函数原型:
char* setlocale (int category, const char* locale);
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 函数的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第⼆个参数仅定义了2种可能取值:"C"
(正常模式)和" "
(本地模式)。
在任意程序执⾏开始,都会隐藏式执⾏调⽤:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "
作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。
⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。
setlocale(LC_ALL, " ");
4)宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字⾯量必须加上前缀“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'□';
wchar_t ch3 = L'张';
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
printf("%c%c\n", 'a', 'b');
return 0;
}
注: 我们需要以管理员的身份打开VS再运行代码才可以观察得更明显。
运行结果:
从输出的结果来看,我们发现一个普通字符占1个字符的位置,但是⼀个宽字符会占⽤2个字符的位置,因此如果我们要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。
普通字符和宽字符打印出宽度的展⽰如下:
5)地图坐标
我们假设实现⼀个棋盘27⾏,58列的棋盘,再围绕地图画出墙,
如下:
2、蛇⾝和⻝物
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24,5)处开始出现蛇,连续5个节点。
- 注意: 蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。
3、数据结构设计
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇身的节点结构如下:
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
- pSnakeNode是结构体指针,等价于
typedef struct SnakeNode* pSnakeNode;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物节点的指针
enum DIRECTION _dir;//蛇的方向
enum GAME_STATUS _status;//游戏的状态
int _food_weight;//一个食物的分数
int _score;//总成绩
int _sleep_time;//休息时间,时间越短,速度越快
}Snake, * pSnake;
- 蛇的⽅向,可以⼀⼀列举,使⽤枚举:
enum DIRECTION
{
UP = 1,//上
DOWN,//下
LEFT,//左
RIGHT//右
};
- 游戏状态,可以⼀⼀列举,使⽤枚举:
enum GAME_STATUS
{
OK,//正常运行
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到自己
END_NOMAL//正常结束
};
4、游戏流程设计
现在我们已经把贪吃蛇大致的步骤梳理了一遍,了解完上述知识后,接着在下篇文章就开始编写方法来实现整个游戏。