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

贪吃蛇

引言

在计算机科学的世界里,经典游戏是无法被忽视的一部分。它们不仅是我们对计算机编程技术进行探索和学习的重要工具,也是我们与计算机交互的一种有趣方式。其中,贪吃蛇游戏(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;
}
  • 27
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,以下是一个简单的贪吃蛇游戏的完整C语言代码,可以在Windows操作系统上编译运行: ```c #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <windows.h> // 定义游戏区域的大小 const int WIDTH = 30; const int HEIGHT = 20; // 定义贪吃蛇的结构体 typedef struct Snake { int x; // 蛇头的x坐标 int y; // 蛇头的y坐标 int length; // 蛇的长度 int direction; // 蛇头移动的方向 int bodyX[WIDTH * HEIGHT]; // 蛇身的x坐标 int bodyY[WIDTH * HEIGHT]; // 蛇身的y坐标 } Snake; // 定义食物的结构体 typedef struct Food { int x; // 食物的x坐标 int y; // 食物的y坐标 } Food; // 定义全局变量 Snake snake; Food food; int score = 0; int speed = 200; // 蛇移动的速度,单位为毫秒 // 游戏初始化 void init() { // 初始化蛇的位置和长度 snake.x = WIDTH / 2; snake.y = HEIGHT / 2; snake.length = 3; snake.direction = 'w'; // 初始方向为向上 for (int i = 0; i < snake.length; i++) { snake.bodyX[i] = snake.x; snake.bodyY[i] = snake.y + i; } // 随机生成食物的位置 srand((unsigned)time(NULL)); food.x = rand() % WIDTH; food.y = rand() % HEIGHT; // 初始化得分和速度 score = 0; speed = 200; } // 显示游戏画面 void display() { system("cls"); // 清屏 // 显示游戏区域 for (int i = 0; i <= HEIGHT + 1; i++) { for (int j = 0; j <= WIDTH + 1; j++) { if (i == 0 || i == HEIGHT + 1 || j == 0 || j == WIDTH + 1) { printf("#"); } else if (i == snake.y && j == snake.x) { printf("O"); // 显示蛇头 } else if (i == food.y && j == food.x) { printf("*"); // 显示食物 } else { int flag = 0; for (int k = 0; k < snake.length; k++) { if (i == snake.bodyY[k] && j == snake.bodyX[k]) { printf("o"); // 显示蛇身 flag = 1; break; } } if (!flag) { printf(" "); } } } printf("\n"); } // 显示得分和速度 printf("Score: %d\n", score); printf("Speed: %d\n", speed); } // 处理用户输入 void input() { if (_kbhit()) { // 判断是否有键盘输入 char ch = _getch(); if (ch == 'w' || ch == 'a' || ch == 's' || ch == 'd') { if (abs(ch - snake.direction) != 2) { // 判断是否按了相反的方向 snake.direction = ch; } } } } // 更新游戏状态 void update() { // 更新蛇身的位置 for (int i = snake.length - 1; i > 0; i--) { snake.bodyX[i] = snake.bodyX[i - 1]; snake.bodyY[i] = snake.bodyY[i - 1]; } snake.bodyX[0] = snake.x; snake.bodyY[0] = snake.y; // 根据方向更新蛇头的位置 switch (snake.direction) { case 'w': snake.y--; break; case 'a': snake.x--; break; case 's': snake.y++; break; case 'd': snake.x++; break; } // 判断是否撞墙或自咬 if (snake.x < 1 || snake.x > WIDTH || snake.y < 1 || snake.y > HEIGHT) { printf("Game over!\n"); system("pause"); // 暂停游戏,等待用户按任意键继续 init(); // 重新开始游戏 return; } for (int i = 1; i < snake.length; i++) { if (snake.x == snake.bodyX[i] && snake.y == snake.bodyY[i]) { printf("Game over!\n"); system("pause"); // 暂停游戏,等待用户按任意键继续 init(); // 重新开始游戏 return; } } // 判断是否吃到食物 if (snake.x == food.x && snake.y == food.y) { snake.length++; // 蛇长度加1 score += 10; // 得分加10 if (score % 50 == 0) { speed -= 10; // 每得50分,速度加快10毫秒 } food.x = rand() % WIDTH; // 重新生成食物的位置 food.y = rand() % HEIGHT; } } // 主函数 int main() { init(); // 初始化游戏 while (1) { display(); // 显示游戏画面 input(); // 处理用户输入 update(); // 更新游戏状态 Sleep(speed); // 暂停一段时间,使蛇移动起来 } return 0; } ``` 这个代码实现了一个简单的贪吃蛇游戏,包括蛇的移动、食物的随机生成、得分的计算、撞墙和自咬的判定等功能。你可以在Windows操作系统上编译运行这个代码,体验一下这个经典游戏

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值