深入解析:【C语言】C语言经典小游戏:贪吃蛇(上)

文章目录一、游戏背景及其功能二、Win32 API介绍1、Win32 API2、控制台程序3、定位坐标(COORD)4、获得句柄(GetStdHandle)5、获得光标属性(GetConsoleCursorInfo)1)描述光标属性(CONSOLE_CURSOR_INFO)6、设置光标属性(SetConsoleCursorInfo)7、设置光标位置(SetConsoleCursorPosition )1)封装函数SetPos8、检测按键状态(GetAsyncKeyState)三、贪吃蛇游戏设计与分析1、地图1)本地化2)类项3)setlocale函数4)宽字符的打印5)地图坐标2、蛇⾝和⻝物3、数据结构设计4、游戏流程设计

一、游戏背景及其功能贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。

在这里我们将使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇。

实现基本的功能:

贪吃蛇地图绘制蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)蛇撞墙死亡蛇撞⾃⾝死亡计算得分蛇⾝加速、减速暂停游戏在编写这个游戏之前,需要我们掌握C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等相关知识,在我的博客中除了Win32 API以外其余知识都有详细的讲解,在这里我们将来学习Win32 API的知识。

二、Win32 API介绍Win32 API对我们来说是一个全新的内容,因此将在这里对其进行全面讲解。

1、Win32 APIWindows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(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

int main(

)

{

//设置控制台的长宽

system("mode con cols=30 lines=30"

)

;

//设置控制台名称

system("title 贪吃蛇"

)

;

return 0

;

}

运行结果:

3、定位坐标(COORD)COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台上的坐标,坐标(0,0)位于缓冲区的顶部左侧单元格。

如图:注意: 使用Windows API 中的内容时需要包含头文件#include

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

#

include

#

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 和宽字符的输⼊和输出函数,加⼊了头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

1)本地化提供的函数⽤于控制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

#

include

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、游戏流程设计

现在我们已经把贪吃蛇大致的步骤梳理了一遍,了解完上述知识后,接着在下篇文章就开始编写方法来实现整个游戏。