贪吃蛇项目实战解析

项目实战

  1. 游戏背景​

贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学生的编程能力和逻辑能力。

目录:

  1. 游戏背景

  2. 游戏效果演示

  3. 目标

  4. 定位

  5. 技术要点

  6. 贪吃蛇游戏设计与分析

  7. 贪吃蛇游戏数据结构设计

  8. 相关Win32API介绍

  9. 参考代码

正文开始

  1. 游戏背景

贪吃蛇是久负盛名的游戏,它也和俄罗斯方块,扫雷等游戏位列经典游戏的行列。
在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升编程能力和逻辑能力。
2. 游戏效果演示

  1. 目标

使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。

实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞自身死亡
• 计算得分
• 蛇身加速、减速
• 暂停游戏

  1. 定位
    • 提高对编程的兴趣
    • 对C语言语法做一个基本的巩固。
    • 对游戏开发有兴趣的同学做一个启发。
    • 项目适合:C语言学完的同学,有一定的代码能力,初步接触数据结构中的链表。

  2. 技术要点

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。

  1. 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  }
  1. 贪吃蛇游戏设计与分析

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

游戏流程设计

  1. 核心逻辑实现分析

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
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值