项目实战
- 游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学生的编程能力和逻辑能力。
目录:
-
游戏背景
-
游戏效果演示
-
目标
-
定位
-
技术要点
-
贪吃蛇游戏设计与分析
-
贪吃蛇游戏数据结构设计
-
相关Win32API介绍
-
参考代码
正文开始
- 游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升编程能力和逻辑能力。
2. 游戏效果演示
- 目标
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞自身死亡
• 计算得分
• 蛇身加速、减速
• 暂停游戏
-
定位
• 提高对编程的兴趣
• 对C语言语法做一个基本的巩固。
• 对游戏开发有兴趣的同学做一个启发。
• 项目适合:C语言学完的同学,有一定的代码能力,初步接触数据结构中的链表。 -
技术要点
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
- Win32 API介绍
本次实现贪吃蛇会使用到的一些Win32 API知识,接下来我们就学习一下。
6.1 Win32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大
的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启
视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便
称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows
32位平台的应用程序编程接口。
6.2 控制台程序
平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
1 mode con cols=100 lines=30
参考:mode命令
也可以通过命令设置控制台窗口的名字:
1 title 贪吃蛇
参考:title命令
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
1 #include <stdio.h>
2 int main()
3 {
4 //设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
5 system("mode con cols=100 lines=30");
6 //设置cmd窗口名称
7 system("title 贪吃蛇");
8 return 0;
9 }
6.3 控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系
(0,0) 的原点位于缓冲区的顶部左侧单元格。
COORD类型的声明:
1 typedef struct _COORD {
2 SHORT X;
3 SHORT Y;
4 } COORD, *PCOORD;
给坐标赋值:
1 COORD pos = {
10, 15 };
6.4 GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标
准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
1 HANDLE GetStdHandle(DWORD nStdHandle);
实例:
1 HANDLE hOutput = NULL;
2
3 //获取标准输出的句柄(用来标识不同设备的数值)
4 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
6.5 GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
1 BOOL WINAPI GetConsoleCursorInfo(
2 HANDLE hConsoleOutput,
3 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
4 );
5
6 PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光
实例:
1 HANDLE hOutput = NULL;
2 //获取标准输出的句柄(用来标识不同设备的数值)
3 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
4
5 CONSOLE_CURSOR_INFO CursorInfo;
6 GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
6.5.1 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
1 typedef struct _CONSOLE_CURSOR_INFO {
2 DWORD dwSize;
3 BOOL bVisible;
4 } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
• dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完
全填充单元格到单元底部的水平线条。
• bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
1 CursorInfo.bVisible = false; //隐藏控制台光标
6.6 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
1 BOOL WINAPI SetConsoleCursorInfo(
2 HANDLE hConsoleOutput,
3 const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
4 );
实例:
```cpp
1 HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
2
3 //影藏光标操作
4 CONSOLE_CURSOR_INFO CursorInfo;
5 GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
6 CursorInfo.bVisible = false; //隐藏控制台光标
7 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
6.7
SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调
用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
```cpp
1 BOOL WINAPI SetConsoleCursorPosition(
2 HANDLE hConsoleOutput,
3 COORD pos
4 );
实例:
```cpp
1 COORD pos = {
10, 5};
2 HANDLE hOutput = NULL;
3 //获取标准输出的句柄(用来标识不同设备的数值)
4 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
5 //设置标准输出上光标的位置为pos
6 SetConsoleCursorPosition(hOutput, pos);
SetPos:封装一个设置光标位置的函数
```cpp
1 //设置光标的坐标
2 void SetPos(short x, short y)
3 {
4 COORD pos = { x, y };
5 HANDLE hOutput = NULL;
6 //获取标准输出的句柄(用来标识不同设备的数值)
7 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
8 //设置标准输出上光标的位置为pos
9 SetConsoleCursorPosition(hOutput, pos);
10 }
6.8 GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
1 SHORT GetAsyncKeyState(
2 int vKey
3 );
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上一次调用
GetAsyncKeyState 函数后,
如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬
起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.
1 #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
实例:检测数字键
1 #include <stdio.h>
2 #include <windows.h>
3
4 int main()
5 {
6 while (1)
7 {
8 if (KEY_PRESS(0x30))
9 {
10 printf("0\n");
11 }
12 else if (KEY_PRESS(0x31))
13 {
14 printf("1\n");
15 }
16 else if (KEY_PRESS(0x32))
17 {
18 printf("2\n");
19 }
20 else if (KEY_PRESS(0x33))
21 {
22 printf("3\n");
23 }
24 else if (KEY_PRESS(0x34))
25 {
26 printf("4\n");
27 }
28 else if (KEY_PRESS(0x35))
29 {
30 printf("5\n");
31 }
32 else if (KEY_PRESS(0x36))
33 {
34 printf("6\n");
35 }
36 else if (KEY_PRESS(0x37))
37 {
38 printf("7\n");
39 }
40 else if (KEY_PRESS(0x38))
41 {
42 printf("8\n");
43 }
44 else if (KEY_PRESS(0x39))
45 {
46 printf("9\n");
47 }
48 }
49 return 0;
50 }
- 贪吃蛇游戏设计与分析
7.1 地图
核心设计架构
这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道
该位置的坐标,所以首先介绍一下控制台窗口的坐标知识。控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用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,使
用两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型 wchar_t
和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定
地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。7.1.1 <locale.h>本地化 <locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准中,依赖地区的部分有以下几项: • 数字量的格式 • 货币量的格式 • 字符集 • 日期和时间的表示形式
7.1.2 类项 通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部 分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏, 指定一个类项:
• LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。 •
LC_CTYPE:影响字符处理函数的行为。 • LC_MONETARY:影响货币格式。 • LC_NUMERIC:影响
printf() 的数字格式。 • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。 •
LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境。 每个类项的详细说明
7.1.3 setlocale函数
```cpp
1 char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参
数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值:“C”(正常模式)和" "(本地模式)。
在任意程序执行开始,都会隐藏式执行调用:
```cpp
1 setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale
函数就可以切换到本地模式,这种模式下程序会适应本地环境。
比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
1 setlocale(LC_ALL, " ");//切换到本地环境
7.1.4 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引
号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应
wprintf() 的占位符为 %ls 。
1 #include <stdio.h>
2 #include<locale.h>
3
4 int main() {
5 setlocale(LC_ALL, "");
6 wchar_t ch1 = L'●';
7 wchar_t ch2 = L'比';
8 wchar_t ch3 = L'特';
9 wchar_t ch4 = L'★';
10
11 printf("%c%c\n", 'a', 'b');
12
13 wprintf(L"%lc\n", ch1);
14 wprintf(L"%lc\n", ch2);
15 wprintf(L"%lc\n", ch3);
16 wprintf(L"%lc\n", ch4);
17 return 0;
18 }
输出结果:
从输出的结果来看,我们发现一个普通字符占一个字符的位置
但是打印一个汉字字符,占用2个字符的位置,那么我们如果
要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。
普通字符和宽字符打印出宽度的展示如下:
7.1.5 地图坐标
我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,
如下:
7.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现
蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,
另外一般在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然
后打印★。
7.3 数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,
所以蛇节点结构如下:
1 typedef struct SnakeNode
2 {
3 int x;
4 int y;
5 struct SnakeNode* next;
6 }SnakeNode, * pSnakeNo
管理蛇
1 typedef struct Snake
2 {
3 pSnakeNode _pSnake;//维护整条蛇的指针
4 pSnakeNode _pFood;//维护食物的指针
5 enum DIRECTION _Dir;//蛇头的方向,默认是向右
6 enum GAME_STATUS _Status;//游戏状态
7 int _Socre;//游戏当前获得分数
8 int _foodWeight;//默认每个食物10分
9 int _SleepTime;//每走一步休眠时间
10 }Snake, * pSnake;
蛇的方向,可以一一列举,使用枚举
1 //方向
2 enum DIRECTION
3 {
4 UP = 1,
5 DOWN,
6 LEFT,
7 RIGHT
8 };
游戏状态,可以一一列举,使用枚举
1 //游戏状态
2 enum GAME_STATUS
3 {
4 OK,//正常运行
5 KILL_BY_WALL,//撞墙
6 KILL_BY_SELF,//咬到自己
7 END_NOMAL//正常结束
8 };
7.4
游戏流程设计
- 核心逻辑实现分析
8.1 游戏主逻辑
程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。
主逻辑分为3个过程:
• 游戏开始(GameStart)完成游戏的初始化
• 游戏运行(GameRun)完成游戏运行逻辑的实现
• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
1 #include <locale.h>
2
3 void test()
4 {
5 int ch = 0;
6 srand((unsigned int)time(NULL));
7
8 do
9 {
10 Snake snake = {
0 };
11 GameStart(&snake);
12 GameRun(&snake);
13 GameEnd(&snake);
14 SetPos(20, 15);
15 printf("再来一局吗?(Y/N):");
16 ch = getchar();
17 getchar();//清理\n
18
19 } while (ch == 'Y');
20 SetPos