前言
贪吃蛇小游戏想必大家都玩过吧,现在就要C语言代码来实现一下贪吃蛇小游戏
在实现之前,我们要对C语言结构体、指针、链表(单链表)有一定的基础
先来看一下预期运行效果
一、Win32 API
这里实现贪吃蛇游戏会使用一些Win32 API的知识,这里简单学习一下
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、绘制图形、使用周边设备等目的,由于这些函数服务的对象是应用程序,所以便称之为Appliccation Programming Interface,简称API。WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。
1.1 控制台程序
在我们的电脑中,windows系统使用快捷键win + R可以打开一个窗口,然后输入cmd就可以打开一个控制台程序,这个控制台可以输入一些命令来控制我们的电脑,这里输入cmd即可打开一个控制台程序窗口
1.1.1 设置控制台程序
本次贪吃蛇小游戏是在VS2022上来实现的,平常我们运行起来的黑框程序就是控制台层序
在VS2022上运行默认是以下情况
这里就需要先修改一个控制台
调出控制台(这里可以使用Win+R,输入cmd调出窗口),点击设置
在默认终端应用程序这里设置成Windows 控制台主机(默认是Windows 终端),点击保存
设置完成后,就是以下这种界面了
1.1.2 设置控制台程序大小
这里我们控制台程序是默认大小,这里我们自己设置控制台程序大小,这里使用cmd控制台程序设置窗口的大小(设置大小为行33,列100)
mode con cols=100 lines=33
1.1.3 设置控制台程序名称
我们设置控制台名称为 贪吃蛇,使用title 指令
title 贪吃蛇
当然,这些能够在控制台窗口执行的命令,也可以通过调用C语言的system函数在中来完成
这里再补充一个指令,暂停控制台程序
system("pause");
这个指令可以暂停程序运行,并会提示按下任意键继续...
int main() { system("mode con cols=100 lines=33"); system("title 贪吃蛇"); system("pause"); return 0; }
1.1.4 控制台屏幕上的坐标
COORD是Windows API中自定义的一个结构体,表示一个字符在控制台屏幕缓冲区的坐标,坐标(0,0)的原点位于缓冲区的顶部左侧单元格。
COORD类型声明
typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD;
给坐标赋值
COORD pos = { 10, 15 };
GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),这个句柄可以操作设备。
HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);
函数参数
函数使用
HANDLE hOutput = NULL; //获取标准输出的句柄(用来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
GetConsoleCursorInfo函数检索有关指定控制台屏幕缓冲区的光标的大小和可见性的信息
函数语法
BOOL WINAPI GetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo );
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息
CONSOLE_CURSOR_INFO结构体
这个结构体包含了有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize 由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元格底部的水平线条
bVisible 游标的可见性。如果光标可见,则此成员为true;如果不可见,此成员为false
函数参数
这里就用到上面GetStdHandle函数获得的句柄了,还需要用到CONSOLE_CURSOR_INFO结构体(注意,这里第二个参数是指针)
函数使用
HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
SetConsoleCursorInfo
SetConsoleCursorInfo函数设置指定控制台屏幕缓冲区的光标的大小和可见性
函数参数
BOOL WINAPI SetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo );
与GetConsoleCursorInfo函数参数相同
函数使用
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false; //隐藏控制台光标 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
SetConsoleCursorPosition
SetConsoleCursorPosition函数设置指定控制台屏幕缓冲区的位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition( _In_ HANDLE hConsoleOutput, _In_ COORD dwCursorPosition );
函数参数
函数使用
HANDLE hOutput = NULL; //获取标准输出的句柄(用来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { 10, 5 }; //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos);
这里为了方便后面定位屏幕坐标,单独封装一个函数来实现
void SetPos(short x, short y) { COORD pos = { x, y }; HANDLE hOutput = NULL; //获取标准输出的句柄(用来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos); }
GetAsyncKeyState
GetAsyncKeyState函数获得按键情况
SHORT GetAsyncKeyState( [in] int vKey );
函数参数
这里函数参数是虚拟键码。
这里仅列出一些在游戏中可能用到的按键的虚拟键码,可以点击查看详细虚拟键码
VK_UP | 0x26 | ↑ |
VK_DOWN | 0x28 | ↓ |
VK_LEFT | 0x25 | ← |
VK_RIGHT | 0x27 | → |
VK_F3 | 0x72 | F3 |
VK_F4 | 0x73 | F4 |
VK_ESCAPE | 0x1B | Esc |
VK_SPACE | 0x20 | 空格 |
函数返回值
GetAsyncKeyState 函数返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,如果最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
在游戏中我们需要检测一个按键是否被按过,就检测 GetAsyncKeyState 函数返回值的最低值是否是1,可以写一个宏来实现:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
二、本地化
在贪吃蛇游戏中,我们会涉及到墙体□ 和蛇的身体● 的打印,而在VS中我们输出出来的是?
这就是因为没有本地化设置,无法输出这些特殊字符(宽字符)。
我们需要通过修改地区,让程序来适应不同的区域,我们就需要进行本地化设置
这里就要使用到C语言中的库函数 setlocale 函数
在C标准中,依赖地区的部分有以下几项
数字量的格式
货币量的格式
字符集
日期和时间的表示形式
通过修改地区,程序可以改变它的行为来适应世界的不同地域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类型进行修改,下面的一个宏就指定一个类型。
LC_COLLATE :影响字符串表函数 strcoll 和strxfrm。
LC_CTYPE : 影响字符处理函数的行为。
LC_MONETARY : 影响货币格式。
LC_NUMERIC : 影响 printf 的数字格式。
LC_TIME : 影响时间格式 strftime 和 wcsftime 。
LC_ALL : 针对所有类型修改,将以上所有类别设置为给定的语言环境。
2.1 setlocale 函数
setlocale 函数用于修改当前地区,可以针对一个类项进行修改,也可以针对所有类项
函数的第一个参数可以是前面类项中的一个,也可以是LC_ALL(影响所以的类项)
函数的第二个参数
C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地化模式)
这里我们需要进行本地化设置
setlocale(LC_ALL, " ");//切换到本地环境
2.2 宽字符的打印
在屏幕中,我们需要打印宽字符
宽字符的字面量必须加上前缀“L”,否则C语言就会把字符量当成窄字符来处理。前缀“L”子啊单引号(或者双引号)的前面,表示宽字符,对于 wprintf 的占位符是 %lc;双引号的前面,对于wprintf 的占位符是 %ls。
可以看到,这里宽字符占两个窄字符的位置。
三、游戏分析和设计
3.1 贪吃蛇数据结构设计
在游戏运行的过程中,蛇每吃一次食物,蛇的身体就会变长;这样我们就可以使用链表来存储蛇的信息,蛇的每一个节身体其实就是链表的一个节点。每个节点只需记录蛇身节点在地图上的坐标就可以。
蛇身节点的结构:
typedef struct Snakenode { int x; int y; struct Snakenode* next; }Snakenode, * pSnakenode; //这里也可以写 typedef Snakenode* pSnakenode
接下来,我们还需要记录游戏过程中的相关信息
贪吃蛇,食物的位置,蛇的方向,游戏状态,当前的分数,每一个食物的分数,蛇的速度等
而这里蛇的方向和游戏状态都可以一一列举出来,这里就使用枚举变量
//蛇的方向 enum DIRECT { UP = 1, DOWN, LEFT, RIGHT }; //蛇的状态——游戏状态 //正常、撞墙、撞到自己、正常退出 enum GAME_STATE { OK, KILL_WALL, KILL_SELF, NORMAL_END }; //贪吃蛇的相关信息 typedef struct Snake { pSnakenode psnake; //指向蛇头部的指针 pSnakenode pfood; //指向食物的指针 enum DIRECT dir;//蛇的方向 enum GAME_STATE state;//蛇的状态 int food_scores;//每个食物的分数 int all_scores; //总分数 int sleep_time; //休息的时间 --即蛇的速度 }Snake; typedef Snake* pSnake;
这样,我们就创建了一个Snake结构体来维护游戏相关信息(维护整条贪吃蛇)
3.2 游戏流程分析
游戏大概分析如下
【C语言】实践:贪吃蛇小游戏(附源码)(二)https://developer.aliyun.com/article/1621361