【项目实践】贪吃蛇

07-08 1329阅读

  • 一、游戏效果展示
  • 二、博客目标
  • 三、使用到的知识
  • 四、Win32 API 介绍
    • 4.1 WIn32 API
    • 4.2 控制台程序
    • 4.3 控制屏幕上的坐标COORD
    • 4.4 GetStdHandle
    • 4.5 GetConsoleCursorInfo
      • 4.5.1 CONSOLE_CURSOR_INFO
      • 4.6 SetConsoleCursorInfo
      • 4.7 SetConsoleCursorPosition
      • 4.8 GetAsyncKeyState
      • 五、贪吃蛇游戏设计与分析
        • 5.1 地图
          • 5.1.1 本地化
          • 5.1.2 类项
          • 5.1.3 setlocale函数
          • 5.1.4 宽字符打印
          • 5.1.5 地图坐标
          • 5.2 蛇身与食物
          • 5.3 数据结构设计
          • 5.4 游戏流程设计
          • 六、核心逻辑实现分析
            • 6.1 游戏主逻辑
            • 6.2 游戏开始
              • 6.2.1 打印欢迎界面
              • 6.2.2 创建地图
              • 6.2.3 创建蛇身
              • 6.2.4 创建第一个食物
              • 6.3 游戏运行
                • 6.3.1 KEY_PRESS
                • 6.3.2 打印右侧的帮助信息
                • 6.3.3 蛇身的移动
                • 6.4游戏结束
                • 七、参考代码
                • 八、控制台设置

                  一、游戏效果展示

                  贪吃蛇效果展示

                  【项目实践】贪吃蛇

                  【项目实践】贪吃蛇

                  【项目实践】贪吃蛇

                  【项目实践】贪吃蛇

                  二、博客目标

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

                  实现基本的功能:

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

                    三、使用到的知识

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

                    四、Win32 API介绍

                    本次实现贪吃蛇会使用到的⼀些Win32 API知识,接下来我们就学习⼀下。

                    4.1Win32 API

                    Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为Application Programming Interface,简称 API 函数。WIN32 API 也就是Microsoft Windows 32位平台的应用程序编程接口。

                    4.2 控制台程序

                    平常我们运行起来的黑框程序其实就是控制台程序。能在控制台窗口执行的命令,也可以调用C语言函数system来执行。

                    4.3 控制台屏幕上的坐标COORD

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

                    【项目实践】贪吃蛇

                    COORD类型的声明:

                    typedef struct _COORD 
                    {
                    	 SHORT X;
                    	 SHORT Y;
                    } COORD, *PCOORD;
                    

                    4.4 GetStdHandle

                    GetStdHandle是⼀个Windows API函数。它用于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。

                    HANDLE GetStdHandle(DWORD nStdHandle);
                    

                    4.5 GetConsoleCursorInfo

                    检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息。

                    BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
                    //PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标.
                    

                    4.5.1 CONSOLE_CURSOR_INFO

                    这个结构体,包含有关控制台光标的信息

                    typedef struct _CONSOLE_CURSOR_INFO 
                    {
                    	 DWORD dwSize;
                    	 BOOL bVisible;
                    } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
                    
                    • dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
                    • bVisible,游标的可见性。如果光标可见,则此成员为 TRUE。
                      CursorInfo.bVisible = false; //隐藏控制台光标 
                      

                      4.6SetConsoleCursorInfo

                      设置指定控制台屏幕缓冲区的光标的大小和可见性。

                      BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);
                      

                      4.7 SetConsoleCursorPosition

                      设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。

                      BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD pos);
                      

                      4.8 GetAsyncKeyState

                      获取按键情况,GetAsyncKeyState的函数原型如下:

                      SHORT GetAsyncKeyState(int vKey);
                      

                      将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

                      GetAsyncKeyState 的返回值是short类型,在上⼀次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。

                      虚拟键码

                      五、贪吃蛇游戏设计与分析

                      5.1 地图

                      这里不得不讲⼀下控制台窗口的⼀些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍⼀下控制台窗口的坐标知识。

                      控制台窗口的坐标,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。

                      在游戏地图上,我们打印墙体使用宽字符 □ ,打印蛇使用宽字符 ● ,打印食物使用宽字符 ★

                      普通的字符是占⼀个字节的,这类宽字符是占用2个字节。

                      这里再简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。

                      C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。

                      C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的 é 的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不⼀样。比如,130在法语编码中代表了 é ,在俄语编码中又会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0–127表示的符号是⼀样的,不⼀样的只是128–255的这⼀段。

                      至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示256 x 256 = 65536 个符号。

                      后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的至支持。比如:加入了宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

                      5.1.1 本地化

                      提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。

                      在标准中,依赖地区的部分有以下几项:

                      • 数字量的格式
                      • 货币量的格式
                      • 字符集
                      • 日期和时间的表示形式

                        5.1.2 类项

                        通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的⼀个宏,

                        指定⼀个类项:

                        • LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。
                        • LC_CTYPE:影响字符处理函数的行为。
                        • LC_MONETARY:影响货币格式。
                        • LC_NUMERIC:影响 printf() 的数字格式。
                        • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
                        • LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境。

                          5.1.3 setlocale函数

                          char* setlocale (int category, const char* locale);
                          

                          setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。

                          setlocale 的第⼀个参数可以是前面说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。

                          C标准给第二个参数仅定义了2种可能取值:“C”(正常模式)和" "(本地模式)。

                          当程序运行起来后想改变地区,就只能显示调用 setlocale 函数。用" "作为第2个参数,调用 setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。

                          setlocale(LC_ALL, " ");//切换到本地环境 
                          

                          5.1.4 宽字符的打印

                          那如果想在屏幕上打印宽字符,怎么打印呢?

                          宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应wprintf() 的占位符为 %ls 。

                          【项目实践】贪吃蛇

                          5.1.5 地图坐标

                          我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,如下:

                          【项目实践】贪吃蛇

                          5.2 蛇身与食物

                          初始化状态,假设蛇的蛇度是5,蛇身的每个节点是 ● ,在固定的⼀个坐标处,比如(24,5)处开始出现蛇,连续5个节点。

                          注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。

                          关于食物,就是在墙体内随机生成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

                          【项目实践】贪吃蛇

                          5.3 数据结构设计

                          在游戏运行的过程中,蛇每次吃⼀个食物物,蛇的身体就会变长⼀节,如果我们使用链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:

                          typedef struct SnakeNode
                          {
                          	 int x;
                          	 int y;
                          	 struct SnakeNode* next;
                          }SnakeNode, * pSnakeNode;
                          

                          要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

                          typedef struct Snake
                          {
                          	 pSnakeNode _pSnake;//维护整条蛇的指针 
                          	 pSnakeNode _pFood;//维护⻝物的指针 
                          	 enum DIRECTION _Dir;//蛇头的⽅向,默认是向右 
                          	 enum GAME_STATUS _Status;//游戏状态 
                          	 int _Socre;//游戏当前获得分数 
                          	 int _foodWeight;//默认每个⻝物分数 
                          	 int _SleepTime;//每⾛⼀步休眠时间 
                          }Snake, * pSnake;
                          

                          蛇的方向,可以⼀⼀列举,使用枚举

                          //⽅向 
                          enum DIRECTION
                          { 
                          	UP = 1,	//向上
                          	DOWN,	//向下
                          	LEFT,	//向左
                          	RIGHT	//向右
                          };
                          

                          游戏状态,可以⼀⼀列举,使用枚举

                          //游戏状态 
                          enum GAME_STATUS
                          {
                          	 OK,//正常运⾏ 
                          	 KILL_BY_WALL,//撞墙 
                          	 KILL_BY_SELF,//咬到⾃⼰ 
                          	 END_NOMAL//正常结束 
                          };
                          

                          5.4 游戏流程设计

                          【项目实践】贪吃蛇

                          六、核心逻辑实现分析

                          6.1 游戏主逻辑

                          程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。

                          主逻辑分为3个过程:

                          • 游戏开始(GameStart)完成游戏的初始化
                          • 游戏运行(GameRun)完成游戏运行逻辑的实现
                          • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
                            void game()
                            {
                            	int ch = 0;
                            	srand((unsigned int)time(NULL));
                            	do
                            	{
                            		Snake snake = { 0 };
                            		GameStart(&snake);
                            		GameRun(&snake);
                            		GameEnd(&snake);
                            		SetPos(20, 15);
                            		printf("再来⼀局吗?(Y/N):");
                            		ch = getchar();
                            		getchar();//清理\n 
                            	} while (ch == 'Y' || ch == 'y');
                            	SetPos(0, 27);
                            }
                            int main()
                            {
                            	//修改当前地区为本地模式,为了支持中文宽字符的打印 
                            	setlocale(LC_ALL, "");
                            	//测试逻辑 
                            	game();
                            	return 0;
                            }
                            

                            6.2游戏开始(GameStart)

                            这个模块完成游戏的初始化任务:

                            • 控制台窗口大小的设置
                            • 控制台窗口名字的设置
                            • 光标的隐藏
                            • 打印欢迎界面
                            • 创建地图
                            • 初始化蛇
                            • 创建第一个食物
                              void GameStart(pSnake ps)
                              {
                              	//设置控制台窗口的大小,30行,100列 
                              	//mode 为DOS命令 
                              	system("mode con cols=100 lines=30");
                              	//设置cmd窗口名称 
                              	system("title 贪吃蛇");
                              	//获取标准输出的句柄(用来标识不同设备的数值) 
                              	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
                              	//影藏光标操作 
                              	CONSOLE_CURSOR_INFO CursorInfo;
                              	GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息 
                              	CursorInfo.bVisible = false; //隐藏控制台光标 
                              	SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态 
                              	//打印欢迎界面
                              	WelcomeToGame();
                              	//打印地图 
                              	CreateMap();
                              	//初始化蛇 
                              	InitSnake(ps);
                              	//创建食物 
                              	CreateFood(ps);
                              }
                              

                              6.2.1 打印欢迎界面

                              在游戏正式开始之前,做⼀些功能提醒.

                              //设置光标的坐标 
                              void SetPos(short x, short y)
                              {
                              	COORD pos = { x, y };
                              	HANDLE hOutput = NULL;
                              	//获取标准输出的句柄(用来标识不同设备的数值) 
                              	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
                              	//设置标准输出上光标的位置为pos 
                              	SetConsoleCursorPosition(hOutput, pos);
                              }
                              //打印欢迎进入游戏
                              void WelcomeToGame()
                              {
                              	SetPos(40, 15);
                              	printf("欢迎进入贪吃蛇游戏");
                              	SetPos(42, 25);//让按任意键继续的出现的位置好看点 
                              	system("pause");
                              	system("cls");
                              	SetPos(20, 12);
                              	printf("用 ↑  ↓  ←   → 分别控制蛇的移动, K为加速,L为减速");
                              	SetPos(37, 14);
                              	printf("加速将能得到更高的分数");
                              	SetPos(40, 25);//让按任意键继续的出现的位置好看点 
                              	system("pause");
                              	system("cls");
                              }
                              

                              6.2.2 创建地图

                              创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L。

                              打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

                              墙体打印的宽字符:

                              #define WALL L'□' 
                              
                              //创建地图
                              void CreateMap()
                              {
                              	int i = 0;
                              	//上(0,0)-(56, 0) 
                              	SetPos(0, 0);
                              	for (i = 0; i  
                              

                              6.2.3 初始化蛇身

                              蛇最开始长度为5节,每节对应链表的⼀个节点,蛇身的每⼀个节点都有自己的坐标。

                              创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每⼀节打印在屏幕上。

                              • 蛇的初始位置从(24,5)开始。再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。
                              • 游戏状态是:OK
                              • 蛇的移动速度:200毫秒
                              • 蛇的默认方向:RIGHT
                              • 初始成绩:0
                              • 每个食物的分数:10

                                蛇身打印的宽字符:

                                #define BODY L'●' 
                                
                                void InitSnake(pSnake ps)
                                {
                                	pSnakeNode cur = NULL;
                                	int i = 0;
                                	//创建蛇身节点,并初始化坐标 
                                	//头插法 
                                	for (i = 0; i next = NULL;
                                		cur->x = POS_X + i * 2;
                                		cur->y = POS_Y;
                                		//头插法 
                                		if (ps->_pSnake == NULL)
                                		{
                                			ps->_pSnake = cur;
                                		}
                                		else
                                		{
                                			cur->next = ps->_pSnake;
                                			ps->_pSnake = cur;
                                		}
                                	}
                                	//打印蛇的身体 
                                	cur = ps->_pSnake;
                                	while (cur)
                                	{
                                		SetPos(cur->x, cur->y);
                                		wprintf(L"%c", BODY);
                                		cur = cur->next;
                                	}
                                	//初始化贪吃蛇数据 
                                	ps->_SleepTime = 200;
                                	ps->_Socre = 0;
                                	ps->_Status = OK;
                                	ps->_Dir = RIGHT;
                                	ps->_foodWeight = 10;
                                }
                                

                                6.2.4 创建第一个食物

                                • 先随机生成食物的坐标
                                  • x坐标必须是2的倍数
                                  • 食物的坐标不能和蛇身每个节点的坐标重复
                                  • 创建食物节点,打印食物

                                    食物打印的宽字符:

                                    #define FOOD L'★'
                                    
                                    //创建食物
                                    void CreateFood(pSnake ps)
                                    {
                                    	int x = 0;
                                    	int y = 0;
                                    again:
                                    	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。 
                                    	do
                                    	{
                                    		x = rand() % 53 + 2;
                                    		y = rand() % 25 + 1;
                                    	} while (x % 2 != 0);
                                    	//获取指向蛇头的指针 
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//食物不能和蛇身冲突 
                                    	while (cur)
                                    	{
                                    		if (cur->x == x && cur->y == y)
                                    		{
                                    			goto again;
                                    		}
                                    		cur = cur->next;
                                    	}
                                    	//创建食物
                                    	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); 
                                    	if (pFood == NULL)
                                    	{
                                    		perror("CreateFood::malloc()");
                                    		return;
                                    	}
                                    	else
                                    	{
                                    		pFood->x = x;
                                    		pFood->y = y;
                                    		SetPos(pFood->x, pFood->y);
                                    		wprintf(L"%c", FOOD);
                                    		ps->_pFood = pFood;
                                    	}
                                    }
                                    

                                    6.3 游戏运行(GameRun)

                                    游戏运行期间,右侧打印帮助信息,提示玩家。

                                    根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。

                                    如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

                                    虚拟按键上面给过。

                                    void GameRun(pSnake ps)
                                    {
                                    	//打印右侧帮助信息 
                                    	PrintHelpInfo();
                                    	do
                                    	{
                                     		SetPos(62, 5);
                                    		printf("得分:%d  ", ps->_Socre);
                                    		printf("每个食物得分:%d分", ps->_foodWeight);
                                    		//按键判断,上下左右WSAD或者数字8246,虚拟键
                                    		if ((KEY_PRESS(0x57) || KEY_PRESS(VK_NUMPAD8)) && ps->_Dir != DOWN)
                                    		{
                                    			ps->_Dir = UP;
                                    		}
                                    		else if ((KEY_PRESS(0x53) || KEY_PRESS(VK_NUMPAD2)) && ps->_Dir != UP)
                                    		{
                                    			ps->_Dir = DOWN;
                                    		}
                                    		else if ((KEY_PRESS(0x41) || KEY_PRESS(VK_NUMPAD4)) && ps->_Dir != RIGHT)
                                    		{
                                    			ps->_Dir = LEFT;
                                    		}
                                    		else if ((KEY_PRESS(0x44) || KEY_PRESS(VK_NUMPAD6)) && ps->_Dir != LEFT)
                                    		{
                                    			ps->_Dir = RIGHT;
                                    		}
                                    		else if (KEY_PRESS(VK_SPACE))
                                    		{
                                    			pause();
                                    		}
                                    		else if (KEY_PRESS(VK_ESCAPE))
                                    		{
                                    			ps->_Status = END_NOMAL;
                                    			break;
                                    		}
                                    		else if (KEY_PRESS(0x4B))
                                    		{
                                    			if (ps->_SleepTime >= 50)
                                    			{
                                    				ps->_SleepTime -= 30;
                                    				ps->_foodWeight += 2;
                                    			}
                                    		}
                                    		else if (KEY_PRESS(0x4C))
                                    		{
                                    			if (ps->_SleepTime _SleepTime += 30;
                                    				ps->_foodWeight -= 2;
                                    				if (ps->_SleepTime == 350)
                                    				{
                                    					ps->_foodWeight = 1;
                                    				}
                                    			}
                                    		}
                                    		//蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快 
                                    		Sleep(ps->_SleepTime);
                                    		SnakeMove(ps);
                                    	} while (ps->_Status == OK);
                                    }
                                    

                                    6.3.1 KEY_PRESS

                                    检测按键状态,我们封装了⼀个宏

                                    #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
                                    

                                    6.3.2打印右侧的帮助信息

                                    //打印提示信息
                                    void PrintHelpInfo()
                                    {	 
                                    	SetPos(62, 7);
                                    	printf("不能穿墙,不能咬到自己");
                                    	SetPos(62, 9);
                                    	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动.");
                                    	SetPos(62, 11);
                                    	printf("K 为加速,L 为减速");
                                    	SetPos(62, 13);
                                    	printf("ESC:退出游戏.");
                                    	SetPos(62, 15);
                                    	printf("space:暂停游戏.");
                                    	SetPos(84, 20);
                                    	printf("卡戎-caryon@版权");
                                    }
                                    

                                    6.3.3蛇身的移动

                                    先创建下⼀个节点,根据移动方向和蛇头的坐标,蛇移动到下⼀个位置的坐标。

                                    确定了下⼀个位置后,看下⼀个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。

                                    蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。

                                    //暂停 
                                    void pause()
                                    {
                                    	while (1)
                                    	{
                                    		Sleep(200);
                                    		if (KEY_PRESS(VK_SPACE))
                                    		{
                                    			break;
                                    		}
                                    	}
                                    }
                                    //pSnakeNode psn 是下一个节点的地址 
                                    //pSnake ps 维护蛇的指针 
                                    int NextIsFood(pSnakeNode psn, pSnake ps)
                                    {
                                    	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
                                    }
                                    //pSnakeNode psn 是下一个节点的地址 
                                    //pSnake ps 维护蛇的指针 
                                    void EatFood(pSnakeNode psn, pSnake ps)
                                    {
                                    	//头插法 
                                    	psn->next = ps->_pSnake;
                                    	ps->_pSnake = psn;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//打印蛇
                                    	while (cur)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%c", BODY);
                                    		cur = cur->next;
                                    	}
                                    	ps->_Socre += ps->_foodWeight;
                                    	free(ps->_pFood);
                                    	CreateFood(ps);
                                    }
                                    //pSnakeNode psn 是下一个节点的地址 
                                    //pSnake ps 维护蛇的指针 
                                    void NoFood(pSnakeNode psn, pSnake ps)
                                    {
                                    	//头插法 
                                    	psn->next = ps->_pSnake;
                                    	ps->_pSnake = psn;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//打印蛇 
                                    	while (cur->next->next)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%c", BODY);
                                    		cur = cur->next;
                                    	}
                                    	//最后一个位置打印空格,然后释放节点 
                                    	SetPos(cur->next->x, cur->next->y);
                                    	printf("  ");
                                    	free(cur->next);
                                    	cur->next = NULL;
                                    }
                                    //pSnake ps 维护蛇的指针 
                                    int KillByWall(pSnake ps)
                                    {
                                    	if ((ps->_pSnake->x == 0)
                                    		|| (ps->_pSnake->x == 56)
                                    		|| (ps->_pSnake->y == 0)
                                    		|| (ps->_pSnake->y == 26))
                                    	{
                                    		ps->_Status = KILL_BY_WALL;
                                    		return 1;
                                    	}
                                    	return 0;
                                    }
                                    //pSnake ps 维护蛇的指针 
                                    int KillBySelf(pSnake ps)
                                    {
                                    	pSnakeNode cur = ps->_pSnake->next;
                                    	while (cur)
                                    	{
                                    		if ((ps->_pSnake->x == cur->x)
                                    			&& (ps->_pSnake->y == cur->y))
                                    		{
                                    			ps->_Status = KILL_BY_SELF;
                                    			return 1;
                                    		}
                                    		cur = cur->next;
                                    	}
                                    	return 0;
                                    }
                                    //蛇的移动
                                    void SnakeMove(pSnake ps)
                                    {
                                    	//创建下一个节点 
                                    	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
                                    	if (pNextNode == NULL)
                                    	{
                                    		perror("SnakeMove()::malloc()");
                                    		return;
                                    	}
                                    	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定 
                                    	switch (ps->_Dir)
                                    	{
                                    	case UP:
                                    		pNextNode->x = ps->_pSnake->x;
                                    		pNextNode->y = ps->_pSnake->y - 1;
                                    		break;
                                    	case DOWN:
                                    		pNextNode->x = ps->_pSnake->x;
                                    		pNextNode->y = ps->_pSnake->y + 1;
                                    		break;
                                    	case LEFT:
                                    		pNextNode->x = ps->_pSnake->x - 2;
                                    		pNextNode->y = ps->_pSnake->y;
                                    		break;
                                    	case RIGHT:
                                    		pNextNode->x = ps->_pSnake->x + 2;
                                    		pNextNode->y = ps->_pSnake->y;
                                    		break;
                                    	}
                                    	//如果下一个位置就是食物
                                    	if (NextIsFood(pNextNode, ps))
                                    	{
                                    		EatFood(pNextNode, ps);
                                    	}
                                    	else//如果没有食物 
                                    	{
                                    		NoFood(pNextNode, ps);
                                    	}
                                    	KillByWall(ps);
                                    	KillBySelf(ps);
                                    }
                                    

                                    6.4 游戏结束

                                    游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。

                                    //游戏结束
                                    void GameEnd(pSnake ps)
                                    {
                                    	pSnakeNode cur = ps->_pSnake;
                                    	SetPos(24, 12);
                                    	switch (ps->_Status)
                                    	{
                                    	case END_NOMAL:
                                    		printf("您主动退出游戏\n");
                                    		break;
                                    	case KILL_BY_SELF:
                                    		printf("您撞上自己了 ,游戏结束!\n");
                                    		break;
                                    	case KILL_BY_WALL:
                                    		printf("您撞墙了,游戏结束!\n");
                                    		break;
                                    	}
                                    	//释放蛇身的节点 
                                    	while (cur)
                                    	{
                                    		pSnakeNode del = cur;
                                    		cur = cur->next;
                                    		free(del);
                                    	}
                                    }
                                    

                                    七、参考代码

                                    //test.c
                                    #include"snake.h"
                                    #include"Game_Start.h"
                                    #include 
                                    void game()
                                    {
                                    	int ch = 0;
                                    	srand((unsigned int)time(NULL));
                                    	do
                                    	{
                                    		Snake snake = { 0 };
                                    		GameStart(&snake);
                                    		GameRun(&snake);
                                    		GameEnd(&snake);
                                    		SetPos(20, 15);
                                    		printf("再来⼀局吗?(Y/N):");
                                    		ch = getchar();
                                    		getchar();//清理\n 
                                    	} while (ch == 'Y' || ch == 'y');
                                    	SetPos(0, 27);
                                    }
                                    int main()
                                    {
                                    	//修改当前地区为本地模式,为了支持中文宽字符的打印 
                                    	setlocale(LC_ALL, "");
                                    	//测试逻辑 
                                    	game();
                                    	return 0;
                                    }
                                    
                                    //snake.h
                                    #pragma once
                                    #include 
                                    #include 
                                    #include 
                                    #include
                                    #include
                                    //判断按键是否被按下过
                                    #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
                                    //方向
                                    enum DIRECTION
                                    {
                                    	UP = 1,
                                    	DOWN,
                                    	LEFT,
                                    	RIGHT
                                    };
                                    //游戏状态 
                                    enum GAME_STATUS
                                    {
                                    	OK,//正常运⾏ 
                                    	KILL_BY_WALL,//撞墙 
                                    	KILL_BY_SELF,//咬到⾃⼰ 
                                    	END_NOMAL//正常结束 
                                    };
                                    #define WALL  L'□' 
                                    #define BODY L'●' 
                                    #define FOOD L'★' 
                                    //蛇的初始位置 
                                    #define POS_X 24
                                    #define POS_Y 5
                                    //蛇身节点 
                                    typedef struct SnakeNode
                                    {
                                    	int x;
                                    	int y;
                                    	struct SnakeNode* next;
                                    }SnakeNode, * pSnakeNode;
                                    //蛇
                                    typedef struct Snake
                                    {
                                    	pSnakeNode _pSnake;//维护整条蛇的指针 
                                    	pSnakeNode _pFood;//维护食物的指针 
                                    	enum DIRECTION _Dir;//蛇头的方向默认是向右 
                                    	enum GAME_STATUS _Status;//游戏状态 
                                    	int _Socre;//当前获得分数 
                                    	int _foodWeight;//默认每个食物10分 
                                    	int _SleepTime;//每走一步休眠时间 
                                    }Snake, * pSnake;
                                    //游戏开始前的初始化 
                                    void GameStart(pSnake ps);
                                    //游戏运行过程 
                                    void GameRun(pSnake ps);
                                    //游戏结束 
                                    void GameEnd(pSnake ps);
                                    
                                    //snack.c
                                    #include"snake.h"
                                    #include"Game_Start.h"
                                    #include"Game_Run.h"
                                    void GameStart(pSnake ps)
                                    {
                                    	//设置控制台窗口的大小,30行,100列 
                                    	//mode 为DOS命令 
                                    	system("mode con cols=100 lines=30");
                                    	//设置cmd窗口名称 
                                    	system("title 贪吃蛇");
                                    	//获取标准输出的句柄(用来标识不同设备的数值) 
                                    	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
                                    	//影藏光标操作 
                                    	CONSOLE_CURSOR_INFO CursorInfo;
                                    	GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息 
                                    	CursorInfo.bVisible = false; //隐藏控制台光标 
                                    	SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态 
                                    	//打印欢迎界面
                                    	WelcomeToGame();
                                    	//打印地图 
                                    	CreateMap();
                                    	//初始化蛇 
                                    	InitSnake(ps);
                                    	//创建食物 
                                    	CreateFood(ps);
                                    }
                                    void GameRun(pSnake ps)
                                    {
                                    	//打印右侧帮助信息 
                                    	PrintHelpInfo();
                                    	do
                                    	{
                                     		SetPos(62, 5);
                                    		printf("得分:%d  ", ps->_Socre);
                                    		printf("每个食物得分:%d分", ps->_foodWeight);
                                    		//按键判断,上下左右WSAD或者数字8246,虚拟键
                                    		if ((KEY_PRESS(0x57) || KEY_PRESS(VK_NUMPAD8)) && ps->_Dir != DOWN)
                                    		{
                                    			ps->_Dir = UP;
                                    		}
                                    		else if ((KEY_PRESS(0x53) || KEY_PRESS(VK_NUMPAD2)) && ps->_Dir != UP)
                                    		{
                                    			ps->_Dir = DOWN;
                                    		}
                                    		else if ((KEY_PRESS(0x41) || KEY_PRESS(VK_NUMPAD4)) && ps->_Dir != RIGHT)
                                    		{
                                    			ps->_Dir = LEFT;
                                    		}
                                    		else if ((KEY_PRESS(0x44) || KEY_PRESS(VK_NUMPAD6)) && ps->_Dir != LEFT)
                                    		{
                                    			ps->_Dir = RIGHT;
                                    		}
                                    		else if (KEY_PRESS(VK_SPACE))
                                    		{
                                    			pause();
                                    		}
                                    		else if (KEY_PRESS(VK_ESCAPE))
                                    		{
                                    			ps->_Status = END_NOMAL;
                                    			break;
                                    		}
                                    		else if (KEY_PRESS(0x4B))
                                    		{
                                    			if (ps->_SleepTime >= 50)
                                    			{
                                    				ps->_SleepTime -= 30;
                                    				ps->_foodWeight += 2;
                                    			}
                                    		}
                                    		else if (KEY_PRESS(0x4C))
                                    		{
                                    			if (ps->_SleepTime _SleepTime += 30;
                                    				ps->_foodWeight -= 2;
                                    				if (ps->_SleepTime == 350)
                                    				{
                                    					ps->_foodWeight = 1;
                                    				}
                                    			}
                                    		}
                                    		//蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快 
                                    		Sleep(ps->_SleepTime);
                                    		SnakeMove(ps);
                                    	} while (ps->_Status == OK);
                                    }
                                    //游戏结束
                                    void GameEnd(pSnake ps)
                                    {
                                    	pSnakeNode cur = ps->_pSnake;
                                    	SetPos(24, 12);
                                    	switch (ps->_Status)
                                    	{
                                    	case END_NOMAL:
                                    		printf("您主动退出游戏\n");
                                    		break;
                                    	case KILL_BY_SELF:
                                    		printf("您撞上自己了 ,游戏结束!\n");
                                    		break;
                                    	case KILL_BY_WALL:
                                    		printf("您撞墙了,游戏结束!\n");
                                    		break;
                                    	}
                                    	//释放蛇身的节点 
                                    	while (cur)
                                    	{
                                    		pSnakeNode del = cur;
                                    		cur = cur->next;
                                    		free(del);
                                    	}
                                    }
                                    
                                    //Game_Start.h
                                    #pragma once
                                    #include"snake.h"
                                    //设置光标的坐标 
                                    void SetPos(short x, short y);
                                    //欢迎界面
                                    void WelcomeToGame();
                                    //打印帮助信息 
                                    void PrintHelpInfo();
                                    //创建地图 
                                    void CreateMap();
                                    //初始化蛇 
                                    void InitSnake(pSnake ps);
                                    //创建食物
                                    void CreateFood(pSnake ps);
                                    
                                    //Game_Start.c
                                    #include"Game_Start.h"
                                    //设置光标的坐标 
                                    void SetPos(short x, short y)
                                    {
                                    	COORD pos = { x, y };
                                    	HANDLE hOutput = NULL;
                                    	//获取标准输出的句柄(用来标识不同设备的数值) 
                                    	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
                                    	//设置标准输出上光标的位置为pos 
                                    	SetConsoleCursorPosition(hOutput, pos);
                                    }
                                    //打印欢迎进入游戏
                                    void WelcomeToGame()
                                    {
                                    	SetPos(40, 15);
                                    	printf("欢迎进入贪吃蛇游戏");
                                    	SetPos(42, 25);//让按任意键继续的出现的位置好看点 
                                    	system("pause");
                                    	system("cls");
                                    	SetPos(20, 12);
                                    	printf("用 ↑  ↓  ←   → 分别控制蛇的移动, K为加速,L为减速");
                                    	SetPos(37, 14);
                                    	printf("加速将能得到更高的分数");
                                    	SetPos(40, 25);//让按任意键继续的出现的位置好看点 
                                    	system("pause");
                                    	system("cls");
                                    }
                                    //创建地图
                                    void CreateMap()
                                    {
                                    	int i = 0;
                                    	//上(0,0)-(56, 0) 
                                    	SetPos(0, 0);
                                    	for (i = 0; i next = NULL;
                                    		cur->x = POS_X + i * 2;
                                    		cur->y = POS_Y;
                                    		//头插法 
                                    		if (ps->_pSnake == NULL)
                                    		{
                                    			ps->_pSnake = cur;
                                    		}
                                    		else
                                    		{
                                    			cur->next = ps->_pSnake;
                                    			ps->_pSnake = cur;
                                    		}
                                    	}
                                    	//打印蛇的身体 
                                    	cur = ps->_pSnake;
                                    	while (cur)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%c", BODY);
                                    		cur = cur->next;
                                    	}
                                    	//初始化贪吃蛇数据 
                                    	ps->_SleepTime = 200;
                                    	ps->_Socre = 0;
                                    	ps->_Status = OK;
                                    	ps->_Dir = RIGHT;
                                    	ps->_foodWeight = 10;
                                    }
                                    //创建食物
                                    void CreateFood(pSnake ps)
                                    {
                                    	int x = 0;
                                    	int y = 0;
                                    again:
                                    	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对齐。 
                                    	do
                                    	{
                                    		x = rand() % 53 + 2;
                                    		y = rand() % 25 + 1;
                                    	} while (x % 2 != 0);
                                    	//获取指向蛇头的指针 
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//食物不能和蛇身冲突 
                                    	while (cur)
                                    	{
                                    		if (cur->x == x && cur->y == y)
                                    		{
                                    			goto again;
                                    		}
                                    		cur = cur->next;
                                    	}
                                    	//创建食物
                                    	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); 
                                    	if (pFood == NULL)
                                    	{
                                    		perror("CreateFood::malloc()");
                                    		return;
                                    	}
                                    	else
                                    	{
                                    		pFood->x = x;
                                    		pFood->y = y;
                                    		SetPos(pFood->x, pFood->y);
                                    		wprintf(L"%c", FOOD);
                                    		ps->_pFood = pFood;
                                    	}
                                    }
                                    
                                    //Game_Run.h
                                    #pragma once
                                    #include"snake.h"
                                    //打印提示信息
                                    void PrintHelpInfo();
                                    //暂停响应 
                                    void pause();
                                    //下一个节点是食物
                                    int NextIsFood(pSnakeNode psn, pSnake ps);
                                    //吃食物 
                                    void EatFood(pSnakeNode psn, pSnake ps);
                                    //不吃食物
                                    void NoFood(pSnakeNode psn, pSnake ps);
                                    //撞墙检测 
                                    int KillByWall(pSnake ps);
                                    //撞自身检测 
                                    int KillBySelf(pSnake ps);
                                    //蛇的移动 
                                    void SnakeMove(pSnake ps);
                                    //游戏初始化 
                                    void GameStart(pSnake ps);
                                    //游戏运行
                                    void GameRun(pSnake ps);
                                    //游戏结束 
                                    void GameEnd(pSnake ps);
                                    
                                    //Game_Run.c
                                    #include"Game_Run.h"
                                    #include"Game_Start.h"
                                    //打印提示信息
                                    void PrintHelpInfo()
                                    {	 
                                    	SetPos(62, 7);
                                    	printf("不能穿墙,不能咬到自己");
                                    	SetPos(62, 9);
                                    	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动.");
                                    	SetPos(62, 11);
                                    	printf("K 为加速,L 为减速");
                                    	SetPos(62, 13);
                                    	printf("ESC:退出游戏.");
                                    	SetPos(62, 15);
                                    	printf("space:暂停游戏.");
                                    	SetPos(84, 20);
                                    	printf("卡戎-caryon@版权");
                                    }
                                    //暂停 
                                    void pause()
                                    {
                                    	while (1)
                                    	{
                                    		Sleep(200);
                                    		if (KEY_PRESS(VK_SPACE))
                                    		{
                                    			break;
                                    		}
                                    	}
                                    }
                                    //pSnakeNode psn 是下一个节点的地址 
                                    //pSnake ps 维护蛇的指针 
                                    int NextIsFood(pSnakeNode psn, pSnake ps)
                                    {
                                    	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
                                    }
                                    //pSnakeNode psn 是下一个节点的地址 
                                    //pSnake ps 维护蛇的指针 
                                    void EatFood(pSnakeNode psn, pSnake ps)
                                    {
                                    	//头插法 
                                    	psn->next = ps->_pSnake;
                                    	ps->_pSnake = psn;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//打印蛇
                                    	while (cur)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%c", BODY);
                                    		cur = cur->next;
                                    	}
                                    	ps->_Socre += ps->_foodWeight;
                                    	free(ps->_pFood);
                                    	CreateFood(ps);
                                    }
                                    //pSnakeNode psn 是下一个节点的地址 
                                    //pSnake ps 维护蛇的指针 
                                    void NoFood(pSnakeNode psn, pSnake ps)
                                    {
                                    	//头插法 
                                    	psn->next = ps->_pSnake;
                                    	ps->_pSnake = psn;
                                    	pSnakeNode cur = ps->_pSnake;
                                    	//打印蛇 
                                    	while (cur->next->next)
                                    	{
                                    		SetPos(cur->x, cur->y);
                                    		wprintf(L"%c", BODY);
                                    		cur = cur->next;
                                    	}
                                    	//最后一个位置打印空格,然后释放节点 
                                    	SetPos(cur->next->x, cur->next->y);
                                    	printf("  ");
                                    	free(cur->next);
                                    	cur->next = NULL;
                                    }
                                    //pSnake ps 维护蛇的指针 
                                    int KillByWall(pSnake ps)
                                    {
                                    	if ((ps->_pSnake->x == 0)
                                    		|| (ps->_pSnake->x == 56)
                                    		|| (ps->_pSnake->y == 0)
                                    		|| (ps->_pSnake->y == 26))
                                    	{
                                    		ps->_Status = KILL_BY_WALL;
                                    		return 1;
                                    	}
                                    	return 0;
                                    }
                                    //pSnake ps 维护蛇的指针 
                                    int KillBySelf(pSnake ps)
                                    {
                                    	pSnakeNode cur = ps->_pSnake->next;
                                    	while (cur)
                                    	{
                                    		if ((ps->_pSnake->x == cur->x)
                                    			&& (ps->_pSnake->y == cur->y))
                                    		{
                                    			ps->_Status = KILL_BY_SELF;
                                    			return 1;
                                    		}
                                    		cur = cur->next;
                                    	}
                                    	return 0;
                                    }
                                    //蛇的移动
                                    void SnakeMove(pSnake ps)
                                    {
                                    	//创建下一个节点 
                                    	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
                                    	if (pNextNode == NULL)
                                    	{
                                    		perror("SnakeMove()::malloc()");
                                    		return;
                                    	}
                                    	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定 
                                    	switch (ps->_Dir)
                                    	{
                                    	case UP:
                                    		pNextNode->x = ps->_pSnake->x;
                                    		pNextNode->y = ps->_pSnake->y - 1;
                                    		break;
                                    	case DOWN:
                                    		pNextNode->x = ps->_pSnake->x;
                                    		pNextNode->y = ps->_pSnake->y + 1;
                                    		break;
                                    	case LEFT:
                                    		pNextNode->x = ps->_pSnake->x - 2;
                                    		pNextNode->y = ps->_pSnake->y;
                                    		break;
                                    	case RIGHT:
                                    		pNextNode->x = ps->_pSnake->x + 2;
                                    		pNextNode->y = ps->_pSnake->y;
                                    		break;
                                    	}
                                    	//如果下一个位置就是食物
                                    	if (NextIsFood(pNextNode, ps))
                                    	{
                                    		EatFood(pNextNode, ps);
                                    	}
                                    	else//如果没有食物 
                                    	{
                                    		NoFood(pNextNode, ps);
                                    	}
                                    	KillByWall(ps);
                                    	KillBySelf(ps);
                                    }
                                    

                                    八、控制台设置

                                    如果同学Win11系统的控制台窗口是这样显示,可以调整⼀下

                                    【项目实践】贪吃蛇

                                    调整方式:

                                    【项目实践】贪吃蛇

                                    保存后,重新打开cmd即可

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]