用C语言手把手教你写贪吃蛇

效果(视频)

贪吃蛇

开始可能有点糊【上传后就糊了,不知道为什么】

全部代码放在了结尾

实现过程的解释大部分都在注释中有


实现环境

这次的贪吃蛇的实现需要借助Windows环境的控制台【一般是输出到屏幕时的黑框】
所以要先准备好环境

例:使用 VS
VS默认的标准输出的黑框是由Windows决定的。
此时就要对其进行修改,不然无法改变控制台的大小,宽字符的打印也会有问题

修改流程如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


如果要将其再修改回去,过程如下:

鼠标右击黑框上边缘,
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


实现所需的一些Win32 API知识

Win32 API:

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

改变控制台大小和标题

使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,40⾏,100列

cmd命令为:mode con cols=40 lines=100

通过cmd命令设置控制台窗⼝的名字:
cmd命令为: title 贪吃蛇

在C语言程序中使用cmd命令可以使用函数system

在这里插入图片描述

隐藏光标

为防止输入光标影响观感,可以隐藏输入光标
隐藏光标可分为以下几个流程

  1. 使用GetStdHandle函数获得控制台句柄,这样才可以改变控制台的光标属性

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

GetStdHandle函数的参数只有三个【标准输⼊、标准输出或标准错误】,它的返回值为句柄指针【HANDLE】

任意创建一个句柄指针变量接收GetStdHandle函数的返回值,就获得了对应标准设备的句柄

控制台是标准输出设备,对应GetStdHandle函数的参数为STD_OUTPUT_HANDLE

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

  1. 创建包含有关控制台光标的信息的结构变量
    包含有关控制台光标的信息的结构体CONSOLE_CURSOR_INFO 的定义为:
typedef struct _CONSOLE_CURSOR_INFO {
 DWORD dwSize;
 BOOL  bVisible;
 } CONSOLE_CURSOR_INFO
  • dwSize,由光标填充的字符单元格的百分⽐。此值介于1到100之间。光标外观会变化,范围从完 全填充一个字符单元格到单元底部的⽔平线条。

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


  1. 检索(获取)控制台光标信息
    使用函数GetConsoleCursorInfo(句柄,包含有关控制台光标的信息的结构变量的地址)

  1. 设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
    使用函数SetConsoleCursorInfo(句柄,包含有关控制台光标的信息的结构变量的地址)

完整代码

获得控制台句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

创建包含有关控制台光标的信息的结构变量
CONSOLE_CURSOR_INFO CursorInfo;

 GetConsoleCursorInfo(hOutput, &CursorInfo);获取控制台光标信息
 
CursorInfo.bVisible = false; 隐藏控制台光标
 
SetConsoleCursorInfo(hOutput, &CursorInfo);设置控制台光标状态
 

设置光标位置

  1. 利用结构体COORDCOORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标(X,Y)】定位坐标
    在这里插入图片描述
typedef struct _COORD {
 SHORT X;
 SHORT Y;
 } COORD

  1. 设置控制台光标坐标
    利用函数SetConsoleCursorPosition(句柄,COORD变量地址)

完整代码:

 COORD a = { 10, 5};创建COORD变量并赋值
 
 HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);获取标准输出的句柄
 
SetConsoleCursorPosition(hOutput, pos);设置标准输出上光标的位置为a中的成员

获取按键情况

因为要通过↑,↓,←,→来操纵贪吃蛇,所以我们需要知道那些按键被按过,并以此做出响应。

通过函数GetAsyncKeyState(虚拟键值【键盘上每一个键都有对应的虚拟键值】)的返回值来判断按键情况

GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤GetAsyncKeyState 函数后,如果返回的16位的short数据中
最⾼位的比特位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;
如果最低位被置为1则说明,该按键被按过,否为0。

所以如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低的比特位的值是否为1

根据此特点我们可以构建一个宏(函数也可以,只不过小运算宏比函数要方便,运算更快

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

上面的宏的运行流程为:
调用宏将虚拟键值(VK)传给宏,宏再把虚拟键值(VK)传给GetAsyncKeyState函数,GetAsyncKeyState函数判断按键情况后返回一个值,返回的值按位与(&)1后即可得出返回值的最低位的比特位的值。
如果返回值的最低位的比特位的值为1,条件运算的结果就为真,宏KEY_PRESS的就为1
如果返回值的最低位的比特位的值为0,条件运算的结果就为假,宏KEY_PRESS的就为0


C语言本地化

由于过去C语⾔并不适合⾮英语国家(地区)使⽤。所以那时是C语言没有宽字符(占两个字节的字符,例如汉字就是宽字符)一说,所以宽字符不能借助字符变量打印。
后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。
⽐如:加⼊和宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊和<locale.h>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

所以要借助字符变量打印宽字符就需要本地化
本地化可借助函数setlocale(类项,“C”或者“”);

setlocale函数的第一个参数是类项,本土化的类项有以下几种:
LC_COLLATE
LC_CTYPE
LC_MONETARY
LC_NUMERIC
LC_TIME
LC_ALL-针对所有类项修改

C标准给setlocale的第⼆个参数仅定义了2种可能取值:“C”和""。

“C”表示C的国际标准,“”表示适应本地标准

所以要本土化就直接使用以下代码

setlocale(LC_ALL, " ");

那么本土化后怎么样可以借助字符变量来打印宽字符呢?

要借助字符变量来打印宽字符,就要先定义 宽字符变量

宽字符的类型为 wchar_t ,使用该类型定义变量后,在给定义的变量赋值时还要在赋值的宽字符前加L

打印宽字符时要用wprintf函数打印
在这里插入图片描述


实现贪吃蛇

准备

创建文件

  • 头文件Snake.h用于存放函数声明和结构体定义等
  • Snake.c用于存放函数的实现
  • test.c用于存放main函数等

包含头文件

  • stdio.h:用于使用布尔类型
  • stdio.h:标准输入输出
  • stdlib.h:用于使用动态内存管理函数
  • windows.h:用于使用Win32 API中的函数
  • time.h:用于产生时间戳
  • locale.h:用于本土化

define定义宏常量和宏

在这里插入图片描述

定义结构体

定义一个结构体Snake将贪吃蛇游戏的状态和蛇的状态放入其中,方便管理
在这里插入图片描述

  • pSnake:我们使用单链表来维护蛇的身体,pSnake就指向蛇身单链表的第一个节点
  • SnakeSpeed:因为程序执行得太快,所以需要停顿给玩家反应时间,停顿时间越长蛇的速度越慢,停顿时间越短
    在这里插入图片描述

创建枚举体

在这里插入图片描述

在main函数中准备

在这里插入图片描述
随机生成食物时需要用到随机数,所以在main函数中设置变化的随机数种子
在这里插入图片描述
test.c全部代码

#define _CRT_SECURE_NO_WARNINGS


#include"Snake.h"

void game()
{
	Snake snake;
	GameStart(&snake);//游戏开始前的初始化
}

int main()
{
	//本土化
	setlocale(LC_ALL, "");
	//设置变化的随机数种子
	srand((unsigned int)time(NULL));
	game();
	return 0;
}

打印选项界面和游戏初始化

游戏初始化

在这里插入图片描述

初始化蛇

在这里插入图片描述

打印游戏开始前的界面

为了方便设置控制台光标的位置,我们将它分装成一个函数
在这里插入图片描述
在这里插入图片描述

根据按键情况选择选项,确认选项

	打印游戏开始前的界面
	Welcome();
	
	enum OPTION option = BEGIN;默认选项为开始游戏
	do
    {
		    打印选项界面
		    SetPos(40, 15);
			printf("1.开始游戏");
			SetPos(40, 16);
			printf("2.排行榜");
			SetPos(36, 18);
			printf("请按↑,↓选择选项,按Enter键确认选项");
			SetPos(36, 19);
			printf("按Esc退出游戏。");
			if (KEY_PRESS(VK_UP))如果按了↑就进去
			{
				SetPos(38, 16);按了↑,→就指向第15printf("  ");所以把第16行的→用空格覆盖
				SetPos(38, 15);
				printf("→");再在第15行打印→

				option = BEGIN;更改枚举变量的值为  开始
			}
			else if (KEY_PRESS(VK_DOWN))如果按了↑就进去
			{
				SetPos(38, 15);按了↓,→就指向第16printf("  ");所以把第15行的→用空格覆盖
				SetPos(38, 16);
				printf("→");再在第16行打印→

				option = CHART;更改枚举变量的值为  排行榜
			}

			if (option ==BEGIN)
			{
				SetPos(38, 16);
				printf("  ");
				SetPos(38, 15);
				printf("→");解决第一次时  →没有指向的选项
				if (KEY_PRESS(VK_RETURN) == 1)如果选项状态为  开始 并且  按了Enter键就进去
				{
					system("cls");清屏
					SetPos(35, 15);设置控制台光标到  合适  的位置
					printf("开始游戏");打印提示信息
					SetPos(35, 17);
					system("pause");达成  按任意键继续。。。 的效果
					system("cls");
					GameRun(snake);游戏进行
					GameEnd(snake);游戏善后
					system("cls");
				}
			}
			if (option == CHART)
			{
				SetPos(38, 15);
				printf("  ");
				SetPos(38, 16);
				printf("→");解决第一次时  →没有指向的选项
				if (KEY_PRESS(VK_RETURN) == 1)如果选项状态为  排行榜 并且  按了Enter键就进去
				{
					system("cls");清屏
					SetPos(35, 15);
					printf("排行榜");
					SetPos(35, 17);
					system("pause");达成  按任意键继续。。。 的效果
					system("cls");
					
					ChartPrint(snake);打印排行榜
					system("pause");
					system("cls");
				}
			}
		} while (KEY_PRESS(VK_ESCAPE) != 1);如果按Esc就结束循环
}

打印游戏开始后的墙和提示信息

在这里插入图片描述

打印游戏时的界面
void PlayInte(Snake* snake)
{
	打印墙
	上墙体
	int i = 0;
	wchar_t ch = L'■';宽字符定义并赋值

	for (i = 0; i <COR; i += 2)因为一个宽字符占两个字节并且一个宽字符占两个单位的横坐标
	{                          所以i+=2
		SetPos(i,0);
		wprintf(L"%lc", ch);打印宽字符
	}

	下墙体
	for (i = 0; i < COR; i += 2)因为一个宽字符占两个字节并且一个宽字符占两个单位的横坐标
                               所以i+=2
	{
		SetPos(i,ROW-1);
		wprintf(L"%lc", ch);
	}
	
	左侧墙体
	for (i = 1; i < ROW; i++)虽然一个宽字符占  2  个单位的横坐标
		                     但是一个宽字符只占  1  个单位的纵坐标
	{
		SetPos(0,i);
		wprintf(L"%lc", ch);
	}

	右侧墙体
	for (i = 1; i < ROW; i++)虽然一个宽字符占  2  个单位的横坐标
		                     但是一个宽字符只占  1  个单位的纵坐标
    {
		SetPos(COR-2, i);
		wprintf(L"%lc", ch);
	}

	打印提示信息
	SetPos(65, 16);
	printf("F3加速");
	SetPos(65, 17);
	printf("F4减速");
	SetPos(65, 18);
	printf("Esc退出游戏");
	SetPos(65, 19);
	printf("空格暂停游戏");
	SetPos(65, 20);
	printf("使用↑,↓,→,←控制蛇的方向");
}

用头插法构建初始蛇身链表

在这里插入图片描述

用头插法 构建   初始   蛇身节点
for (i = 5; i >= 1; i--)
{
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));申请新节点
	if (newnode == NULL)
	{
		printf("GameRun函数中malloc失败!");
		return;
	}
	newnode->x = SNAKE_HEAD_X - i * 2;根据  初始蛇头坐标  控制链表尾节点的坐标
	newnode->y = SNAKE_HEAD_Y;        因为是头插所以第一个插入的节点在插入完成后是最后一个节点
	if (snake->pSnake == NULL)
	{
		snake->pSnake = newnode;
		newnode->next = NULL;
	}
	else
	{
		newnode->next = snake->pSnake;
		snake->pSnake = newnode;
	}
}

打印初始蛇身

打印蛇身
void SnakePrint(Snake* snake)
{
	SnakeNode* cur = snake->pSnake;获得snake中存储的蛇头
	wchar_t ch1 = L'□';为了区分蛇头用  蛇头用 □
	wchar_t ch2 = L'●';蛇身用 ●
	int flag = 0;用于标识是否为第一次打印

	while (cur)遍历链表
	{
		SetPos(cur->x, cur->y);设置控制台光标位置
		if (flag == 0)为第一次打印就  打印蛇头 □
		{
			wprintf(L"%lc", ch1);
			flag = 1;
		}
		else
		{
			wprintf(L"%lc", ch2);不为第一次打印就  打印蛇身 ●
		}
		cur = cur->next;
	}
}

随机生成食物

随机生成食物
void RandomFood(Snake* snake)
{
	蛇身节点与食物的类型一致,吃掉食物的时候就   只需要头插就行
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (newnode == NULL)
	{
		printf("RandomFood函数中malloc失败!");
		return;
	}
	SnakeNode* cur = snake->pSnake;获得snake中存储的食物信息
	控制随机食物的坐标生成的范围
	newnode->x = (rand() % ((COR - 3 - 2)/2) + 1)*2;控制食物的横坐标与蛇一样  是偶数  方便蛇吃
	newnode->y = rand() % (ROW - 1 - 1) + 1;

	食物不能生成在蛇的身体中
	while (cur)遍历链表
	{
		if (cur->x == newnode->x && cur->y == newnode->y)
		{   如果在蛇身中就再随机生成一次坐标
			newnode->x = (rand() % ((COR - 3 - 2) / 2) + 1) * 2;
			newnode->y = rand() % (ROW - 1 - 1) + 1;
			让cur回到链表开头,重新遍历,防止再次随机生成的食物又再蛇身里
			cur = snake->pSnake;
		}
		else
		{
			cur = cur->next;
		}
	}
	snake->pFood = newnode;  更改snake中存储的食物信息
	SetPos(newnode->x, newnode->y);
	wprintf(L"卍");  打印食物
}

根据按键情况操纵蛇


do
{
	打印会随游戏进行而变化的信息
	SetPos(65, 10);
	printf("当前分数:%-5d", snake->Score);
	SetPos(65, 11);
	printf("当前每一个食物的分数:%-2d", snake->foodWeight);
	SetPos(65, 13);
	printf("当前蛇的速度:%c(D为初始速度加速一次", (snake->SnakeSpeed - 200) / 40 + 'D');
	SetPos(65, 14);
	printf("为C,减速一次为E,最快为A)");
	if (KEY_PRESS(VK_UP)&&snake->Snakedir != DOWN)按上并且 蛇在向下时不能直接向上走
	{
		snake->Snakedir = UP;更改蛇的方向为上
	}
	else if (KEY_PRESS(VK_DOWN) && snake->Snakedir != UP)下并且 蛇在向上时不能直接向下走
	{
		snake->Snakedir = DOWN;更改蛇的方向为下
	}
	else if (KEY_PRESS(VK_LEFT) && snake->Snakedir != RIGHT)左并且 蛇在向右时不能直接向左走
	{
		snake->Snakedir = LEFT;更改蛇的方向为左
	}
	else if (KEY_PRESS(VK_RIGHT) && snake->Snakedir != LEFT)右并且 蛇在向左时不能直接向右走
	{
		snake->Snakedir = RIGHT;更改蛇的方向为右
	}
	else if (KEY_PRESS(VK_SPACE))暂停
	{
		pause();
	}
	else if(KEY_PRESS(VK_ESCAPE))  退出
	{
		提示信息打印
		SetPos(20, 15);
		printf("若退出游戏当前游戏进度无法保存!!!");
		SetPos(20, 16);
		printf("确定退出游戏吗?(Y/N)【输入Y/N+回车】");
		char ch = 0;
		do
		{
			ch = getchar();
			if (ch == 'Y' || ch == 'y')
			{
				snake->status = ESC;退出游戏  更改游戏状态为退出
				break;
			}
			if (ch == 'N' || ch == 'n')
			{
				继续游戏
				SetPos(20, 15);
				打印空格覆盖提示信息
				printf("                                    ");
				SetPos(20, 16);
				printf("                      ");
			}
		} while (ch!='Y' &&ch!='y' && ch != 'N' && ch != 'n');控制输入符号有效性
	}
	else if (KEY_PRESS(VK_F3))加速
	{
		if (snake->SnakeSpeed > 80)不能一直加速
		{
			snake->SnakeSpeed -= 40;加速蛇  暂停  时间减少
			snake->foodWeight += 2;加速 吃掉一个食物的分数增加
		}
	}
	else if (KEY_PRESS(VK_F4))减速
	{
		if (snake->SnakeSpeed < 320)  不能一直减速
		{
			snake->SnakeSpeed += 40;  减速蛇  暂停  时间增加
			snake->foodWeight -= 2;  减速 吃掉一个食物的分数减少
		}
	}
	休眠
	Sleep(snake->SnakeSpeed);按完键后,蛇(程序)暂停一下,给玩家反应时间
	走一步
	SnakeMove(snake);根据按键情况走一步
} while (snake->status == OK);游戏状态要OK循环才继续

游戏进行的GameRun全部代码

//游戏进行
void GameRun(Snake* snake)
{
	//打印游戏开始后界面
	PlayInte(snake);
	int i = 0;

	//用头插法 构建   初始   蛇身节点
	for (i = 5; i >= 1; i--)
	{
		SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));//申请新节点
		if (newnode == NULL)
		{
			printf("GameRun函数中malloc失败!");
			return;
		}
		newnode->x = SNAKE_HEAD_X - i * 2;//根据  初始蛇头坐标  控制链表尾节点的坐标
		newnode->y = SNAKE_HEAD_Y;        //因为是头插所以第一个插入的节点在插入完成后是最后一个节点
		if (snake->pSnake == NULL)
		{
			snake->pSnake = newnode;
			newnode->next = NULL;
		}
		else
		{
			newnode->next = snake->pSnake;
			snake->pSnake = newnode;
		}
	}

	//打印蛇身
	SnakePrint(snake);

	RandomFood(snake);
	//蛇走
	do
	{
		//打印会随游戏进行而变化的信息
		SetPos(65, 10);
		printf("当前分数:%-5d", snake->Score);
		SetPos(65, 11);
		printf("当前每一个食物的分数:%-2d", snake->foodWeight);
		SetPos(65, 13);
		printf("当前蛇的速度:%c(D为初始速度加速一次", (snake->SnakeSpeed - 200) / 40 + 'D');
		SetPos(65, 14);
		printf("为C,减速一次为E,最快为A)");
		if (KEY_PRESS(VK_UP)&&snake->Snakedir != DOWN)//按上并且 蛇在向下时不能直接向上走
		{
			snake->Snakedir = UP;//更改蛇的方向为上
		}
		else if (KEY_PRESS(VK_DOWN) && snake->Snakedir != UP)//下并且 蛇在向上时不能直接向下走
		{
			snake->Snakedir = DOWN;//更改蛇的方向为下
		}
		else if (KEY_PRESS(VK_LEFT) && snake->Snakedir != RIGHT)//左并且 蛇在向右时不能直接向左走
		{
			snake->Snakedir = LEFT;//更改蛇的方向为左
		}
		else if (KEY_PRESS(VK_RIGHT) && snake->Snakedir != LEFT)//右并且 蛇在向左时不能直接向右走
		{
			snake->Snakedir = RIGHT;//更改蛇的方向为右
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			pause();
		}
		else if(KEY_PRESS(VK_ESCAPE))//退出
		{
			//提示信息打印
			SetPos(20, 15);
			printf("若退出游戏当前游戏进度无法保存!!!");
			SetPos(20, 16);
			printf("确定退出游戏吗?(Y/N)【输入Y/N+回车】");
			char ch = 0;
			do
			{
				ch = getchar();
				if (ch == 'Y' || ch == 'y')
				{
					snake->status = ESC;//退出游戏  更改游戏状态为退出
					break;
				}
				if (ch == 'N' || ch == 'n')
				{
					//继续游戏
					SetPos(20, 15);
					//打印空格覆盖提示信息
					printf("                                    ");
					SetPos(20, 16);
					printf("                      ");
				}
			} while (ch!='Y' &&ch!='y' && ch != 'N' && ch != 'n');//控制输入符号有效性
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (snake->SnakeSpeed > 80)//不能一直加速
			{
				snake->SnakeSpeed -= 40;//加速蛇  暂停  时间减少
				snake->foodWeight += 2;//加速 吃掉一个食物的分数增加
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (snake->SnakeSpeed < 320)//不能一直减速
			{
				snake->SnakeSpeed += 40;//减速蛇  暂停  时间增加
				snake->foodWeight -= 2;//减速 吃掉一个食物的分数减少
			}
		}
		//休眠
		Sleep(snake->SnakeSpeed);//按完键后,蛇(程序)暂停一下,给玩家反应时间
		//走一步
		SnakeMove(snake);//根据按键情况走一步
	} while (snake->status == OK);//游戏状态要OK循环才继续
}

暂停函数
在这里插入图片描述

根据按键情况让蛇走

根据按键情况让蛇走一步涉及的函数

吃掉食物
void EtaFoodNode(Snake* snake)
{
	让snake中存储的食物节点的头插在蛇头上
	snake->pFood->next = snake->pSnake;
	snake->pSnake = snake->pFood;让原食物节点成为蛇头节点
	打印蛇
	SnakePrint(snake);
}
下一步不是食物
void NoFoodNode(Snake* snake, SnakeNode* newnode)
{
	让新节点头插在蛇头上
	newnode->next = snake->pSnake;
	snake->pSnake = newnode;
	SnakeNode* cur = snake->pSnake;
	SnakeNode* prev = snake->pSnake;  prev指向cur的前一个节点
	wchar_t ch1 = L'□';
	wchar_t ch2 = L'●';
	int flag = 0;
	while (cur->next)  找尾并打印
	{
		prev = cur;
		SetPos(cur->x, cur->y);
		if (flag == 0)
		{
			wprintf(L"%lc", ch1);
			flag = 1;
		}
		else
		{
			wprintf(L"%lc", ch2);
		}
		cur = cur->next;
	}
	SetPos(cur->x, cur->y);  找到尾节点的坐标
	printf("  ");  用空格覆盖尾节点之前打印的  ●
	free(cur);  释放尾节点
	prev->next = NULL;  让尾节点的前一个节点的指针域置空
}


 判断下一个节点是不是食物
bool IsFoodNode(Snake* snake,int x,int y)
{
	if (x == snake->pFood->x && y == snake->pFood->y)
		return true;
	else
		return false;
}
判断是否撞墙
bool IsWall(SnakeNode* newnode)
{
	因为墙的横坐标只能为0/墙的最大横坐标(COR)-2
	墙的纵坐标只能是0/ROW-1
	又因为蛇的横纵坐标不能为墙的横纵坐标
	所以  已经要连接在蛇头的  下一个节点的坐标为墙的横纵坐标,蛇就撞墙了
	if (newnode->x == 0 || newnode->x == COR - 2 || newnode->y == 0 || newnode->y == ROW - 1)
		return true;
	else
		return false;
}

判断是否咬到自己
bool IsSnakeNode(Snake* snake, SnakeNode* newnode)
{
	SnakeNode* cur = snake->pSnake;
	while (cur)  遍历蛇身链表
	{
		已经要连接在蛇头的  下一个节点的坐标与蛇身节点的坐标重合
		时就要到了自己
		if (cur->x == newnode->x && cur->y == newnode->y)
		{
			return true;
		}
		cur = cur->next;
	}
	return false;
}

根据按键情况让蛇走一步

走一步
void SnakeMove(Snake* snake)
{
	蛇走的方法是  制造一个新节点【新节点为蛇可能的下一个节点】,
	将新节点头插连接在蛇头上,成为新的头,并根据情况删除尾节点
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));  申请新节点
	if (newnode == NULL)
	{
		printf("NoFoodNode函数中malloc失败!");
		return;
	}
	switch (snake->Snakedir)  获取蛇的方向
	{
	case UP:
		newnode->x=snake->pSnake->x;   蛇的方向为上时下一个节点(新节点)的横坐标不变
		newnode->y=snake->pSnake->y - 1;  蛇的方向为上时下一个节点(新节点)的纵坐标-1
		break;
	case DOWN:
		newnode->x = snake->pSnake->x;
		newnode->y = snake->pSnake->y+1;
		break;
	case LEFT:
		newnode->x = snake->pSnake->x-2;
		newnode->y = snake->pSnake->y;
		break;
	case RIGHT:
		newnode->x = snake->pSnake->x+2;
		newnode->y = snake->pSnake->y;
		break;
	}
	if (IsFoodNode(snake, newnode->x, newnode->y))  如果下一个节点(新节点)为食物
	{
		EtaFoodNode(snake);  吃掉食物
		 随机生成食物
		RandomFood(snake);
		增加分数
		snake->Score += snake->foodWeight;
	}
	else
	{
		判断是否撞墙
		if (IsWall(newnode))
		{
			snake->status = KILL_BY_WALL;   将游戏状态置为  因撞墙死亡
			return;  结束函数
		}

		判断是否咬到自己
		if (IsSnakeNode(snake, newnode))
		{
			snake->status = KILL_BY_SELF;  将游戏状态置为  因咬自己死亡
			return;
		}
		NoFoodNode(snake, newnode);
	}
}

游戏善后

游戏善后
void GameEnd(Snake* snake)
{
	if (snake->status == KILL_BY_WALL)如果是因为撞墙死就进去
	{
		system("cls");清屏
		SetPos(45, 15);

		打印提示信息
		printf("你撞到墙了,你失败了");
		SetPos(45, 18);
		printf("你的总分为:%d",snake->Score);

		使用count来统计游玩的次数
		int count = 0;

		如果是第一次游玩,就还没有chart.txt文件,以  r  的方式打开就会失败
		FILE* pf = fopen("chart.txt", "r+");

		if (pf == NULL)文件打开失败 fopen函数返回NULL
		{
			如果是第一次游玩,就还没有chart.txt文件,就再次以w的方式打开并创建chart.txt文件
			pf= fopen("chart.txt", "w");
			if (pf == NULL)
			{
				printf("GameEnd函数中,chart.txt文件打开失败");
				exit(-1);
			}
			把count的值和分数存入chart.txt文件
			fprintf(pf, "%-5d %-10d\n",count+1, snake->Score);
			fclose(pf);  关闭文件
		}
		else
		{
			如果不是第一次游玩就取出之前游玩时存入的count的值
			
			rewind(pf);  让文件读写指针回到  文件最开头

			fscanf(pf, "%d", &count);  取出之前游玩时存入的count的值

			count++;  取出值后count再++,将这次的游玩也统计

			fseek(pf, 0, SEEK_END);  让文件读写指针定位到文件末尾

			fprintf(pf, "%-10d\n", snake->Score);  在文件最末尾记录这次的分数

			rewind(pf);  再让文件读写指针回到  文件最开头

			fprintf(pf, "%-5d", count);++后的count覆盖文件最开头的上一次的count的值
			fclose(pf);  关闭文件
		}
		SetPos(45, 20);
		system("pause");
	}
	if (snake->status == KILL_BY_SELF)  如果是因为咬到自己就进去
	{
		system("cls");
		SetPos(45, 15);
		printf("你咬到自己了,你失败了");
		SetPos(45, 18);
		printf("你的总分为:%d", snake->Score);
        一样的存储流程
		int count =  0;
		FILE* pf = fopen("chart.txt", "r+");
		if (pf == NULL)
		{
			pf = fopen("chart.txt", "w");
			if (pf == NULL)
			{
				printf("GameEnd函数中,chart.txt文件打开失败");
				exit(-1);
			}
			fprintf(pf, "%-5d\n%-10d\n", count + 1, snake->Score);
			fclose(pf);
		}
		else
		{
			rewind(pf);
			fscanf(pf, "%d", &count);
			//确定游玩次数
			count++;
			fseek(pf, 0, 2);
			fprintf(pf, "%-10d\n", snake->Score);
			rewind(pf);
			fprintf(pf, "%-5d", count);
			fclose(pf);
		}
		SetPos(45, 20);
		system("pause");
	}
	if (snake->status ==ESC)如果是按Esc退出
	{
		system("cls");
		SetPos(45, 15);
		printf("已退出游戏");
		SetPos(0, 35);
	}
	销毁蛇身链表
	DestSnake(snake);
	初始化蛇
	SnakeInit(snake);  为下一次游玩做准备
}
销毁蛇身链表
void DestSnake(Snake* snake)
{
	SnakeNode* cur = snake->pSnake->next;  cur指向蛇头节点的下一个节点
	SnakeNode* prev = snake->pSnake;  prev指向cur的前一个节点
	snake->pSnake = NULL;  蛇头置空
	while (cur)  遍历链表
	{
		free(prev);  释放prev
		prev = cur;  让prev向后走一步
		cur = cur->next;  让cur向后走一步
	}
	free(prev);  释放尾节点
}
初始化蛇
void SnakeInit(Snake* snake)
{
	snake->pSnake = NULL;
	snake->pFood = NULL;  游戏还未开始时没有食物
	snake->Score = 0;  初始分数为0
	snake->foodWeight = 10;  初始每个食物分数
	snake->SnakeSpeed = 200;  初始速度
	snake->status = OK;  游戏状态OK表示可以进行游戏
	snake->Snakedir = RIGHT;  蛇最开始的方向默认为右
}

打印排行榜

比较函数
int compare(const void* p, const void* q)
{
	return *(int*)q - *(int*)p;
}

打印排行榜
void ChartPrint(Snake* snake)
{
	美化一下界面
	SetPos(0, 0);
	printf("---------------------------------------------");
	SetPos(45, 0);
	printf("__排行榜__");
	SetPos(55, 0);
	printf("---------------------------------------------");

	打开游戏时存放分数的文件
	FILE* pf = fopen("chart.txt", "r");
	if (pf == NULL)
	{
		printf("ChartPrint函数中chart.txt文件打开失败!");
		exit(-1);
	}

	rewind(pf);  让文件读写指针来到  最开头,读取count的值

	int count = 0;
	fscanf(pf, "%d", &count);  读取count的值
	int i = 0;

	打印提示信息
	SetPos(34, 1);
	printf("|名次  |                |总分      |\n");

	申请空间存放分数
	int* tmp = (int*)malloc(sizeof(int) * count);
	for (i = 0; i < count; i++)分数一共count个
	{
		int score = 0;
		fscanf(pf, "%d", &score);  读取分数
		tmp[i] = score;  将读取的分数放入tmp中
	}

	定义供qsort比较的  比较函数
	int (*p)(const void*, const void*) = compare;

	使用qsort函  降序  排序分数
	qsort(tmp, count, sizeof(int), p);
	for (i = 0; i < count; i++)
	{
		SetPos(34, i + 2);
		printf("|第%2d名|                |%-10d|\n", i + 1, tmp[i]);//打印分数
	}
	SetPos(34, i + 4);  将控制台光标放到合适的位置
	fclose(pf);  关闭文件
}



全部代码

snake.h

#define _CRT_SECURE_NO_WARNINGS

#include<locale.h>
#include<stdbool.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<time.h>

//定义判断按键情况的宏
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 1) ? 1 : 0 ) 

//贪吃蛇墙体的行,列
#define ROW 30
#define COR 60

//初始蛇头的坐标
#define SNAKE_HEAD_X 34
#define SNAKE_HEAD_Y 15

enum GAME_STATUS//枚举游戏状态
{
	OK,
	ESC,
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//咬自己
};
enum SNAKE_DIRECTION//枚举蛇的方向
{
	UP,
	DOWN,
	LEFT,
	RIGHT
};
enum OPTION//枚举开始选项状态
{
	BEGIN,//开始
	CHART//排行榜
};

//蛇身节点的结构体
typedef struct SnakeNode
{
	int x;//存储控制台对应横坐标
	int y;//存储控制台对应纵坐标
	struct SnakeNode* next;//存储下一个节点的地址
}SnakeNode;

typedef struct Snake
{
	SnakeNode* pSnake;//维护构成蛇的身体的链表的指针
	SnakeNode* pFood;//指向食物的指针
	int Score;//当前分数
	int foodWeight;//当前一个食物的分数
	int SnakeSpeed;//蛇的休眠时间
	enum GAME_STATUS status;//游戏当前状态
	enum SNAKE_DIRECTION Snakedir;//蛇的方向
}Snake;


//游戏开始前的初始化
void GameStart(Snake*snake);
//初始化蛇
void SnakeInit(Snake*snake);
//设置光标位置
void SetPos(int x, int y);
//游戏进行
void GameRun(Snake* snake);
//打印欢迎界面
void Welcome();
//打印游戏时的界面
void PlayInte(Snake* snake);
//打印蛇身
void SnakePrint(Snake* snake);
//随机生成食物
void RandomFood(Snake* snake);
//判断下一个节点是不是食物
bool IsFoodNode(Snake* snake, int x, int y);
//下一步为食物
void EtaFoodNode(Snake* snake);
//下一步不是食物
void NoFoodNode(Snake* snake, SnakeNode* newnode);
//暂停函数
void pause();
//判断是否撞墙
bool IsWall(SnakeNode* newnode);
//判断是否咬到自己
bool IsSnakeNode(Snake* snake, SnakeNode* newnode);
//蛇走一步
void SnakeMove(Snake* snake);
//销毁蛇身链表
void DestSnake(Snake* snake);
//游戏善后
void GameEnd(Snake* snake);
//打印排行榜
void ChartPrint(Snake* snake);

snake.c

#define _CRT_SECURE_NO_WARNINGS

#include"Snake.h"

void SetPos(int x, int y)
{
	COORD pos = { x,y };
	//获得控制台句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置光标位置
	SetConsoleCursorPosition(hOutput, pos);
}
//打印欢迎界面
void Welcome()
{
	//设置控制台光标到  合适  的位置
	SetPos(40,15);
	printf("欢迎游玩贪吃蛇");
	SetPos(40, 17);//设置控制台光标到  合适  的位置,方便打印 按任意键继续...

	system("pause");//暂停程序,达成  按任意键继续...  的功能
	system("cls");//清除控制台之前打印的信息
	SetPos(36, 15);
	printf("按↑,↓,←,→即可操纵蛇的方向");
	SetPos(40, 16);
	printf("按F3加速,按F4减速");
	SetPos(24, 17);
	printf("加速时吃掉食物可以获得更高分数,减速时吃掉食物获得分数减少");
	SetPos(26, 18);
	printf("按Esc退出游戏,按空格可暂停游戏,再次按空格时可恢复");
	SetPos(40, 20);
	system("pause");
	system("cls");
}

//打印游戏时的界面
void PlayInte(Snake* snake)
{
	//打印墙
	//上墙体
	int i = 0;
	wchar_t ch = L'■';//宽字符定义并赋值

	for (i = 0; i <COR; i += 2)//因为一个宽字符占两个字节并且一个宽字符占两个单位的横坐标
	{                          //所以i+=2
		SetPos(i,0);
		wprintf(L"%lc", ch);//打印宽字符
	}

	//下墙体
	for (i = 0; i < COR; i += 2)//因为一个宽字符占两个字节并且一个宽字符占两个单位的横坐标
                               //所以i+=2
	{
		SetPos(i,ROW-1);
		wprintf(L"%lc", ch);
	}
	
	//左侧墙体
	for (i = 1; i < ROW; i++)//虽然一个宽字符占  2  个单位的横坐标
		                     //但是一个宽字符只占  1  个单位的纵坐标
	{
		SetPos(0,i);
		wprintf(L"%lc", ch);
	}

	//右侧墙体
	for (i = 1; i < ROW; i++)//虽然一个宽字符占  2  个单位的横坐标
		                     //但是一个宽字符只占  1  个单位的纵坐标
    {
		SetPos(COR-2, i);
		wprintf(L"%lc", ch);
	}

	//打印提示信息
	SetPos(65, 16);
	printf("F3加速");
	SetPos(65, 17);
	printf("F4减速");
	SetPos(65, 18);
	printf("Esc退出游戏");
	SetPos(65, 19);
	printf("空格暂停游戏");
	SetPos(65, 20);
	printf("使用↑,↓,→,←控制蛇的方向");
}

//打印蛇身
void SnakePrint(Snake* snake)
{
	SnakeNode* cur = snake->pSnake;
	wchar_t ch1 = L'□';//为了区分蛇头用  蛇头用 □
	wchar_t ch2 = L'●';//蛇身用 ●
	int flag = 0;//用于标识是否为第一次打印

	while (cur)//遍历链表
	{
		SetPos(cur->x, cur->y);//设置控制台光标位置
		if (flag == 0)//为第一次打印就  打印蛇头 □
		{
			wprintf(L"%lc", ch1);
			flag = 1;
		}
		else
		{
			wprintf(L"%lc", ch2);//不为第一次打印就  打印蛇身 ●
		}
		cur = cur->next;
	}
}

//随机生成食物
void RandomFood(Snake* snake)
{
	//蛇身节点与食物的类型一致,吃掉食物的时候就   只需要头插就行
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));
	if (newnode == NULL)
	{
		printf("RandomFood函数中malloc失败!");
		return;
	}
	SnakeNode* cur = snake->pSnake;//获得snake中存储的食物信息
	//控制随机食物的坐标生成的范围
	newnode->x = (rand() % ((COR - 3 - 2)/2) + 1)*2;//控制食物的横坐标与蛇一样  是偶数  方便蛇吃
	newnode->y = rand() % (ROW - 1 - 1) + 1;

	//食物不能生成在蛇的身体中
	while (cur)//遍历链表
	{
		if (cur->x == newnode->x && cur->y == newnode->y)
		{   //如果在蛇身中就再随机生成一次坐标
			newnode->x = (rand() % ((COR - 3 - 2) / 2) + 1) * 2;
			newnode->y = rand() % (ROW - 1 - 1) + 1;
			//让cur回到链表开头,重新遍历,防止再次随机生成的食物又再蛇身里
			cur = snake->pSnake;
		}
		else
		{
			cur = cur->next;
		}
	}
	snake->pFood = newnode;//更改snake中存储的食物信息
	SetPos(newnode->x, newnode->y);
	wprintf(L"卍");//打印食物
}

//吃掉食物
void EtaFoodNode(Snake* snake)
{
	//让snake中存储的食物节点的头插在蛇头上
		snake->pFood->next = snake->pSnake;
	snake->pSnake = snake->pFood; //让原食物节点成为蛇头节点
		//打印蛇
		SnakePrint(snake);
}
//下一步不是食物
void NoFoodNode(Snake* snake, SnakeNode* newnode)
{
	//让新节点头插在蛇头上
		newnode->next = snake->pSnake;
	snake->pSnake = newnode;
	SnakeNode* cur = snake->pSnake;
	SnakeNode* prev = snake->pSnake;  //prev指向cur的前一个节点
		wchar_t ch1 = L'□';
	wchar_t ch2 = L'●';
	int flag = 0;
	while (cur->next) // 找尾并打印
	{
		prev = cur;
		SetPos(cur->x, cur->y);
		if (flag == 0)
		{
			wprintf(L"%lc", ch1);
			flag = 1;
		}
		else
		{
			wprintf(L"%lc", ch2);
		}
		cur = cur->next;
	}
	SetPos(cur->x, cur->y);  //找到尾节点的坐标
		printf("  ");  //用空格覆盖尾节点之前打印的  ●
		free(cur);  //释放尾节点
		prev->next = NULL;  //让尾节点的前一个节点的指针域置空
}



//判断下一个节点是不是食物
bool IsFoodNode(Snake* snake,int x,int y)
{
	if (x == snake->pFood->x && y == snake->pFood->y)
		return true;
	else
		return false;
}
//判断是否撞墙
bool IsWall(SnakeNode* newnode)
{
	//因为墙的横坐标只能为0/墙的最大横坐标(COR)-2
	//墙的纵坐标只能是0/ROW-1
	//又因为蛇的横纵坐标不能为墙的横纵坐标
	//所以  已经要连接在蛇头的  下一个节点的坐标为墙的横纵坐标,蛇就撞墙了
	if (newnode->x == 0 || newnode->x == COR - 2 || newnode->y == 0 || newnode->y == ROW - 1)
		return true;
	else
		return false;
}

//判断是否咬到自己
bool IsSnakeNode(Snake* snake, SnakeNode* newnode)
{
	SnakeNode* cur = snake->pSnake;
	while (cur)//遍历蛇身链表
	{
		//已经要连接在蛇头的  下一个节点的坐标与蛇身节点的坐标重合
		//时就要到了自己
		if (cur->x == newnode->x && cur->y == newnode->y)
		{
			return true;
		}
		cur = cur->next;
	}
	return false;
}



//走一步
void SnakeMove(Snake* snake)
{
	//蛇走的方法是  制造一个新节点【新节点为蛇可能的下一个节点】,
	//将新节点头插连接在蛇头上,成为新的头,并根据情况删除尾节点
	SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));//申请新节点
	if (newnode == NULL)
	{
		printf("NoFoodNode函数中malloc失败!");
		return;
	}
	switch (snake->Snakedir)//获取蛇的方向
	{
	case UP:
		newnode->x=snake->pSnake->x; //蛇的方向为上时下一个节点(新节点)的横坐标不变
		newnode->y=snake->pSnake->y - 1;//蛇的方向为上时下一个节点(新节点)的纵坐标-1
		break;
	case DOWN:
		newnode->x = snake->pSnake->x;
		newnode->y = snake->pSnake->y+1;
		break;
	case LEFT:
		newnode->x = snake->pSnake->x-2;
		newnode->y = snake->pSnake->y;
		break;
	case RIGHT:
		newnode->x = snake->pSnake->x+2;
		newnode->y = snake->pSnake->y;
		break;
	}
	if (IsFoodNode(snake, newnode->x, newnode->y))//如果下一个节点(新节点)为食物
	{
		EtaFoodNode(snake);//吃掉食物
		//随机生成食物
		RandomFood(snake);
		//增加分数
		snake->Score += snake->foodWeight;
	}
	else
	{
		//判断是否撞墙
		if (IsWall(newnode))
		{
			snake->status = KILL_BY_WALL; //将游戏状态置为  因撞墙死亡
			return;//结束函数
		}

		//判断是否咬到自己
		if (IsSnakeNode(snake, newnode))
		{
			snake->status = KILL_BY_SELF;//将游戏状态置为  因咬自己死亡
			return;
		}
		NoFoodNode(snake, newnode);
	}
}

//销毁蛇身链表
void DestSnake(Snake* snake)
{
	SnakeNode* cur = snake->pSnake->next;//cur指向蛇头节点的下一个节点
	SnakeNode* prev = snake->pSnake;//prev指向cur的前一个节点
	snake->pSnake = NULL;//蛇头置空
	while (cur)//遍历链表
	{
		free(prev);//释放prev
		prev = cur;//让prev向后走一步
		cur = cur->next;//让cur向后走一步
	}
	free(prev);//释放尾节点
}

//游戏善后
void GameEnd(Snake* snake)
{
	if (snake->status == KILL_BY_WALL)//如果是因为撞墙死就进去
	{
		system("cls");//清屏
		SetPos(45, 15);

		//打印提示信息
		printf("你撞到墙了,你失败了");
		SetPos(45, 18);
		printf("你的总分为:%d",snake->Score);

		//使用count来统计游玩的次数
		int count = 0;

		//如果是第一次游玩,就还没有chart.txt文件,以  r  的方式打开就会失败
		FILE* pf = fopen("chart.txt", "r+");

		if (pf == NULL)//文件打开失败 fopen函数返回NULL
		{
			//如果是第一次游玩,就还没有chart.txt文件,就再次以w的方式打开并创建chart.txt文件
			pf= fopen("chart.txt", "w");
			if (pf == NULL)
			{
				printf("GameEnd函数中,chart.txt文件打开失败");
				exit(-1);
			}
			//把count的值和分数存入chart.txt文件
			fprintf(pf, "%-5d %-10d\n",count+1, snake->Score);
			fclose(pf);//关闭文件
		}
		else
		{
			//如果不是第一次游玩就取出之前游玩时存入的count的值
			
			rewind(pf);//让文件读写指针回到  文件最开头

			fscanf(pf, "%d", &count);//取出之前游玩时存入的count的值

			count++;//取出值后count再++,将这次的游玩也统计

			fseek(pf, 0, SEEK_END);//让文件读写指针定位到文件末尾

			fprintf(pf, "%-10d\n", snake->Score);//在文件最末尾记录这次的分数

			rewind(pf);//再让文件读写指针回到  文件最开头

			fprintf(pf, "%-5d", count);//把++后的count覆盖文件最开头的上一次的count的值
			fclose(pf);//关闭文件
		}
		SetPos(45, 20);
		system("pause");
	}
	if (snake->status == KILL_BY_SELF)//如果是因为咬到自己就进去
	{
		system("cls");
		SetPos(45, 15);
		printf("你咬到自己了,你失败了");
		SetPos(45, 18);
		printf("你的总分为:%d", snake->Score);

		int count = 0;
		FILE* pf = fopen("chart.txt", "r+");
		if (pf == NULL)
		{
			pf = fopen("chart.txt", "w");
			if (pf == NULL)
			{
				printf("GameEnd函数中,chart.txt文件打开失败");
				exit(-1);
			}
			fprintf(pf, "%-5d\n%-10d\n", count + 1, snake->Score);
			fclose(pf);
		}
		else
		{
			rewind(pf);
			fscanf(pf, "%d", &count);
			//确定游玩次数
			count++;
			fseek(pf, 0, 2);
			fprintf(pf, "%-10d\n", snake->Score);
			rewind(pf);
			fprintf(pf, "%-5d", count);
			fclose(pf);
		}
		SetPos(45, 20);
		system("pause");
	}
	if (snake->status ==ESC)//如果是按Esc退出
	{
		system("cls");
		SetPos(45, 15);
		printf("已退出游戏");
		SetPos(0, 35);
	}
	//销毁蛇身链表
	DestSnake(snake);
	//初始化蛇
	SnakeInit(snake);//为下一次游玩做准备
}

void pause()
{
	while (1)//死循环执行暂停
	{
		Sleep(200);//暂停程序200毫秒
		if (KEY_PRESS(VK_SPACE))//直到再次按下空格,就结束暂停
			break;
	}
}

//游戏进行
void GameRun(Snake* snake)
{
	//打印游戏开始后界面
	PlayInte(snake);
	int i = 0;

	//用头插法 构建   初始   蛇身节点
	for (i = 5; i >= 1; i--)
	{
		SnakeNode* newnode = (SnakeNode*)malloc(sizeof(SnakeNode));//申请新节点
		if (newnode == NULL)
		{
			printf("GameRun函数中malloc失败!");
			return;
		}
		newnode->x = SNAKE_HEAD_X - i * 2;//根据  初始蛇头坐标  控制链表尾节点的坐标
		newnode->y = SNAKE_HEAD_Y;        //因为是头插所以第一个插入的节点在插入完成后是最后一个节点
		if (snake->pSnake == NULL)
		{
			snake->pSnake = newnode;
			newnode->next = NULL;
		}
		else
		{
			newnode->next = snake->pSnake;
			snake->pSnake = newnode;
		}
	}

	//打印蛇身
	SnakePrint(snake);

	RandomFood(snake);
	//蛇走
	do
	{
		//打印会随游戏进行而变化的信息
		SetPos(65, 10);
		printf("当前分数:%-5d", snake->Score);
		SetPos(65, 11);
		printf("当前每一个食物的分数:%-2d", snake->foodWeight);
		SetPos(65, 13);
		printf("当前蛇的速度:%c(D为初始速度加速一次", (snake->SnakeSpeed - 200) / 40 + 'D');
		SetPos(65, 14);
		printf("为C,减速一次为E,最快为A)");
		if (KEY_PRESS(VK_UP)&&snake->Snakedir != DOWN)//按上并且 蛇在向下时不能直接向上走
		{
			snake->Snakedir = UP;//更改蛇的方向为上
		}
		else if (KEY_PRESS(VK_DOWN) && snake->Snakedir != UP)//下并且 蛇在向上时不能直接向下走
		{
			snake->Snakedir = DOWN;//更改蛇的方向为下
		}
		else if (KEY_PRESS(VK_LEFT) && snake->Snakedir != RIGHT)//左并且 蛇在向右时不能直接向左走
		{
			snake->Snakedir = LEFT;//更改蛇的方向为左
		}
		else if (KEY_PRESS(VK_RIGHT) && snake->Snakedir != LEFT)//右并且 蛇在向左时不能直接向右走
		{
			snake->Snakedir = RIGHT;//更改蛇的方向为右
		}
		else if (KEY_PRESS(VK_SPACE))//暂停
		{
			pause();
		}
		else if(KEY_PRESS(VK_ESCAPE))//退出
		{
			//提示信息打印
			SetPos(20, 15);
			printf("若退出游戏当前游戏进度无法保存!!!");
			SetPos(20, 16);
			printf("确定退出游戏吗?(Y/N)【输入Y/N+回车】");
			char ch = 0;
			do
			{
				ch = getchar();
				if (ch == 'Y' || ch == 'y')
				{
					snake->status = ESC;//退出游戏  更改游戏状态为退出
					break;
				}
				if (ch == 'N' || ch == 'n')
				{
					//继续游戏
					SetPos(20, 15);
					//打印空格覆盖提示信息
					printf("                                    ");
					SetPos(20, 16);
					printf("                      ");
				}
			} while (ch!='Y' &&ch!='y' && ch != 'N' && ch != 'n');//控制输入符号有效性
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (snake->SnakeSpeed > 80)//不能一直加速
			{
				snake->SnakeSpeed -= 40;//加速蛇  暂停  时间减少
				snake->foodWeight += 2;//加速 吃掉一个食物的分数增加
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (snake->SnakeSpeed < 320)//不能一直减速
			{
				snake->SnakeSpeed += 40;//减速蛇  暂停  时间增加
				snake->foodWeight -= 2;//减速 吃掉一个食物的分数减少
			}
		}
		//休眠
		Sleep(snake->SnakeSpeed);//按完键后,蛇(程序)暂停一下,给玩家反应时间
		//走一步
		SnakeMove(snake);//根据按键情况走一步
	} while (snake->status == OK);//游戏状态要OK循环才继续
}

//比较函数
int compare(const void* p, const void* q)
{
	return *(int*)q - *(int*)p;
}

//打印排行榜
void ChartPrint(Snake* snake)
{
	//美化一下界面
	SetPos(0, 0);
	printf("---------------------------------------------");
	SetPos(45, 0);
	printf("__排行榜__");
	SetPos(55, 0);
	printf("---------------------------------------------");

	//打开游戏时存放分数的文件
	FILE* pf = fopen("chart.txt", "r");
	if (pf == NULL)
	{
		printf("ChartPrint函数中chart.txt文件打开失败!");
		exit(-1);
	}

	rewind(pf);//让文件读写指针来到  最开头,读取count的值

	int count = 0;
	fscanf(pf, "%d", &count);//读取count的值
	int i = 0;

	//打印提示信息
	SetPos(34, 1);
	printf("|名次  |                |总分      |\n");

	//申请空间存放分数
	int* tmp = (int*)malloc(sizeof(int) * count);
	for (i = 0; i < count; i++)//分数一共count个
	{
		int score = 0;
		fscanf(pf, "%d", &score);//读取分数
		tmp[i] = score;//将读取的分数放入tmp中
	}

	//定义供qsort比较的  比较函数
	int (*p)(const void*, const void*) = compare;

	//使用qsort函  降序  排序分数
	qsort(tmp, count, sizeof(int), p);
	for (i = 0; i < count; i++)
	{
		SetPos(34, i + 2);
		printf("|第%2d名|                |%-10d|\n", i + 1, tmp[i]);//打印分数
	}
	SetPos(34, i + 4);//将控制台光标放到合适的位置
	fclose(pf);//关闭文件
}

//初始化蛇
void SnakeInit(Snake* snake)
{
	snake->pSnake = NULL;
	snake->pFood = NULL;//游戏还未开始时没有食物
	snake->Score = 0;//初始分数为0
	snake->foodWeight = 10;//初始每个食物分数
	snake->SnakeSpeed = 200;//初始速度
	snake->status = OK;//游戏状态OK表示可以进行游戏
	snake->Snakedir = RIGHT;//蛇最开始的方向默认为右
}
void GameStart(Snake* snake)
{
	//设置游戏窗口大小
	system("mode con cols=100 lines=40");
	system("title 贪吃蛇 ");

	//获得控制台句柄
	HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	//创建保存光标信息的结构变量
	CONSOLE_CURSOR_INFO CursorInfo;
	//获取控制台光标信息
	GetConsoleCursorInfo(hOutput, &CursorInfo);
	//隐藏光标
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(hOutput, &CursorInfo);

	//初始化蛇
	SnakeInit(snake);
	//打印游戏开始前的界面
	Welcome();
	
	enum OPTION option = BEGIN;//默认选项为开始游戏
	do
    {
		    //打印选项界面
		    SetPos(40, 15);
			printf("1.开始游戏");
			SetPos(40, 16);
			printf("2.排行榜");
			SetPos(36, 18);
			printf("请按↑,↓选择选项,按Enter键确认选项");
			SetPos(36, 19);
			printf("按Esc退出游戏。");
			if (KEY_PRESS(VK_UP))//如果按了↑就进去
			{
				SetPos(38, 16);//按了↑,→就指向第15行
				printf("  ");//所以把第16行的→用空格覆盖
				SetPos(38, 15);
				printf("→");//再在第15行打印→

				option = BEGIN;//更改枚举变量的值为  开始
			}
			else if (KEY_PRESS(VK_DOWN))//如果按了↑就进去
			{
				SetPos(38, 15);//按了↓,→就指向第16行
				printf("  ");//所以把第15行的→用空格覆盖
				SetPos(38, 16);
				printf("→");//再在第16行打印→

				option = CHART;//更改枚举变量的值为  排行榜
			}

			if (option ==BEGIN)
			{
				SetPos(38, 16);
				printf("  ");
				SetPos(38, 15);
				printf("→");//解决第一次时  →没有指向的选项
				if (KEY_PRESS(VK_RETURN) == 1)//如果选项状态为  开始 并且  按了Enter键就进去
				{
					system("cls");//清屏
					SetPos(35, 15);//设置控制台光标到  合适  的位置
					printf("开始游戏");//打印提示信息
					SetPos(35, 17);
					system("pause");//达成  按任意键继续。。。 的效果
					system("cls");
					GameRun(snake);//游戏进行
					GameEnd(snake);//游戏善后
					system("cls");
				}
			}
			if (option == CHART)
			{
				SetPos(38, 15);
				printf("  ");
				SetPos(38, 16);
				printf("→");//解决第一次时  →没有指向的选项
				if (KEY_PRESS(VK_RETURN) == 1)//如果选项状态为  排行榜 并且  按了Enter键就进去
				{
					system("cls");//清屏
					SetPos(35, 15);
					printf("排行榜");
					SetPos(35, 17);
					system("pause");//达成  按任意键继续。。。 的效果
					system("cls");
					//排行榜
					ChartPrint(snake);//打印排行榜
					system("pause");
					system("cls");
				}
			}
		} while (KEY_PRESS(VK_ESCAPE) != 1);//如果按Esc就结束循环
}



test.c

#define _CRT_SECURE_NO_WARNINGS


#include"Snake.h"

void game()
{
	Snake snake;
	GameStart(&snake);//游戏开始前的初始化
}

int main()
{
	//本土化
	setlocale(LC_ALL, "");
	//设置变化的随机数种子
	srand((unsigned int)time(NULL));
	game();
	return 0;
}

结语

如果觉得有意思可以给文章点个赞,支持一下!!!

  • 65
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
非常感谢您的提问,以下是用C语言在VS里贪吃蛇代码的示例: ```c #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <windows.h> #define WIDTH 30 #define HEIGHT 20 int x, y, fruitX, fruitY, score, gameover; int tailX[100], tailY[100], nTail; enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN }; enum eDirection dir; void Setup() { gameover = 0; dir = STOP; x = WIDTH / 2; y = HEIGHT / 2; fruitX = rand() % WIDTH; fruitY = rand() % HEIGHT; score = 0; } void Draw() { system("cls"); for (int i = 0; i < WIDTH + 2; i++) printf("#"); printf("\n"); for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (j == 0) printf("#"); if (i == y && j == x) printf("O"); else if (i == fruitY && j == fruitX) printf("F"); else { int print = 0; for (int k = 0; k < nTail; k++) { if (tailX[k] == j && tailY[k] == i) { printf("o"); print = 1; } } if (!print) printf(" "); } if (j == WIDTH - 1) printf("#"); } printf("\n"); } for (int i = 0; i < WIDTH + 2; i++) printf("#"); printf("\n"); printf("Score: %d\n", score); } void Input() { if (_kbhit()) { switch (_getch()) { case 'a': dir = LEFT; break; case 'd': dir = RIGHT; break; case 'w': dir = UP; break; case 's': dir = DOWN; break; case 'x': gameover = 1; break; } } } void Logic() { int prevX = tailX[0]; int prevY = tailY[0]; int prev2X, prev2Y; tailX[0] = x; tailY[0] = y; for (int i = 1; i < nTail; i++) { prev2X = tailX[i]; prev2Y = tailY[i]; tailX[i] = prevX; tailY[i] = prevY; prevX = prev2X; prevY = prev2Y; } switch (dir) { case LEFT: x--; break; case RIGHT: x++; break; case UP: y--; break; case DOWN: y++; break; default: break; } if (x >= WIDTH) x = 0; else if (x < 0) x = WIDTH - 1; if (y >= HEIGHT) y = 0; else if (y < 0) y = HEIGHT - 1; for (int i = 0; i < nTail; i++) if (tailX[i] == x && tailY[i] == y) gameover = 1; if (x == fruitX && y == fruitY) { score += 10; fruitX = rand() % WIDTH; fruitY = rand() % HEIGHT; nTail++; } } int main() { Setup(); while (!gameover) { Draw(); Input(); Logic(); Sleep(50); } return 0; } ``` 希望这个示例能够帮助您!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值