C语言(万字讲解,帮你再现经典游戏) | 贪吃蛇 —— 附完整代码可以直接copy运行游玩

avatar
作者
筋斗云
阅读量:4

贪吃蛇

引言

在计算机科学的世界里,经典游戏是无法被忽视的一部分。它们不仅是我们对计算机编程技术进行探索和学习的重要工具,也是我们与计算机交互的一种有趣方式。其中,贪吃蛇游戏(Snake Game)无疑是这些经典游戏中的一颗明珠,它简单而充满挑战,一直以来都备受欢迎。

贪吃蛇游戏的核心玩法简单而经典:控制一条蛇在有限的空间内移动,吃掉食物,不断成长,但要避免撞到墙壁或者自己的身体。尽管规则简单,但这个游戏却蕴含了丰富的编程技术和算法挑战。从基本的用户输入处理,到数据结构的应用,再到图形界面的展示,贪吃蛇游戏是一个极好的项目,让我们有机会探索和实践各种计算机科学的基础概念。

在本文中,我们将深入探讨如何使用C语言来实现贪吃蛇游戏。C语言作为一种高效而强大的编程语言,是实现游戏的理想选择。通过这个项目,我们不仅可以加深对C语言的理解,还能够锻炼自己的逻辑思维和问题解决能力。

在接下来的内容中,我们将逐步介绍贪吃蛇游戏的实现过程,从所需要的基本知识到游戏逻辑再到用户界面的设计,带领读者一步步走进这个有趣而充满挑战的编程世界。让我们一起开始这段奇妙的编程之旅吧!

目录

1. 游戏效果及基本功能

使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
游戏画面展示:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
需要实现的基本功能:

  1. 贪吃蛇地图控制
  2. 蛇吃食物的功能(上,下,左,右方向键控制蛇的动作)
  3. 蛇撞墙死亡
  4. 蛇撞自身死亡
  5. 计算得分
  6. 蛇身加速、减速
  7. 暂停游戏

2. 技术要点

要想实现贪吃蛇游戏首先我们需要掌握:
C语言随机数的生成、函数、枚举、 结构体、动态内存管理、预处理指令链表、Win32API 等。
这些知识点在我的前几期博客都有介绍。

2.0 随机数的生成

2.0.1 rand

C语言提供了一个函数叫 rand,这函数是可以生成随机数,函数原型如下所示:

int rand (void); 

rand函数会返回一个伪随机数,这个随机数的范围是在0~RAND_MAX之间,这个RAND_MAX的大小是
依赖编译器上实现的,但是大部分编译器上是32767
rand函数的使用需要包含一个头文件是:stdlib.h

我们现在通过一个示例来了解一下什么是伪随机数。
示例

#include <stdio.h> #include <stdlib.h> int main() {  printf("%d\n", rand());  printf("%d\n", rand());  printf("%d\n", rand());  printf("%d\n", rand());  printf("%d\n", rand());  return 0; } 

在这里插入图片描述
运行两次之后,看到结果我们会发现,两次产生的随机数序列竟然是一样的,所以说伪随机数不是真正的随机数,是通过某种算法生成的随机数。真正的随机数的是无法预测下一个值是多少的。而rand函数是对一个叫“种子”的基准值进行运算生成的随机数。
之所以前面每次运行程序产生的随机数序列是一样的,那是因为rand函数⽣成随机数的默认种子是1。
如果要生成不同的随机数,就要让种子是变化的。


2.0.2 srand

C语言中又提供了一个函数叫 srand,用来初始化随机数的生成器的,srand的原型如下:

void srand (unsigned int seed); 

srand函数是不需要频繁调用的,一次运行的程序中调用一次就够了。
程序中在调用rand 函数之前先调用 srand 函数,通过 srand 函数的参数seed来设置rand函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。
因此,我们接下来介绍一个函数time来帮助我们实现“种子随机”。

2.0.3 time

在程序中我们一般是使用程序运行的时间作为种子,因为时间时刻在发生变化的。
在C语言中有一个函数叫 time ,就可以获得这个时间,time函数原型如下:

time_t time (time_t* timer); 

time 函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t类型的,time_t 类型本质上其实就是32位或者64位的整型类型。
time函数的参数 timer 如果是非NULL指针的话,函数也会将这个返回的差值放在timer指向的内存中带回去。
如果 timerNULL,就只返回这个时间的差值。time函数返回的这个时间差也被叫做:时间戳。
time函数的时候需要包含头文件:time.h

2.0.4 设置随机数的范围

如果要生成a~b的随机数,方法如下:

a + rand()%(b-a+1) 

接下来介绍实现贪吃蛇会用到的一些Win32 API知识

2.1 Win32 API

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

2.2 控制台程序

平常我们运行起来的黑框程序其实就是控制台程序。

在这里插入图片描述

如果大家显示的是终端,可以通过这样设置来打开控制台程序。

在这里插入图片描述
只有这样才能正常运行贪吃蛇游戏!


我们可以使用cmd命令来设置控制台窗口的长宽
示例 :设置控制台窗口的大小,30行,100列。

mode con cols=100 lines=30 

参考:mode指令

也可以通过命令设置控制台窗口的名字
示例 :命名为贪吃蛇

title 贪吃蛇 

在这里插入图片描述
参考: title命令

这样能在控制台窗口执行的命令,也可以调用C语言函数system来执行。
示例

#include<stdio.h> int main() { 	//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列 	system("mode con cols=100 lines=30"); 	//设置cmd窗⼝名称 	system("title 贪吃蛇"); 	return 0; } 

2.3 控制台屏幕上的坐标 COORD

COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
在这里插入图片描述
COORD类型的声明

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

给坐标复制

COORD pos = { 10, 15 }; 

2.4 GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle); 

示例

HANDLE hOutput = NULL; //获取标准输出的句柄(用来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); 

STD_OUTPUT_HANDLE 是一个预定义的常量,表示标准输出流。通过调用 GetStdHandle(STD_OUTPUT_HANDLE),就可以获取到标准输出流的句柄,并将其赋值给变量 hOutput,以便后续在程序中使用。


2.5 GetConsoleCursorInfo

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

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

示例

HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);  CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 

2.6 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; //隐藏控制台光标 

2.7 SetConsoleCursorInfo

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

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

示例

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);// 获得标准输出设备的句柄 //隐藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; //定义了一个光标信息的结构体变量,名为CursorInfo GetConsoleCursorInfo(hOutput, &CursorInfo);//获取和hOutpot句柄相关的控制台上的光标信息,并存放在CursorInfo CursorInfo.bVisible = false; //隐藏控制台光标 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置和hOutpot句柄相关的控制台上的光标状态 

2.8 SetConsoleCursorPosition

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

BOOL WINAPI SetConsoleCursorPosition(  HANDLE hConsoleOutput,  COORD pos ); 

示例

 COORD pos = {10 , 5};  HANDLE hOutput = NULL;  //获取标准输出的句柄(用来标识不同设备的数值)  hOutput = GetStdHandle(STD_OUTPUT_HANDLE);  //设置标准输出上光标的位置为pos  SetConsoleCursorPosition(hOutput, pos); 

SetPos:封装一个设置光标位置的函数。

//设置光标的坐标 void SetPos(short x, short y) {  	COORD pos = { x, y };  	HANDLE hOutput = NULL;  	//获取标准输出的句柄(用来标识不同设备的数值)  	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);  	//设置标准输出上光标的位置为pos  	SetConsoleCursorPosition(hOutput, pos); } 

2.9 GetAsyncKeyState

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

SHORT GetAsyncKeyState(  int vKey ); 

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1,来确定。

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

参考:虚拟键码 (Winuser.h) - Win32 apps

我们可以参考键码表输入参数来使用GetAsyncKeyState函数判断键是否被按下

示例:检测数字键

#include <stdio.h> #include <windows.h> 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; } 

3. 贪吃蛇游戏设计和分析

3.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>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

3.1.1 <locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准中,依赖地区的部分有以下几项:

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

3.1.2 类项

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

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

参考: 每个类项的详细说明


3.1.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, " ");//切换到本地环境 

3.1.4 宽字符的打印

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

#include <stdio.h> #include<locale.h> int main() {  	setlocale(LC_ALL, "");  	wchar_t ch1 = L'●';  	wchar_t ch2 = L'⽐';  	wchar_t ch3 = L'特';  	wchar_t ch4 = L'★';    	printf("%c%c\n", 'a', 'b');    	wprintf(L"%lc\n", ch1);  	wprintf(L"%lc\n", ch2);  	wprintf(L"%lc\n", ch3);  	wprintf(L"%lc\n", ch4); 	return 0; } 

输出结果为:
在这里插入图片描述
从输出的结果来看,我们发现⼀个普通字符占一个字符的位置。但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

普通字符和宽字符打印宽度的展示如下:
在这里插入图片描述


3.1.5 地图坐标

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

3.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标 (x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

在这里插入图片描述


3.3 数据结构设计

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

//snake.h文件  //蛇身的节点类型 typedef struct SnakeNode { 	//坐标 	int x; 	int y; 	//指向下一个节点的指针 	struct SnakeNode* next; }SnakeNode, * pSnakeNode;   //对于这个结构体变量定义一个指针变量pSnakeNode  //typedef struct SnakeNode* pSnakeNode;   //这样写也可以  

要管理整条贪吃蛇,我们再封装一下Snake的结构来维护整条贪吃蛇。

//snake.h文件  //贪吃蛇 typedef struct Snake { 	pSnakeNode _pSnake;//指向蛇头的指针 	pSnakeNode _pFood;//指向食物节点的指针 	enum DIRECTION _dir;//蛇的方向,默认向右 	enum GAME_STATUS _status;//游戏的状态 	int _food_weight;//一个食物的分数 	int _score;      //总成绩 	int _sleep_time; //每一步的休眠时间,时间越短,速度越快,时间越长,速度越慢 }Snake, * pSnake; 

蛇的方向可以使用枚举来一一列举。

//snake.h文件  //蛇的方向 enum DIRECTION { 	UP = 1, 	DOWN, 	LEFT, 	RIGHT }; 

游戏状态也是通过枚举来一一列举。

//snake.h文件  //蛇的状态 //正常、撞墙、撞到自己、正常退出 enum GAME_STATUS { 	OK, //正常 	KILL_BY_WALL, //撞墙 	KILL_BY_SELF, //撞到自己 	END_NORMAL //正常退出 }; 

3.4 游戏流程设计

在这里插入图片描述


4. 核心逻辑实现分析

4.1 游戏主逻辑

程序开始就设置程序支持本模式,然后进入游戏的主逻辑。
主逻辑一共分为3个过程:

  • 游戏开始(GameStart)完成游戏的初始化
  • 游戏运行(GameRun)完成游戏运行逻辑的实现
  • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
//test.c  文件 #include<locale.h>  //本地化头文件  void test() { 	int ch = 0; 	do 	{ 		system("cls");      		//清空窗口 		//创建贪吃蛇 		Snake snake = { 0 }; 		//初始化游戏 		//1. 打印环境界面 		//2. 功能介绍 		//3. 绘制地图 		//4. 创建蛇 		//5. 创建食物 		//6. 设置游戏的相关信息 		GameStart(&snake); 		//运行游戏 		GameRun(&snake); 		 		//检测是否有按键被按下—— 因为控制台的本身机制,如果在游玩时按上键,控制台会把历史命令显示出来。因此使用系统调用函数来拿走按键 		while (_kbhit()) 		{ 			// 使用 _getch() 获取按下的键,不阻塞程序 			_getch(); 			// 处理按键事件,可以根据需要进行相应的操作 		} 		 		//结束游戏 - 善后工作 		GameEnd(&snake);  		//打印是否进入下一局的引导语 		SetPos(20, 15);					//定位 		printf("再来一局吗?(Y/N):"); 		ch = getchar();    //获取玩家输入 		while (getchar() != '\n');  //防止输入过多无用字符导致程序崩溃  	} while (ch=='Y' || ch=='y'); 	SetPos(0, 27);						//将进程结束提示至于窗口最下方 }  int main() { 	//设置适配本地环境 	setlocale(LC_ALL, "");  	srand((unsigned int)time(NULL));    //只需初始化一次的随机种子用于随机数 	test();  	return 0; } 

4.2 游戏开始(GameStart)

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

  • 控制台窗口大小的设置
  • 控制台窗口名字的设置
  • 鼠标光标的隐藏
  • 打印欢迎界面
  • 创建地图
  • 初始化蛇身
  • 创建第一个食物
//snake.c 文件  void GameStart(pSnake ps) { 	//0. 先设置窗口的大小,再光标隐藏 	system("mode con cols=100 lines=30"); 	 	system("title 贪吃蛇");  //设置窗口标题 	HANDLE houtput= GetStdHandle(STD_OUTPUT_HANDLE);   //获得句柄 	 	//影藏光标操作 	CONSOLE_CURSOR_INFO CursorInfo; 	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息 	CursorInfo.bVisible = false; //隐藏控制台光标 	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态  	//1. 打印环境界面和功能介绍 	WelcomeToGame(); 	//2. 绘制地图 	CreateMap(); 	//3. 创建蛇 	InitSnake(ps); 	//4. 创建食物 	CreateFood(ps); } 

4.2.1 打印欢迎界面

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

首先为我们的界面创建做些准备——创建一个位置函数
使用这个函数,我们就可以在控制台窗口的任意位置放入我们想要插入的信息。

//snake.c文件  void SetPos(short x, short y) { 	//获得标准输出设备的句柄 	HANDLE houtput = NULL; 	houtput = GetStdHandle(STD_OUTPUT_HANDLE);  	//定位光标的位置 	COORD pos = { x, y }; 	SetConsoleCursorPosition(houtput, pos); } 

然后在指定位置插入我们的提示信息

//snake.c文件  void WelcomeToGame() { 	SetPos(40, 14); 	wprintf(L"欢迎来到贪吃蛇小游戏\n"); 	 	SetPos(42, 20); 	system("pause");    //按任意键继续......    按下后会切换页面 	system("cls");      //清空上一页面的窗口信息 	 	SetPos(25, 14); 	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n"); 	 	SetPos(25, 15); 	wprintf(L"加速能够得到更高的分数\n");  	SetPos(42, 20); 	system("pause");    //按任意键继续......    按下后会切换页面 	system("cls");		//清空上一页面的窗口信息 }  

在这里插入图片描述


4.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L。打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
墙体打印宽字符:

//snake.h文件 //在这里我们直接将常用的几个宽字符常量进行定义 #define WALL L'□' 

坐标的计算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
创建地图函数CreateMap

//snake.c 文件  void CreateMap() { 	//上 	int i = 0; 	SetPos(0, 0);	//上(0,0)-(56, 0) 	for (i = 0; i < 29; i++) 	{ 		wprintf(L"%lc", WALL); 	}  	//下 	SetPos(0, 26);	//下(0,26)-(56, 26) 	for (i = 0; i < 29; i++) 	{ 		wprintf(L"%lc", WALL); 	} 	//左 	for (i = 1; i <= 25; i++) 	{ 		SetPos(0, i);	//x是0,y从1开始增⻓ 		wprintf(L"%lc", WALL); 	}  	//右 	for (i = 1; i <= 25; i++) 	{ 		SetPos(56, i);	//x是56,y从1开始增⻓ 		wprintf(L"%lc", WALL); 	} } 

在这里插入图片描述


4.2.3 初始化蛇身

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每⼀节打印在屏幕上。

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

蛇身打印的宽字符:

//snake.h文件 //在这里我们直接将常用的几个宽字符常量进行定义 #define BODY L'●' 

初始化蛇身函数:InitSnake

//snake.c 文件  void InitSnake(pSnake ps)  //初始化蛇身需要将蛇的结构指针传给函数 { 	int i = 0; 	pSnakeNode cur = NULL; 	//创建蛇⾝节点,并初始化坐标 	//头插法 	for (i = 0; i < 5; i++) 	{ 		cur = (pSnakeNode)malloc(sizeof(SnakeNode));  //为蛇身节点申请空间 		if (cur == NULL)		//防止空间申请失败 		{ 			perror("InitSnake()::malloc()"); 			return; 		} 		 		//给节点进行坐标定位 		cur->next = NULL;   		cur->x = POS_X + 2 * i; 		cur->y = POS_Y;  		//头插法插入链表 		if (ps->_pSnake == NULL) //空链表 		{ 			ps->_pSnake = cur; 		} 		else //非空 		{ 			cur->next = ps->_pSnake; 			ps->_pSnake = cur; 		} 	}  	//打印贪吃蛇 	cur = ps->_pSnake;  //将蛇头地址给cur 	while (cur) 	{ 		SetPos(cur->x, cur->y);	//给定位置 		wprintf(L"%lc", BODY); 		cur = cur->next; 	}  	//设置贪吃蛇的属性 	ps->_dir = RIGHT;//默认向右 	ps->_score = 0; 	ps->_food_weight = 10; 	ps->_sleep_time = 200;//单位是毫秒,休眠时间是蛇每走一步中间间隔的时间 	ps->_status = OK;  } 

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

食物打印的宽字符:

//snake.h文件 //在这里我们直接将常用的几个宽字符常量进行定义 #define FOOD L'★' 

创建食物的函数:CreateFood

//snake.c 文件  void CreateFood(pSnake ps) { 	int x = 0; 	int y = 0;  	//生成x坐标需是2的倍数 	//x:2~54 	//y: 1~25 	 again: 	do 	{ 		x = rand() % 53 + 2; 	//根据合适的坐标范围随机生成食物坐标 		y = rand() % 25 + 1; 	} while (x % 2 != 0);		//判断x坐标是否是2的倍数,不是就进入循环重新生成坐标  	//x和y的坐标不能和蛇的身体坐标冲突 	pSnakeNode cur = ps->_pSnake;	//将指向蛇头的指针赋给cur 	while (cur) 	{ 		if (x == cur->x && y == cur->y)	//判断是否和身体坐标发生冲突 		{ 			goto again;					//如果冲突,就返回到again标点重新生成坐标 		} 		cur = cur->next; 	}							//到这里随机生成的食物坐标就通过检查了  	//创建食物的节点 	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));  //为食物节点申请空间 	if (pFood == NULL)		//防止空间申请失败 	{ 		perror("CreateFood()::malloc()"); 		return; 	} 	 	//将生成的坐标赋给食物节点 	pFood->x = x;		 	pFood->y = y; 	pFood->next = NULL;  //让食物节点的next指向空,达到食物节点独立。  	SetPos(x, y);//定位位置 	wprintf(L"%lc", FOOD);  	ps->_pFood = pFood;		//将食物信息赋给食物指针 } 

4.3 游戏运行(GameRun)

游戏运行期间,右侧打印帮助信息提示玩家,坐标(64, 15)。
PrintfHelpInfo

//snake.c 文件 void PrintHelpInfo() { 	SetPos(64, 5); 	wprintf(L"%ls", L"按空格开始游戏!"); 	SetPos(64, 14); 	wprintf(L"%ls", L"不能穿墙,不能咬到自己"); 	SetPos(64, 15); 	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动"); 	SetPos(64, 16); 	wprintf(L"%ls", L"按F3加速,F4减速"); 	SetPos(64, 17); 	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");  	SetPos(64, 18); 	wprintf(L"%ls", L"版权归Jason所有"); } 

在这里插入图片描述

根据游戏状态检查游戏是否继续,如果状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
需要的虚拟按键的罗列:

  • 上:VK_UP
  • 下:VK_DOWN
  • 左:VK_LEFT
  • 右:VK_RIGHT
  • 空格:VK_SPACE
  • ESC:VK_ESCAPE
  • F3:VK_F3
  • F4:VK_F4

确定了蛇的方向和速度,蛇就可以移动了。

//snake.c文件  void GameRun(pSnake ps) { 	//在窗口右侧打印帮助信息 	PrintHelpInfo(); 	do 	{ 		//打印总分数和食物的分值 		SetPos(64, 10); 		printf("总分数:%d\n", ps->_score); 		SetPos(64, 11); 		printf("当前食物的分数:%2d\n", ps->_food_weight); 		 		//检测按键是否被按下,以及防止按键冲突 		if (KEY_PRESS(VK_UP) && ps->_dir != DOWN) 		{ 			ps->_dir = UP; 		} 		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP) 		{ 			ps->_dir = DOWN; 		} 		else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT) 		{ 			ps->_dir = LEFT; 		} 		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT) 		{ 			ps->_dir = RIGHT; 		} 		else if (KEY_PRESS(VK_SPACE)) 		{ 			Pause();   //暂停函数 		} 		else if (KEY_PRESS(VK_ESCAPE)) 		{ 			//正常退出游戏 			ps->_status = END_NORMAL;    //将游戏状态设为正常退出 		} 		else if (KEY_PRESS(VK_F3)) 		{ 			//加速 			if (ps->_sleep_time > 80)    //当休眠时间被缩短到80ms时,速度已经足够快了 			{ 				ps->_sleep_time -= 30;   //减少休眠时间 				ps->_food_weight += 2;   //增加食物的分数,最高分是20分 			} 		}  		else if (KEY_PRESS(VK_F4)) 		{ 			//减速 			if (ps->_food_weight > 2)  //食物分数已经降到了最低标准 			{ 				ps->_sleep_time += 30;  //增加休眠时间 				ps->_food_weight -= 2;  //减少食物的分数 			} 		} 		 		SnakeMove(ps);//蛇走一步的过程的函数  		Sleep(ps->_sleep_time);  //蛇每次到达一定状态,就要进行休眠  	} while (ps->_status==OK);   //只有游戏状态是ok,才会再次进入循环重复进行检测,使游戏进行下去。 } 

封装宏来检测按键状态——低位为1为按下,0为未按下

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

暂停函数Pause

//snake.c文件  void Pause() { 	while (1) 	{ 		Sleep(200);					//暂停200毫秒 		if (KEY_PRESS(VK_SPACE))	//如果没有按下空格,则会反复暂停200毫秒 		{ 			break; 		} 	} } 
4.3.1 蛇身移动SnakeMove

先创建下一个节点,根据移动方向和蛇头的坐标,确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。

//snake.c文件  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); } 
4.3.2 NextIsFood
//snake.c 文件  //pSnakeNode pn 是下⼀个节点的地址 //pSnake ps 贪吃蛇的指针 int NextIsFood(pSnakeNode pn, pSnake ps) { 	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y); } 
4.3.3 EatFood
//snake.c 文件  //pSnakeNode pn 是下⼀个节点的地址 //pSnake ps 贪吃蛇的指针 void EatFood(pSnakeNode pn, pSnake ps) { 	//头插法 	ps->_pFood->next = ps->_pSnake; 	ps->_pSnake = ps->_pFood;  	//释放下一个位置的节点 	free(pn); 	pn = NULL;  	pSnakeNode cur = ps->_pSnake; 	//打印蛇 	while (cur) 	{ 		SetPos(cur->x, cur->y); 		wprintf(L"%lc", BODY); 		cur = cur->next; 	} 	ps->_score += ps->_food_weight;  //增加食物获得总分  	//重新创建食物 	CreateFood(ps); } 
4.3.4 NoFood

将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格(遮盖之前的蛇尾,要不然蛇走过的会形成轨迹),释放掉蛇身的最后一个节点。
易错点:这里最容易错误的是,释放最后一个结点后,还得将指向在最后一个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。

//snake.c 文件  void NoFood(pSnakeNode pn, pSnake ps) { 	//头插法 	pn->next = ps->_pSnake; 	ps->_pSnake = pn;  	pSnakeNode cur = ps->_pSnake; 	while (cur->next->next != NULL) 	{ 		SetPos(cur->x, cur->y); 		wprintf(L"%lc", BODY); 		cur = cur->next; 	}  	//把最后一个结点打印成空格 	SetPos(cur->next->x, cur->next->y); 	printf("  ");  	//释放最后一个结点 	free(cur->next);  	//把倒数第二个节点的地址置为NULL 	cur->next = NULL; } 
4.3.5 KillByWall

判断蛇头的坐标是否和墙的坐标发生冲突。

//snake.c 文件  void 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;    //将游戏状态改为:撞墙死 	} }  

在这里插入图片描述

4.3.6KillBySelf
//snake.c 文件  void KillBySelf(pSnake ps)  //此函数和上边的撞墙函数同理 { 	pSnakeNode cur = ps->_pSnake->next; 	while (cur) 	{ 		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y) 		{ 			ps->_status = KILL_BY_SELF; 			break; 		} 		cur = cur->next;    //通过函数遍历蛇身所有节点来检测蛇头是否撞到蛇身 	} } 

在这里插入图片描述


4.4 游戏结束(GameEnd)

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

//snake.c 文件  void GameEnd(pSnake ps) { 	SetPos(24, 12); 	switch (ps->_status)    //根据最后游戏结束的不同状态打印信息 	{ 	case END_NORMAL: 		wprintf(L"您主动结束游戏\n"); 		break; 	case KILL_BY_WALL: 		wprintf(L"您撞到墙上,游戏结束\n"); 		break; 	case KILL_BY_SELF: 		wprintf(L"您撞到了自己,游戏结束\n"); 		break; 	}  	//释放蛇身的链表 	pSnakeNode cur = ps->_pSnake; 	while (cur) 	{ 		pSnakeNode del = cur; 		cur = cur->next; 		free(del); 	} } 

5. 参考代码

snake.h 文件

#pragma once  #include <windows.h> #include <stdbool.h> #include <stdlib.h> #include <stdio.h> #include <time.h>    #define POS_X 24 #define POS_Y 5  #define WALL L'□' #define BODY L'●' #define FOOD L'★'  //类型的声明  //蛇的方向 enum DIRECTION { 	UP = 1, 	DOWN, 	LEFT, 	RIGHT };  //蛇的状态 //正常、撞墙、撞到自己、正常退出 enum GAME_STATUS { 	OK, //正常 	KILL_BY_WALL, //撞墙 	KILL_BY_SELF, //撞到自己 	END_NORMAL //正常退出 };  //蛇身的节点类型 typedef struct SnakeNode { 	//坐标 	int x; 	int y; 	//指向下一个节点的指针 	struct SnakeNode* next; }SnakeNode, * pSnakeNode;   //对于这个结构体变量定义一个指针变量pSnakeNode  //typedef struct SnakeNode* pSnakeNode;   //这样写也可以    //贪吃蛇 typedef struct Snake { 	pSnakeNode _pSnake;//指向蛇头的指针 	pSnakeNode _pFood;//指向食物节点的指针 	enum DIRECTION _dir;//蛇的方向,默认向右 	enum GAME_STATUS _status;//游戏的状态 	int _food_weight;//一个食物的分数 	int _score;      //总成绩 	int _sleep_time; //每一步的休眠时间,时间越短,速度越快,时间越长,速度越慢 }Snake, * pSnake;  //函数的声明  //定位光标位置 void SetPos(short x, short y);  //游戏的初始化 void GameStart(pSnake ps);  //欢迎界面的打印 void WelcomeToGame();  //创建地图 void CreateMap();  void CreateMap();  //初始化蛇身 void InitSnake(pSnake ps);  //创建食物 void CreateFood(pSnake ps);  //游戏运行的逻辑 void GameRun(pSnake ps);  //蛇的移动-走一步 void SnakeMove(pSnake ps);  //判断下一个坐标是否是食物 int NextIsFood(pSnakeNode pn, pSnake ps);  //下一个位置是食物,就吃掉食物 void EatFood(pSnakeNode pn, pSnake ps);  //下一个位置不是食物 void NoFood(pSnakeNode pn, pSnake ps);  //检测蛇是否撞墙 void KillByWall(pSnake ps);  //检测蛇是否撞到自己 void KillBySelf(pSnake ps);  //游戏善后的工作 void GameEnd(pSnake ps); 

snake.c 文件

#define _CRT_SECURE_NO_WARNINGS 1  #include "snake.h"  //GAMESTART  //控制台窗口大小的设置 //控制台窗口名字的设置 //鼠标光标的隐藏 //打印欢迎界面 //创建地图 //初始化蛇身 //创建第一个食物 void GameStart(pSnake ps) { 	//0. 先设置窗口的大小,再光标隐藏 	system("mode con cols=100 lines=30");  	system("title 贪吃蛇");  //设置窗口标题 	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);   //获得句柄  	//影藏光标操作 	CONSOLE_CURSOR_INFO CursorInfo; 	GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息 	CursorInfo.bVisible = false; //隐藏控制台光标 	SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态  	//1. 打印环境界面和功能介绍 	WelcomeToGame(); 	//2. 绘制地图 	CreateMap(); 	//3. 创建蛇 	InitSnake(ps); 	//4. 创建食物 	CreateFood(ps); }  //位置函数 void SetPos(short x, short y) { 	//获得标准输出设备的句柄 	HANDLE houtput = NULL; 	houtput = GetStdHandle(STD_OUTPUT_HANDLE);  	//定位光标的位置 	COORD pos = { x, y }; 	SetConsoleCursorPosition(houtput, pos); }  //1. 打印环境界面和功能介绍 void WelcomeToGame() { 	SetPos(40, 14); 	wprintf(L"欢迎来到贪吃蛇小游戏\n");  	SetPos(42, 20); 	system("pause");    //按任意键继续......    按下后会切换页面 	system("cls");      //清空上一页面的窗口信息  	SetPos(25, 14); 	wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");  	SetPos(25, 15); 	wprintf(L"加速能够得到更高的分数\n");  	SetPos(42, 20); 	system("pause");    //按任意键继续......    按下后会切换页面 	system("cls");		//清空上一页面的窗口信息 }  //2. 绘制地图 void CreateMap() { 	//上 	int i = 0; 	SetPos(0, 0);	//上(0,0)-(56, 0) 	for (i = 0; i < 58; i+= 2) 	{ 		wprintf(L"%lc", WALL); 	}  	//下 	SetPos(0, 26);	//下(0,26)-(56, 26) 	for (i = 0; i < 58; i+=2) 	{ 		wprintf(L"%lc", WALL); 	} 	//左 	for (i = 1; i <= 25; i++) 	{ 		SetPos(0, i);	//x是0,y从1开始增⻓ 		wprintf(L"%lc", WALL); 	}  	//右 	for (i = 1; i <= 25; i++) 	{ 		SetPos(56, i);	//x是56,y从1开始增⻓ 		wprintf(L"%lc", WALL); 	} }  //3. 创建蛇 void InitSnake(pSnake ps)  //初始化蛇身需要将蛇的结构指针传给函数 { 	int i = 0; 	pSnakeNode cur = NULL; 	//创建蛇⾝节点,并初始化坐标 	//头插法 	for (i = 0; i < 5; i++) 	{ 		cur = (pSnakeNode)malloc(sizeof(SnakeNode));  //为蛇身节点申请空间 		if (cur == NULL)		//防止空间申请失败 		{ 			perror("InitSnake()::malloc()"); 			return; 		}  		//给节点进行坐标定位 		cur->next = NULL; 		cur->x = POS_X + 2 * i; 		cur->y = POS_Y;  		//头插法插入链表 		if (ps->_pSnake == NULL) //空链表 		{ 			ps->_pSnake = cur; 		} 		else //非空 		{ 			cur->next = ps->_pSnake; 			ps->_pSnake = cur; 		} 	}  	//打印贪吃蛇 	cur = ps->_pSnake;  //将蛇头地址给cur 	while (cur) 	{ 		SetPos(cur->x, cur->y);	//给定位置 		wprintf(L"%lc", BODY); 		cur = cur->next; 	}  	//设置贪吃蛇的属性 	ps->_dir = RIGHT;//默认向右 	ps->_score = 0; 	ps->_food_weight = 10; 	ps->_sleep_time = 200;//单位是毫秒,休眠时间是蛇每走一步中间间隔的时间 	ps->_status = OK;  }  //4. 创建食物 void CreateFood(pSnake ps) { 	int x = 0; 	int y = 0;  	//生成x坐标需是2的倍数 	//x:2~54 	//y: 1~25  again: 	do 	{ 		x = rand() % 53 + 2; 	//根据合适的坐标范围随机生成食物坐标 		y = rand() % 25 + 1; 	} while (x % 2 != 0);		//判断x坐标是否是2的倍数,不是就进入循环重新生成坐标  	//x和y的坐标不能和蛇的身体坐标冲突 	pSnakeNode cur = ps->_pSnake;	//将指向蛇头的指针赋给cur 	while (cur) 	{ 		if (x == cur->x && y == cur->y)	//判断是否和身体坐标发生冲突 		{ 			goto again;					//如果冲突,就返回到again标点重新生成坐标 		} 		cur = cur->next; 	}							//到这里随机生成的食物坐标就通过检查了  	//创建食物的节点 	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));  //为食物节点申请空间 	if (pFood == NULL)		//防止空间申请失败 	{ 		perror("CreateFood()::malloc()"); 		return; 	}  	//将生成的坐标赋给食物节点 	pFood->x = x; 	pFood->y = y; 	pFood->next = NULL;  //让食物节点的next指向空,达到食物节点独立。  	SetPos(x, y);//定位位置 	wprintf(L"%lc", FOOD);  	ps->_pFood = pFood;		//将食物信息赋给食物指针 }   //GAMERUN  //打印帮助信息 void PrintHelpInfo() { 	SetPos(64, 5); 	wprintf(L"%ls", L"按空格开始游戏!"); 	SetPos(64, 14); 	wprintf(L"%ls", L"不能穿墙,不能咬到自己"); 	SetPos(64, 15); 	wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动"); 	SetPos(64, 16); 	wprintf(L"%ls", L"按F3加速,F4减速"); 	SetPos(64, 17); 	wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");  	SetPos(64, 18); 	wprintf(L"%ls", L"版权归Jason所有"); }  //封装宏来检测按键状态——低位为1为按下,0为未按下 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)  //暂停函数 void Pause() { 	while (1) 	{ 		Sleep(200);					//暂停200毫秒 		if (KEY_PRESS(VK_SPACE))	//如果没有按下空格,则会反复暂停200毫秒 		{ 			break; 		} 	} }  //游戏运行 void GameRun(pSnake ps) { 	//在窗口右侧打印帮助信息 	PrintHelpInfo(); 	do 	{ 		//打印总分数和食物的分值 		SetPos(64, 10); 		printf("总分数:%d\n", ps->_score); 		SetPos(64, 11); 		printf("当前食物的分数:%2d\n", ps->_food_weight);  		//检测按键是否被按下,以及防止按键冲突 		if (KEY_PRESS(VK_UP) && ps->_dir != DOWN) 		{ 			ps->_dir = UP; 		} 		else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP) 		{ 			ps->_dir = DOWN; 		} 		else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT) 		{ 			ps->_dir = LEFT; 		} 		else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT) 		{ 			ps->_dir = RIGHT; 		} 		else if (KEY_PRESS(VK_SPACE)) 		{ 			Pause();   //暂停函数 		} 		else if (KEY_PRESS(VK_ESCAPE)) 		{ 			//正常退出游戏 			ps->_status = END_NORMAL;    //将游戏状态设为正常退出 		} 		else if (KEY_PRESS(VK_F3)) 		{ 			//加速 			if (ps->_sleep_time > 80)    //当休眠时间被缩短到80ms时,速度已经足够快了 			{ 				ps->_sleep_time -= 30;   //减少休眠时间 				ps->_food_weight += 2;   //增加食物的分数,最高分是20分 			} 		} 		else if (KEY_PRESS(VK_F4)) 		{ 			//减速 			if (ps->_food_weight > 2)  //食物分数已经降到了最低标准 			{ 				ps->_sleep_time += 30;  //增加休眠时间 				ps->_food_weight -= 2;  //减少食物的分数 			} 		}  		SnakeMove(ps);//蛇走一步的过程的函数  		Sleep(ps->_sleep_time);  //蛇每次到达一定状态,就要进行休眠  	} while (ps->_status == OK);   //只有游戏状态是ok,才会再次进入循环重复进行检测,使游戏进行下去。 }      //蛇身运动检测 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); }   //判断下一个坐标是否是食物 //pSnakeNode pn 是下⼀个节点的地址 //pSnake ps 贪吃蛇的指针 int NextIsFood(pSnakeNode pn, pSnake ps) { 	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y); }  //下一个位置是食物,就吃掉食物 //pSnakeNode pn 是下⼀个节点的地址 //pSnake ps 贪吃蛇的指针 void EatFood(pSnakeNode pn, pSnake ps) { 	//头插法 	ps->_pFood->next = ps->_pSnake; 	ps->_pSnake = ps->_pFood;  	//释放下一个位置的节点 	free(pn); 	pn = NULL;  	pSnakeNode cur = ps->_pSnake; 	//打印蛇 	while (cur) 	{ 		SetPos(cur->x, cur->y); 		wprintf(L"%lc", BODY); 		cur = cur->next; 	} 	ps->_score += ps->_food_weight;  //增加食物获得总分  	//重新创建食物 	CreateFood(ps); }   //下一个位置不是食物 void NoFood(pSnakeNode pn, pSnake ps) { 	//头插法 	pn->next = ps->_pSnake; 	ps->_pSnake = pn;  	pSnakeNode cur = ps->_pSnake; 	while (cur->next->next != NULL) 	{ 		SetPos(cur->x, cur->y); 		wprintf(L"%lc", BODY); 		cur = cur->next; 	}  	//把最后一个结点打印成空格 	SetPos(cur->next->x, cur->next->y); 	printf("  ");  	//释放最后一个结点 	free(cur->next);  	//把倒数第二个节点的地址置为NULL 	cur->next = NULL; }  //检测蛇是否撞墙 void 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;    //将游戏状态改为:撞墙死 	} }   //检测蛇是否撞到自己 void KillBySelf(pSnake ps)  //此函数和上边的撞墙函数同理 { 	pSnakeNode cur = ps->_pSnake->next; 	while (cur) 	{ 		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y) 		{ 			ps->_status = KILL_BY_SELF; 			break; 		} 		cur = cur->next;    //通过函数遍历蛇身所有节点来检测蛇头是否撞到蛇身 	} }   //游戏善后的工作 void GameEnd(pSnake ps) { 	SetPos(24, 12); 	switch (ps->_status)    //根据最后游戏结束的不同状态打印信息 	{ 	case END_NORMAL: 		wprintf(L"您主动结束游戏\n"); 		break; 	case KILL_BY_WALL: 		wprintf(L"您撞到墙上,游戏结束\n"); 		break; 	case KILL_BY_SELF: 		wprintf(L"您撞到了自己,游戏结束\n"); 		break; 	}  	//释放蛇身的链表 	pSnakeNode cur = ps->_pSnake; 	while (cur) 	{ 		pSnakeNode del = cur; 		cur = cur->next; 		free(del); 	} } 

test.c 文件

#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #include <locale.h> #include "snake.h"  #include<locale.h>  //本地化头文件  void test() { 	int ch = 0; 	do 	{ 		system("cls");      		//清空窗口 		//创建贪吃蛇 		Snake snake = { 0 }; 		//初始化游戏 		//1. 打印环境界面 		//2. 功能介绍 		//3. 绘制地图 		//4. 创建蛇 		//5. 创建食物 		//6. 设置游戏的相关信息 		GameStart(&snake); 		//运行游戏 		GameRun(&snake); 		//检测是否有按键被按下—— 因为控制台的本身机制,如果在游玩时按上键,控制台会把历史命令显示出来。因此使用系统调用函数来拿走按键 		while (_kbhit()) 		{ 			// 使用 _getch() 获取按下的键,不阻塞程序 			_getch(); 			// 处理按键事件,可以根据需要进行相应的操作 		} 		 		//结束游戏 - 善后工作 		GameEnd(&snake);  		//打印是否进入下一局的引导语 		SetPos(20, 15);					//定位 		printf("再来一局吗?(Y/N):"); 		ch = getchar(); 		while (getchar() != '\n');  	} while (ch == 'Y' || ch == 'y'); 	SetPos(0, 27);						//将进程结束提示至于窗口最下方 }  int main() { 	//设置适配本地环境 	setlocale(LC_ALL, "");  	srand((unsigned int)time(NULL));    //只需初始化一次的随机种子用于随机数 	test();  	return 0; } 

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!