贪吃蛇游戏C语言破解:成为编程高手的必修课!

个人主页:秋风起,再归来~

文章专栏:C语言实战项目

个人格言:悟已往之不谏,知来者犹可追

克心守己,律己则安!

1、游戏效果演示

贪吃蛇游戏效果演示

2、win32 API介绍

这里实现贪吃蛇会使⽤到的⼀些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

也可以通过命令设置控制台窗⼝的名字:

title 贪吃蛇

这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。例如

#include<stdlib.h>
int main()
{
	//设置控制台相关属性
	//要包含头文件<stdlib.h>
	system("mode con cols=100 lines=25");
	system("title 贪吃蛇");
	//getchar();
	system("pause");
	return 0;
}

2.3 控制台屏幕上的坐标COORD

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

COORD类型的声明:
typedef struct _COORD {
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;

默认光标的位置~

//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型
COORD pos = { 10,20 };

2.4 GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle);

//获得标准输出设备的句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

2.5 GetConsoleCursorInfo

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.5.1 CONSOLE_CURSOR_INFO

CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度

typedef struct _CONSOLE_CURSOR_INFO {
    DWORD  dwSize;
    BOOL   bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

• dwSize,由光标填充的字符单元格的百分⽐。此值介于1到100之间。光标外观会变化,范围从完 全填充单元格到单元底部的⽔平线条。

• bVisible,游标的可⻅性。如果光标可⻅,则此成员为TRUE。

2.6 SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。

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

示例:

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<stdbool.h>
int main()
{
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度
	CONSOLE_CURSOR_INFO cursorInfo = { 0 };
	//获得光标信息
	GetConsoleCursorInfo(houtput, &cursorInfo);


	//获得光标信息
	GetConsoleCursorInfo(houtput, &cursorInfo);
	
	//修改控制台的光标信息
	cursorInfo.dwSize = 100;//比例
	cursorInfo.bVisible = false;//可见度

	//设置光标的信息
	SetConsoleCursorInfo(houtput, &cursorInfo);

	system("pause");
	return 0;
}

光标被隐藏啦!

2.7 SetConsoleCursorPosition

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

BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

示例:

int main()
{
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型
	COORD pos = { 10,20 };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);

	getchar();
	//system("pause");
	return 0;
}

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

//把设置光标位置的操作封装成为一个函数
void _SetPos(short x, short y)
{
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型
	COORD pos = { x,y };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

2.8 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

3、<locale.h>本地化

如上图游戏效果演示所示:

在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤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,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。

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

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。在标准中,依赖地区的部分有以下⼏项:

++• 数字量的格式++

++• 货币量的格式++

++• 字符集++

++• ⽇期和时间的表⽰形式++

3.1 类项

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

++• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。++

++• LC_CTYPE:影响字符处理函数的⾏为。++

++• LC_MONETARY:影响货币格式。++

++• LC_NUMERIC:影响 printf() 的数字格式。++

++• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。++

++• LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境++

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

#include<locale.h>
int main()
{
	//C语言标准模式
	char* ret = setlocale(LC_ALL, NULL);
	printf("%s\n", ret);

	//本地化之后的模式
	ret = setlocale(LC_ALL, "");
	printf("%s\n", ret);
	return 0;
}

3.3宽字符的打印

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

宽字符的字⾯量必须加上前缀"L",否则C语⾔会把字⾯量当作窄字符类型处理。

前缀"L"在单引 号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls 。

#include<locale.h>
int main()
{
	//本地化
	setlocale(LC_ALL, "");
	//打印宽字符
	wchar_t ch1 = L'●';
	wchar_t ch2 = L'□';
	wchar_t ch3 = L'★';
	wprintf(L"%lc\n", ch1);
	wprintf(L"%lc\n", ch2);
	wprintf(L"%lc\n", ch3);
	return 0;
}

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

4、游戏实现的整体思路

++1. (第一界面)通过贪吃蛇游戏的视频演示我们可以知道我们首先要在屏幕上打印欢迎界面++

++2. (第二界面)然后再在屏幕上打印帮助手册++

++3. (第三界面)接着在屏幕上打印游戏运行时的界面++

++4. (第四界面)游戏结束后再在屏幕上打印结束原因和最后得分++

++5. (第五界面)打印玩家是否想在来一局++

4.1 我们要用到的数据结构

实现这些步骤前,我们先要思考并完成我们要用到的数据结构

在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信 息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,

所以蛇节点结构如下:

//蛇身的节点类型
typedef struct SnakeNode
{
	//蛇的身体的每个节点的坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

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

//创建一个结构体类型来维护蛇的各种信息
typedef struct Snake
{
	pSnakeNode _pSnakeHead;//维护蛇头的指针
	pSnakeNode _pFood;//维护食物的指针
	enum DIRECTION _dri;//维护蛇的方向
	enum GAME_STATE _state;//维护蛇的状态
	int _score;//维护当前游戏的总分
	int _foodWeight;//维护一个食物默认的分数
	int _sleepTime;//维护蛇的速度
}Snake,*pSnake;

蛇的⽅向,可以⼀⼀列举,使⽤枚举:

//枚举蛇的四种方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

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

//枚举四种游戏状态
enum GAME_STATE
{
	OK,//正常运行
	END_NORMAL,//正常退出
	KILL_BY_WALL,//撞到墙了
	KILL_BY_SELF//撞到自己了
};

4.2 封装三个文件

++1.test.h(游戏测试逻辑)++

++2. snake.h(所有头文件的包含和函数的声明)++

++3 snake.c(函数的具体实现)++

4.3 test.c

这里是游戏的整体逻辑,具体实现在snake.c中

#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"

//完成游戏的测试逻辑
void test()
{
	int ch = 0;
	do
	{
		//创建贪吃蛇
		Snake snake = { 0 };
		pSnake ps = &snake;
		//初始化游戏
		//0. 光标隐藏
		//1. 打印欢迎界面
		//2. 绘制地图
		//3. 蛇身初始化
		//4. 食物初始化
		GameStart(&snake);
		//运行游戏
		GameRun(&snake);
		//结束游戏(善后工作)
		GameEnd(&snake);
		/*SetPos(62, 18);
		printf("您要再来一局吗?(Y/N):");*/
		ch = getchar();
		//清理缓冲区里面的内容
		getchar(); 
		system("cls");
	} while (ch == 'Y' || ch == 'y');
}

int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();
	return 0;
}

4.4 snake.h

我先把会用到的头文件,结构类型和函数声明放到这里(看完这些之后后面的代码就更容易看懂)

下面有很详细的注释,先不需要知道函数具体怎么实现,这里只需要知道它们的功能即可。

当然,还有一些函数并没在这里声明(这些函数大多都是为实现某个功能为具体的另一个函数(被这个函数调用)服务却没有在整体上都用到)

#pragma once
//包含所需要的头文件
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>
#include<conio.h>

#define WALL L'□'
#define POS_X 24
#define POS_Y 5
#define BODY L'●'
#define FOOD L'★'
//定义宏判断键盘上的按键是否被按过
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)

//类型的声明

//枚举蛇的四种方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//枚举四种游戏状态
enum GAME_STATE
{
	OK,//正常运行
	END_NORMAL,//正常退出
	KILL_BY_WALL,//撞到墙了
	KILL_BY_SELF//撞到自己了
};

//蛇身的节点类型
typedef struct SnakeNode
{
	//蛇的身体的每个节点的坐标
	int x;
	int y;
	//指向下一个节点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;

//创建一个结构体类型来维护蛇的各种信息
typedef struct Snake
{
	pSnakeNode _pSnakeHead;//维护蛇头的指针
	pSnakeNode _pFood;//维护食物的指针
	enum DIRECTION _dri;//维护蛇的方向
	enum GAME_STATE _state;//维护蛇的状态
	int _score;//维护当前游戏的总分
	int _foodWeight;//维护一个食物默认的分数
	int _sleepTime;//维护蛇的速度
}Snake,*pSnake;


//函数的声明

//游戏初始化
void GameStart(pSnake ps);

//定位光标位置
void SetPos(int x, int y);

//打印地图
void CreatMap();

//初始化蛇身
void InitSnake(pSnake ps);

//食物初始化
void CreateFood(pSnake ps);

//游戏运行
void GameRun(pSnake ps);

//游戏结束(善后工作)
void GameEnd(pSnake ps);

//蛇走一步
void SnakeMove(pSnake ps);

//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);

//下一个位置不是食物
void NoFood(pSnake ps,pSnakeNode pn);

//检查是否撞墙
void KillByWall(pSnake ps);

//检查是否撞到自己
void KillBySelf(pSnake ps);

//检测是否有按键被按下
void KeyFun();

4.5 GameStart(游戏初始化)

1.SetPos(定位光标位置)

//把设置光标位置的操作封装成为一个函数
void SetPos(int x, int y)
{
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型
	COORD pos = { x,y };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

2.游戏初始化逻辑

//游戏初始化
void GameStart(pSnake ps)
{
	//0. 先设置窗口的大小再隐藏光标
	system("mode con cols=150 lines=40");
	system("title 贪吃蛇");
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度
	CONSOLE_CURSOR_INFO cursorInfo = { 0 };
	//获得光标信息
	GetConsoleCursorInfo(houtput, &cursorInfo);
	//修改控制台的光标信息
	cursorInfo.dwSize = 100;//比例
	cursorInfo.bVisible = false;//可见度
	//设置光标的信息
	SetConsoleCursorInfo(houtput, &cursorInfo);

	//1. 打印欢迎界面和游戏功能介绍
	WelcomeToGame();
	//2. 绘制地图
	CreatMap();
	//3. 蛇身初始化
	InitSnake(ps);
	//4. 食物初始化
	CreateFood(ps);
}

4.5.1 WelcomeToGame(打印欢迎界面)

//打印欢迎界面
void WelcomeToGame()
{
	//第一界面(欢迎界面)
	SetPos(62, 14);
	printf("欢迎来到贪吃蛇小游戏!");
	SetPos(64, 16);
	system("pause");
	system("cls");//清理屏幕
	//第二界面(游戏功能介绍)
	SetPos(50, 12);
	printf("1、用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速!");
	SetPos(50, 13);
	printf("2、加速将可以得到更高的分数!");
	SetPos(65, 15);
	system("pause");
	system("cls");//清理屏幕
}

4.5.2 CreatMap(绘制地图)

//打印地图
void CreatMap()
{
	int i = 0;
	for (int i = 0; i <= 100; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 36);
	for (int i = 0; i <= 100; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	for (int i = 1; i <= 36; i ++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 1; i <= 36; i++)
	{
		SetPos(100, i);
		wprintf(L"%lc", WALL);
	}
}

4.5.3 InitSnake(蛇身初始化)

//初始化蛇身
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	//创建5个蛇身的节点
	for (int i = 1; i <= 5; i++)
	{
		cur = CreatSnakeNode();
		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		//将5个蛇身节点串起来
		if (ps->_pSnakeHead == NULL)
		{//蛇头为空就直接插入
			ps->_pSnakeHead = cur;
		}
		else//头插
		{
			cur->next = ps->_pSnakeHead;
			ps->_pSnakeHead = cur;
		}
	}
	cur = ps->_pSnakeHead;
	while (cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_dri = RIGHT;//方向默认向右
	ps->_foodWeight = 10;
	ps->_score = 0;
	ps->_sleepTime = 200;
	ps->_state = OK;
}

4.5.4 CreateFood(食物初始化)

//初始化食物
void CreateFood(pSnake ps)
{
	//先随机生成食物的坐标
	int x = 0;
	int y = 0;
again:
	//生成的位置必须在地图内部
	do
	{
		x = rand() % 97 + 2;
		y = rand() % 35 + 1;
	} while (x % 2 != 0);
	pSnakeNode cur = ps->_pSnakeHead;
	//判断食物位置是否与蛇身重叠
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	//打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	pSnakeNode food = CreatSnakeNode();
	food->x = x;
	food->y = y;
	food->next = NULL;
	//将食物节点放到ps中维护起来
	ps->_pFood = food;
}

4.6 GameRun(游戏运行)

1.检测按键是否被按过

//定义宏判断键盘上的按键是否被按过
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)

2.检测是否有按键被按下

//检测是否有按键被按下
void KeyFun()
{
	while (_kbhit())
	{
		//使用_getch()获取按下的键,不阻塞程序
		int key = _getch();
	}
}

3.游戏暂停

//游戏暂停
void Pause()
{
	while(1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(200);
		KeyFun();
	}
}

游戏运行逻辑

//游戏运行
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do 
	{
		SetPos(104, 12);
		printf("按空格键开始游戏!");
		SetPos(104, 13);
		printf("您当前的总分是%3d", ps->_score);
		SetPos(104, 14);
		printf("当前每个食物的总分是%3d", ps->_foodWeight);
		if (KEY_PRESS(VK_UP) && ps->_dri != DOWN)
		{
			ps->_dri = UP;//上
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dri != UP)
		{
			ps->_dri = DOWN;//下
		}	
		else if(KEY_PRESS(VK_LEFT) && ps->_dri != RIGHT)
		{
			ps->_dri = LEFT;//左
		}
		else if(KEY_PRESS(VK_RIGHT) && ps->_dri != LEFT)
		{
			ps->_dri = RIGHT;//右
		}
		else if (KEY_PRESS(VK_SPACE) )
		{
			Pause();//暂停
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = END_NORMAL;//正常退出
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->_sleepTime>80)
			{
				ps->_sleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->_foodWeight>2)
			{
				ps->_sleepTime += 30;
				ps->_foodWeight -= 2;
			}
		}
		//走一步
		SnakeMove(ps);
		//每走一步休息一下
		Sleep(ps->_sleepTime);
		KeyFun();
	} while (ps->_state == OK);
}

4.6.1 PrintHelpInfo(右侧打印帮助手册)

//打印帮助手册
void PrintHelpInfo()
{
	SetPos(104, 16);
	printf("--------------------------------------------");
	SetPos(104, 17);
	printf("|1、不能撞墙,不能咬到自己!               |");
	SetPos(104, 18);
	printf("|2、用 ↑ . ↓ . ← . → 分别控制蛇的移动! |");
	SetPos(104, 19);
	printf("|3、F3为加速,F4为减速!                   |");
	SetPos(104, 20);
	printf("|4、按Esc退出游戏,按空格暂停游戏 !        |");
	SetPos(104, 21);
	printf("--------------------------------------------");
	SetPos(60, 17);
}

4.6.2 SnakeMove(蛇走一步)

1.蛇走一步逻辑

//蛇走一步
void SnakeMove(pSnake ps)
{
	//创建一个节点来记录蛇头的下一个位置
	pSnakeNode nextHead = CreatSnakeNode();
	nextHead->next = NULL;
	switch (ps->_dri)
	{
	case UP:
		nextHead->x = ps->_pSnakeHead->x;
		nextHead->y = ps->_pSnakeHead->y - 1;
		break;
	case DOWN:
		nextHead->x = ps->_pSnakeHead->x;
		nextHead->y = ps->_pSnakeHead->y +1;
		break;
	case LEFT:
		nextHead->y = ps->_pSnakeHead->y;
		nextHead->x = ps->_pSnakeHead->x - 2;
		break;
	case RIGHT:
		nextHead->y = ps->_pSnakeHead->y;
		nextHead->x = ps->_pSnakeHead->x + 2;
		break;
	default:
		break;
	}
	//判断下一个节点是不是食物
	if((ps->_pFood->x == nextHead->x) && (ps->_pFood->y == nextHead->y))
	{
		//下一个是食物那就吃掉食物
		EatFood(nextHead, ps);
	}
	else
	{
		//下一个位置不是食物
		NoFood(ps, nextHead);
	}
	//
	KillByWall(ps);
	KillBySelf(ps);
}

2. EatFood(吃掉食物)

//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插吃掉食物
	ps->_pFood->next = ps->_pSnakeHead;
	ps->_pSnakeHead = ps->_pFood;
	ps->_score += ps->_foodWeight;//分数增加
	//打印蛇身
	pSnakeNode cur = ps->_pSnakeHead;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//释放刚刚malloc的位置
	free(pn);
	pn = NULL;
	//食物被吃掉了,那就再创建一个食物
	CreateFood(ps);
}

3. NoFood往前走一步(不是食物)

//下一个位置不是食物
void NoFood(pSnake ps,pSnakeNode pn)
{
	//头插
	pn->next = ps->_pSnakeHead;
	ps->_pSnakeHead = pn;
	pSnakeNode cur = ps->_pSnakeHead;
	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);
	cur->next = NULL;
}

4. KillByWall(检测是否撞墙)

//检查是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnakeHead->x == 0
		|| ps->_pSnakeHead->x == 98
		|| ps->_pSnakeHead->y == 0
		|| ps->_pSnakeHead->y == 36)
	{
		ps->_state = KILL_BY_WALL;
		return;
	}
	return;
}

5. KillBySelf(检测是否撞到自己)

//检查是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnakeHead->next;
	while (cur)
	{
		if ((cur->x == ps->_pSnakeHead->x) &&
			(cur->y == ps->_pSnakeHead->y))
		{
			ps->_state = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
	return;
}

4.7 GameEnd(游戏善后)

//游戏善后
void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnakeHead;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	system("cls");
	SetPos(62, 16);
	switch (ps->_state)
	{
	case END_NORMAL:
		printf("您主动退出游戏!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了!\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到自己了!\n");
		break;
	}
	SetPos(62, 17);
	printf("您最终的成绩是:%d", ps->_score);
	Sleep(2000);
	KeyFun();
	system("cls");
	SetPos(62, 18);
	printf("您要再来一局吗?(Y/N):");
}

4.8 snake.c(完整代码)

#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"

//检测是否有按键被按下
void KeyFun()
{
	while (_kbhit())
	{
		//使用_getch()获取按下的键,不阻塞程序
		int key = _getch();
	}
}

//把设置光标位置的操作封装成为一个函数
void SetPos(int x, int y)
{
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型
	COORD pos = { x,y };

	//设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

//打印地图
void CreatMap()
{
	int i = 0;
	for (int i = 0; i <= 100; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 36);
	for (int i = 0; i <= 100; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	for (int i = 1; i <= 36; i ++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 1; i <= 36; i++)
	{
		SetPos(100, i);
		wprintf(L"%lc", WALL);
	}
}

//打印欢迎界面
void WelcomeToGame()
{
	//第一界面(欢迎界面)
	SetPos(62, 14);
	printf("欢迎来到贪吃蛇小游戏!");
	SetPos(64, 16);
	system("pause");
	system("cls");//清理屏幕
	//第二界面(游戏功能介绍)
	SetPos(50, 12);
	printf("1、用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速!");
	SetPos(50, 13);
	printf("2、加速将可以得到更高的分数!");
	SetPos(65, 15);
	system("pause");
	system("cls");//清理屏幕
}

//创建1个蛇身的节点
pSnakeNode CreatSnakeNode()
{
	pSnakeNode ret = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (ret == NULL)
	{
		perror("CreatSnakeNode::fail\n");
		return NULL;
	}
	return ret;
}

//初始化蛇身
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	//创建5个蛇身的节点
	for (int i = 1; i <= 5; i++)
	{
		cur = CreatSnakeNode();
		cur->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		//将5个蛇身节点串起来
		if (ps->_pSnakeHead == NULL)
		{//蛇头为空就直接插入
			ps->_pSnakeHead = cur;
		}
		else//头插
		{
			cur->next = ps->_pSnakeHead;
			ps->_pSnakeHead = cur;
		}
	}
	cur = ps->_pSnakeHead;
	while (cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_dri = RIGHT;//方向默认向右
	ps->_foodWeight = 10;
	ps->_score = 0;
	ps->_sleepTime = 200;
	ps->_state = OK;
}

//初始化食物
void CreateFood(pSnake ps)
{
	//先随机生成食物的坐标
	int x = 0;
	int y = 0;
again:
	//生成的位置必须在地图内部
	do
	{
		x = rand() % 97 + 2;
		y = rand() % 35 + 1;
	} while (x % 2 != 0);
	pSnakeNode cur = ps->_pSnakeHead;
	//判断食物位置是否与蛇身重叠
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	//打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	pSnakeNode food = CreatSnakeNode();
	food->x = x;
	food->y = y;
	food->next = NULL;
	//将食物节点放到ps中维护起来
	ps->_pFood = food;
}

//打印帮助手册
void PrintHelpInfo()
{
	SetPos(104, 16);
	printf("--------------------------------------------");
	SetPos(104, 17);
	printf("|1、不能撞墙,不能咬到自己!               |");
	SetPos(104, 18);
	printf("|2、用 ↑ . ↓ . ← . → 分别控制蛇的移动! |");
	SetPos(104, 19);
	printf("|3、F3为加速,F4为减速!                   |");
	SetPos(104, 20);
	printf("|4、按Esc退出游戏,按空格暂停游戏 !        |");
	SetPos(104, 21);
	printf("--------------------------------------------");
	SetPos(60, 17);
}

//游戏暂停
void Pause()
{
	while(1)
	{
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
		Sleep(200);
		KeyFun();
	}
}

//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插吃掉食物
	ps->_pFood->next = ps->_pSnakeHead;
	ps->_pSnakeHead = ps->_pFood;
	ps->_score += ps->_foodWeight;//分数增加
	//打印蛇身
	pSnakeNode cur = ps->_pSnakeHead;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//释放刚刚malloc的位置
	free(pn);
	pn = NULL;
	//食物被吃掉了,那就再创建一个食物
	CreateFood(ps);
}

//下一个位置不是食物
void NoFood(pSnake ps,pSnakeNode pn)
{
	//头插
	pn->next = ps->_pSnakeHead;
	ps->_pSnakeHead = pn;
	pSnakeNode cur = ps->_pSnakeHead;
	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);
	cur->next = NULL;
}

//蛇走一步
void SnakeMove(pSnake ps)
{
	//创建一个节点来记录蛇头的下一个位置
	pSnakeNode nextHead = CreatSnakeNode();
	nextHead->next = NULL;
	switch (ps->_dri)
	{
	case UP:
		nextHead->x = ps->_pSnakeHead->x;
		nextHead->y = ps->_pSnakeHead->y - 1;
		break;
	case DOWN:
		nextHead->x = ps->_pSnakeHead->x;
		nextHead->y = ps->_pSnakeHead->y +1;
		break;
	case LEFT:
		nextHead->y = ps->_pSnakeHead->y;
		nextHead->x = ps->_pSnakeHead->x - 2;
		break;
	case RIGHT:
		nextHead->y = ps->_pSnakeHead->y;
		nextHead->x = ps->_pSnakeHead->x + 2;
		break;
	default:
		break;
	}
	//判断下一个节点是不是食物
	if((ps->_pFood->x == nextHead->x) && (ps->_pFood->y == nextHead->y))
	{
		//下一个是食物那就吃掉食物
		EatFood(nextHead, ps);
	}
	else
	{
		//下一个位置不是食物
		NoFood(ps, nextHead);
	}
	//
	KillByWall(ps);
	KillBySelf(ps);
}

//检查是否撞墙
void KillByWall(pSnake ps)
{
	if (ps->_pSnakeHead->x == 0
		|| ps->_pSnakeHead->x == 98
		|| ps->_pSnakeHead->y == 0
		|| ps->_pSnakeHead->y == 36)
	{
		ps->_state = KILL_BY_WALL;
		return;
	}
	return;
}

//检查是否撞到自己
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnakeHead->next;
	while (cur)
	{
		if ((cur->x == ps->_pSnakeHead->x) &&
			(cur->y == ps->_pSnakeHead->y))
		{
			ps->_state = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
	return;
}

//游戏初始化
void GameStart(pSnake ps)
{
	//0. 先设置窗口的大小再隐藏光标
	system("mode con cols=150 lines=40");
	system("title 贪吃蛇");
	//获得标准输出设备的句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度
	CONSOLE_CURSOR_INFO cursorInfo = { 0 };
	//获得光标信息
	GetConsoleCursorInfo(houtput, &cursorInfo);
	//修改控制台的光标信息
	cursorInfo.dwSize = 100;//比例
	cursorInfo.bVisible = false;//可见度
	//设置光标的信息
	SetConsoleCursorInfo(houtput, &cursorInfo);

	//1. 打印欢迎界面和游戏功能介绍
	WelcomeToGame();
	//2. 绘制地图
	CreatMap();
	//3. 蛇身初始化
	InitSnake(ps);
	//4. 食物初始化
	CreateFood(ps);
}

//游戏运行
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do 
	{
		SetPos(104, 12);
		printf("按空格键开始游戏!");
		SetPos(104, 13);
		printf("您当前的总分是%3d", ps->_score);
		SetPos(104, 14);
		printf("当前每个食物的总分是%3d", ps->_foodWeight);
		if (KEY_PRESS(VK_UP) && ps->_dri != DOWN)
		{
			ps->_dri = UP;//上
		}
		else if (KEY_PRESS(VK_DOWN) && ps->_dri != UP)
		{
			ps->_dri = DOWN;//下
		}	
		else if(KEY_PRESS(VK_LEFT) && ps->_dri != RIGHT)
		{
			ps->_dri = LEFT;//左
		}
		else if(KEY_PRESS(VK_RIGHT) && ps->_dri != LEFT)
		{
			ps->_dri = RIGHT;//右
		}
		else if (KEY_PRESS(VK_SPACE) )
		{
			Pause();//暂停
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_state = END_NORMAL;//正常退出
		}
		else if (KEY_PRESS(VK_F3))
		{
			//加速
			if (ps->_sleepTime>80)
			{
				ps->_sleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			//减速
			if (ps->_foodWeight>2)
			{
				ps->_sleepTime += 30;
				ps->_foodWeight -= 2;
			}
		}
		//走一步
		SnakeMove(ps);
		//每走一步休息一下
		Sleep(ps->_sleepTime);
		KeyFun();
	} while (ps->_state == OK);
}

//游戏善后
void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnakeHead;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	system("cls");
	SetPos(62, 16);
	switch (ps->_state)
	{
	case END_NORMAL:
		printf("您主动退出游戏!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了!\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到自己了!\n");
		break;
	}
	SetPos(62, 17);
	printf("您最终的成绩是:%d", ps->_score);
	Sleep(2000);
	KeyFun();
	system("cls");
	SetPos(62, 18);
	printf("您要再来一局吗?(Y/N):");
}

5、 完结散花

好了,这期的分享到 这里就结束了~

如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~

如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~

我们下期不见不散~~

​​​​

相关推荐
海棠AI实验室1 小时前
AI的进阶之路:从机器学习到深度学习的演变(一)
人工智能·深度学习·机器学习
IT古董2 小时前
【机器学习】机器学习的基本分类-强化学习-策略梯度(Policy Gradient,PG)
人工智能·机器学习·分类
睡觉狂魔er2 小时前
自动驾驶控制与规划——Project 3: LQR车辆横向控制
人工智能·机器学习·自动驾驶
scan7243 小时前
LILAC采样算法
人工智能·算法·机器学习
菌菌的快乐生活3 小时前
理解支持向量机
算法·机器学习·支持向量机
爱喝热水的呀哈喽3 小时前
《机器学习》支持向量机
人工智能·决策树·机器学习
大山同学3 小时前
第三章线性判别函数(二)
线性代数·算法·机器学习
苏言の狗3 小时前
Pytorch中关于Tensor的操作
人工智能·pytorch·python·深度学习·机器学习
bastgia4 小时前
Tokenformer: 下一代Transformer架构
人工智能·机器学习·llm
paixiaoxin6 小时前
CV-OCR经典论文解读|An Empirical Study of Scaling Law for OCR/OCR 缩放定律的实证研究
人工智能·深度学习·机器学习·生成对抗网络·计算机视觉·ocr·.net