C语言实现贪吃蛇(详解)

1.游戏效果演示

在这里插入图片描述

2.游戏代码

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

// 全局变量,保存原始的文本属性
WORD originalAttributes;


#define WALL L'■'//墙体     
#define BODY L'□'//蛇身
#define FOOD L'●'//食物

#define POS_X 24
#define POS_Y 5

//定义一个宏用于检测此按键是否被按下
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)

enum DIRECTION//读取按键状态
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

enum GAME_STATUS//运行状态
{
	OK,
	END_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

//贪吃蛇蛇身节点
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

//贪食蛇结构(贪吃蛇完全体)
typedef struct Snake
{
	pSnakeNode _pSnake;//指向贪食蛇头结点的指针(蛇身)
	pSnakeNode _pFood;//指向食物节点的指针(可将食物就当作蛇身的一部分,吃了后蛇身就变长一节)
	int _Score;//总分数
	int _FoodWeight;//单个食物分数
	int _SleepTime;//每走一步停顿时间
	enum DIRECTION _Dir;//获取按键状态描述蛇的方向
	enum GAME_STATUS _Status;//游戏的运行状态:正常、按ESC退出、撞墙死、自杀
	int _HighScore;// 历史最高分
	const char* _FilePath;// 历史最高分记录文件路径
}Snake,*pSnake;

//游戏开始,完成游戏的初始化
void GameStart(pSnake ps);

//设置光标坐标
void SetPos(short x, short y);

//设置贪吃蛇颜色
void color(int c);

//恢复原始的文本颜色属性
void restoreOriginalColor();

//打印欢迎界面
void WelComeToGame();

//创建地图
void CreateMap();

//初始化贪食蛇结构
void InitSnake(pSnake ps);

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

//蛇的移动
void SnakeMove(pSnake ps);

//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext);

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

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext);

//蛇是否撞墙死
void KillByWall(pSnake ps);

//蛇是否自杀
void KillBySelf(pSnake ps);

//打印帮助信息
void PrintHelpInfo();

//按空格暂停以及恢复
void Pause();

//游戏结束后的善后处理
void GameEnd(pSnake ps);

//设置贪吃蛇颜色
void color(int c)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	// 保存原始的文本属性
	CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
	GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
	originalAttributes = consoleInfo.wAttributes;

	// 设置新的文本属性
	SetConsoleTextAttribute(hConsole, c);
}

//恢复原始的文本属性
void restoreOriginalColor()
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	// 恢复原始的文本属性
	SetConsoleTextAttribute(hConsole, originalAttributes);
}

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

//打印欢迎界面
void WelComeToGame()
{
	//设置颜色
	color(3);

	SetPos(40, 14);//定位光标
	printf("欢迎来到贪食蛇!");
	SetPos(40, 25);
	system("pause");//对程序暂停一下
	system("cls");//清空屏幕
	SetPos(20, 14);
	printf("使用 ↑.↓.←.→.分别控制蛇的移动, F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

//创建地图
void CreateMap()
{
	//设置墙体颜色
	color(FOREGROUND_RED | FOREGROUND_GREEN);
	//上
	SetPos(0, 0);
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

	//恢复原始的文本属性
	restoreOriginalColor();
}

//初始化贪食蛇
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{//初始蛇身给他五个节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		//头插法-----将每个蛇身连接起来
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

	//打印蛇身
	//蛇的颜色
	color(FOREGROUND_GREEN);
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//恢复原始的文本属性
	restoreOriginalColor();

	//初始化下贪吃蛇结构中的其他内容
	ps->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_FoodWeight = 10;
	ps->_SleepTime = 200;
	ps->_Dir = RIGHT;

	ps->_HighScore = 0;  // 初始化历史最高分
	// 检查历史最高分文件是否存在,如果不存在则创建
	FILE* file = fopen("highscore.txt", "r");
	if (file == NULL)//不存在highscore.txt文件
	{
		// 创建新文件
		file = fopen("highscore.txt", "w");
		if (file != NULL)
		{
			fprintf(file, "%d", 0);  // 初始时将历史最高分设置为0
			fclose(file);
		}
		else
		{
			perror("GameStart()::fopen()");
		}
	}
	else//存在highscore.txt文件
	{
		fclose(file);
	}
	ps->_FilePath = "highscore.txt";  // 设置历史最高分记录文件路径     
}

//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{//在墙内寻找生成食物的坐标
		x = rand() % 53 + 2;//随机数范围2-54
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//食物的生成坐标要为2的倍数,要不让蛇嵌进墙里咋办,还有蛇只吃到一半食物咋办

	//食物已经在墙内了,现在要看它是否与蛇身重叠
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{//遍历判断蛇身坐标是否与所寻找的食物坐标重叠
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物节点(可将食物理解成蛇身的一部分)
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::mallloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	ps->_pFood = pFood;

	//打印食物
	//食物颜色
	color(FOREGROUND_RED);
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	//恢复原始的文本属性
	restoreOriginalColor();
}

//游戏开始,完成游戏的初始化
void GameStart(pSnake ps)
{
	//调整控制台窗口
	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);//设置控制台光标状态

	//打印欢迎界面
	WelComeToGame();

	//创建地图
	CreateMap();

	//初始化贪食蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);

	// 读取历史最高分记录文件
	FILE* file = fopen(ps->_FilePath, "r");
	if (file != NULL)
	{
		fscanf(file, "%d", &ps->_HighScore);//从ps->_FilePath文件中读取最高分放到ps->_HighScore种
		fclose(file);
	}
}

//判断蛇头到达的坐标处是否是食物
int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{//是
		return 1;
	}
	else
	{//不是
		return 0;
	}
}

//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	//蛇的颜色
	color(FOREGROUND_GREEN);
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//恢复原始的文本属性
	restoreOriginalColor();

	free(ps->_pFood);//此处的食物节点已被pnext记录,并被打印了,所以用后要及时释放
	ps->_Score += ps->_FoodWeight;

	//创建一个新的食物
	CreateFood(ps);

}

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{

	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	//蛇的颜色
	color(FOREGROUND_GREEN);
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	//恢复原始的文本属性
	restoreOriginalColor();
	free(cur->next);
	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 ((ps->_pSnake->x == (cur->x)) && (ps->_pSnake->y == (cur->y)))
		{
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
		cur = cur->next;
	}
}

//蛇的移动
void SnakeMove(pSnake ps)
{
	//定义一个pNext用于记录蛇头将要到达的下一个位置节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	//根据按键状态来寻找pNext的坐标位置
	switch (ps->_Dir)
	{//上、下、左、右
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//蛇是否撞墙死
	KillByWall(ps);

	//蛇是否自杀
	KillBySelf(ps);
}

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能撞墙,不能咬自己");
	SetPos(64, 16);
	printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");
	SetPos(64, 17);
	printf("3.F3加速,F4减速");
	SetPos(64, 18);
	printf("4.ESC-退出,空格-暂停");

	SetPos(64, 20);
	printf("@摆烂的小z");
}

//按空格暂停
void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//游戏的正常运行
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//分数时时更新啦
		SetPos(64, 10);
		printf("得分:%5d", ps->_Score);
		SetPos(64, 11);
		printf("每个食物分数:%2d", ps->_FoodWeight);
		SetPos(64, 12);
		printf("历史最高分:%5d", ps->_HighScore);
		// 比较并更新历史最高分
		if (ps->_Score > ps->_HighScore)
		{
			ps->_HighScore = ps->_Score;

			// 保存历史最高分到文件
			FILE* file = fopen(ps->_FilePath, "w");
			if (file != NULL)
			{
				fprintf(file, "%d", ps->_HighScore);//将新的历史最高分ps->_HighScore写到ps->_FilePath文件中
				fclose(file);
			}
		}

		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_ESCAPE))//按ESC退出
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))//按空格暂停
		{
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime <= 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

//游戏结束后的善后处理
void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出了游戏");
		break;
	case KILL_BY_SELF:
		printf("您自杀了");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了");
		break;
	}

	//一局游戏结束,释放蛇身的节点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

	//释放食物节点
	free(ps->_pFood);
	ps->_pFood = NULL; 
}

void test()
{
	char ch = 0;
	do
	{
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(20, 18);
		printf("再来一局?(Y/N):");
		ch = getchar();  // 获取用户输入的字符
		//清空输入缓冲区,直到遇到换行符
		int c;
		while ((c = getchar()) != '\n' && c != EOF);
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}
int main()
{
	//设置程序适应本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));

	test();
	return 0;
}

3.Win32 API介绍

3.1Win32 API

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

3.2控制台程序

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

 mode con cols=100 lines=30

参考:mode命令

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

title 贪食蛇启动!!!

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

3.3 控制台屏幕上的坐标COORD

COORD

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

在这里插入图片描述
COORD类型的声明:

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

给坐标赋值:

 COORD pos = { 10, 15 };

3.4 GetStdHandle

GetStdHandle

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

 HANDLE GetStdHandle(DWORD nStdHandle);

例如:

HANDLE hOutput = NULL;

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

3.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);//获取控制台光标信息

3.5.1 CONSOLE_CURSOR_INFO

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

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

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

bVisible,游标的可见性。如果光标可见,则此成员为TRUE。例:

 CursorInfo.bVisible = false; //隐藏控制台光标

3.6 SetConsoleCursorInfo

SetConsoleCursorInfo

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

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

例如:

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
 
 //影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标 
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

3.7 SetConsoleCursorPosition

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);
}

3.8 GetAsyncKeyState

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)-Win32apps

3.9 SetConsoleTextAttribute

SetConsoleTextAttribute

SetConsoleTextAttribute 函数用于设置控制台文本的前景(文本)颜色和背景颜色。

BOOL WINAPI SetConsoleTextAttribute(
  _In_ HANDLE hConsoleOutput,
  _In_ WORD   wAttributes
);
//wAttributes: 一个 WORD 类型,表示文本属性,包括前景色、背景色等。

我们可以封装一个函数用于改变程序中的贪吃蛇颜色,例:

设置贪吃蛇颜色的函数:

//设置贪吃蛇颜色
void color(int c)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	// 保存原始的文本属性
	CONSOLE_SCREEN_BUFFER_INFO consoleInfo;//CONSOLE_SCREEN_BUFFER_INFO 是一个结构体,用于存储有关控制台屏幕缓冲区的信息。
	GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
	originalAttributes = consoleInfo.wAttributes;//originalAttributes为我们在前面定义的一个全局变量,用来保存原来的文本颜色

	// 设置新的文本属性
	SetConsoleTextAttribute(hConsole, c);
}

恢复原来的文本颜色的函数:

//恢复原始的文本属性
void restoreOriginalColor()
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	// 恢复原始的文本属性
	SetConsoleTextAttribute(hConsole, originalAttributes);
}

4.贪吃蛇游戏设计与分析

4.1 地图(宽字符的打印)

在这里插入图片描述
在上面游戏地图中,我们打印墙体使用宽字符:■,打印蛇使用宽字符□,打印食物使用宽字符●。

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

下面我们详细介绍一下宽字符相关知识及其的打印。

4.1.1 <locale.h>本地化

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

在标准中,依赖地区的部分有以下几项:
• 数字量的格式
• 货币量的格式
• 字符集
• 日期和时间的表示形式

4.1.2 类项

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

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

每个类项的详细说明

4.1.3 setlocale

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

4.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 ch4 = L'■';
    
    printf("%c%c\n", 'a', 'b');
    wprintf(L"%lc\n", ch1);
    wprintf(L"%lc\n", ch2);
    printf("%c\n", 'z');
    wprintf(L"%lc\n", ch4);
    return 0;
}

在这里插入图片描述

4.1.5 地图坐标

我选择的是27行,58列(当然也可以根据自己的情况修改)。
在这里插入图片描述

4.2 蛇身和食物

蛇身: 蛇的长度是5,连续5个节点。蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀半在墙外的现象,坐标不好对齐。

食物:坐标不能和蛇的身体重合

4.3 数据结构设计

我们用链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标即可,蛇节点结构如下:

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

封装⼀个Snake的结构来维护整条贪吃蛇:

typedef struct Snake
{
	pSnakeNode _pSnake;//指向贪食蛇头结点的指针(蛇身)
	pSnakeNode _pFood;//指向食物节点的指针(可将食物就当作蛇身的一部分,吃了后蛇身就变长一节)
	int _Score;//总分数
	int _FoodWeight;//单个食物分数
	int _SleepTime;//每走一步停顿时间
	enum DIRECTION _Dir;//获取按键状态描述蛇的方向
	enum GAME_STATUS _Status;//游戏的运行状态:正常、按ESC退出、撞墙死、自杀
	int _HighScore;// 历史最高分
	const char* _FilePath;// 历史最高分记录文件路径
}Snake,*pSnake;

用枚举记录蛇的方向

enum DIRECTION//读取按键状态
{//上、下、左、右
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

记录游戏状态

enum GAME_STATUS//运行状态
{
	OK,//正常运行
	END_NORMAL,//按ESC结束
	KILL_BY_WALL,//撞墙死
	KILL_BY_SELF//自杀
};

5. 核心逻辑实现分析

5.1 游戏主逻辑

主逻辑分为3个过程:

• 游戏开始(GameStart)完成游戏的初始化
• 游戏运行(GameRun)完成游戏运行逻辑的实现
• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

5.2 游戏开始(GameStart)

执行任务:

• 控制台窗口大小的设置
• 控制台窗口名字的设置
• 鼠标光标的隐藏
• 打印欢迎界面
• 创建地图
• 初始化第蛇
• 创建食物
• 读取历史最高分

总逻辑:

//游戏开始,完成游戏的初始化
void GameStart(pSnake ps)
{
	//调整控制台窗口
	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);//设置控制台光标状态

	//打印欢迎界面
	WelComeToGame();

	//创建地图
	CreateMap();

	//初始化贪食蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
	
	// 读取历史最高分记录文件
	FILE* file = fopen(ps->_FilePath, "r");
	if (file != NULL)
	{
		fscanf(file, "%d", &ps->_HighScore);//从ps->_FilePath文件中读取最高分放到ps->_HighScore种
		fclose(file);
	}
}

5.2.1 打印欢迎界面

//打印欢迎界面
void WelComeToGame()
{
	//设置颜色
	color(3);
	
	SetPos(40, 14);//定位光标
	printf("欢迎来到贪食蛇!");
	SetPos(40, 25);
	system("pause");//对程序暂停一下
	system("cls");//清空屏幕
	SetPos(20, 14);
	printf("使用 ↑.↓.←.→.分别控制蛇的移动, F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

5.2.2 创建地图

//创建地图
void CreateMap()
{
	//FOREGROUND_RED、FOREGROUND_GREEN 是 Windows API 中定义的一些常量,可以通过按位或操作(|)进行组合使用,以设置多个属性。
	//设置墙体颜色
	color(FOREGROUND_RED | FOREGROUND_GREEN);
	//上
	SetPos(0, 0);
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

	//恢复原始的文本属性
	restoreOriginalColor();
}

5.2.3 初始化蛇身

//初始化贪食蛇
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{//初始蛇身给他五个节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		//头插法-----将每个蛇身连接起来
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

	//打印蛇身
	//蛇的颜色
	color(FOREGROUND_GREEN);
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//恢复原始的文本属性
	restoreOriginalColor();

	//初始化下贪吃蛇结构中的其他内容
	ps->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_FoodWeight = 10;
	ps->_SleepTime = 200;
	ps->_Dir = RIGHT;

	ps->_HighScore = 0;  // 初始化历史最高分
	// 检查历史最高分文件是否存在,如果不存在则创建
	FILE* file = fopen("highscore.txt", "r");
	if (file == NULL)//不存在highscore.txt文件
	{
		// 创建新文件
		file = fopen("highscore.txt", "w");
		if (file != NULL)
		{
			fprintf(file, "%d", 0);  // 初始时将历史最高分设置为0
			fclose(file);
		}
		else
		{
			perror("GameStart()::fopen()");
		}
	}
	else//存在highscore.txt文件
	{
		fclose(file);
	}
	ps->_FilePath = "highscore.txt";  // 设置历史最高分记录文件路径     
}

5.2.4 创建食物

//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{//在墙内寻找生成食物的坐标
		x = rand() % 53 + 2;//随机数范围2-54
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//食物的生成坐标要为2的倍数,要不让蛇嵌进墙里咋办,还有蛇只吃到一半食物咋办

	//食物已经在墙内了,现在要看它是否与蛇身重叠
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{//遍历判断蛇身坐标是否与所寻找的食物坐标重叠
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物节点(可将食物理解成蛇身的一部分)
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::mallloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	ps->_pFood = pFood;

	//打印食物
	//食物颜色
	color(FOREGROUND_RED);
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	//恢复原始的文本属性
	restoreOriginalColor();
}

5.3 游戏运行(GameRun)

执行任务:

• 打印帮助信息
• 获取键值
• 蛇的移动:

  1. 吃掉食物

  2. 不吃食物

  3. 蛇是否撞墙死

  4. 蛇是否自杀

总逻辑:

//游戏的正常运行
void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();
	do
	{
		//分数时时更新啦
		SetPos(64, 10);
		printf("得分:%5d", ps->_Score);
		SetPos(64, 11);
		printf("每个食物分数:%2d", ps->_FoodWeight);
		SetPos(64, 12);
		printf("历史最高分:%5d", ps->_HighScore);

		// 比较并更新历史最高分
		if (ps->_Score > ps->_HighScore)
		{
			ps->_HighScore = ps->_Score;

			// 保存历史最高分到文件
			FILE* file = fopen(ps->_FilePath, "w");
			if (file != NULL)
			{
				fprintf(file, "%d", ps->_HighScore);//将新的历史最高分ps->_HighScore写到ps->_FilePath文件中
				fclose(file);
			}
		}

		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_ESCAPE))//按ESC退出
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))//按空格暂停
		{
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//加速
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//减速
		{
			if (ps->_SleepTime <= 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

5.3.1打印帮助信息

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能撞墙,不能咬自己");
	SetPos(64, 16);
	printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");
	SetPos(64, 17);
	printf("3.F3加速,F4减速");
	SetPos(64, 18);
	printf("4.ESC-退出,空格-暂停");

	SetPos(64, 20);
	printf("@摆烂的小z");
}

5.3.2蛇的移动

封装⼀个宏KEY_PRESS,检测按键状态

//定义一个宏用于检测此按键是否被按下
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
//吃掉食物
void EatFood(pSnake ps, pSnakeNode pnext)
{
	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	//蛇的颜色
	color(FOREGROUND_GREEN);
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	//恢复原始的文本属性
	restoreOriginalColor();

	free(ps->_pFood);
	ps->_Score += ps->_FoodWeight;

	//创建一个新的食物
	CreateFood(ps);
}

//不吃食物
void NoFood(pSnake ps, pSnakeNode pnext)
{

	//头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	//打印蛇身
	//蛇的颜色
	color(FOREGROUND_GREEN);
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	//恢复原始的文本属性
	restoreOriginalColor();
	free(cur->next);
	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 ((ps->_pSnake->x == (cur->x)) && (ps->_pSnake->y == (cur->y)))
		{
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
		cur = cur->next;
	}
}

//蛇的移动
void SnakeMove(pSnake ps)
{
	//定义一个pNext用于记录蛇头将要到达的下一个位置节点
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	//根据按键状态来寻找pNext的坐标位置
	switch (ps->_Dir)
	{//上、下、左、右
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	}

	//判断蛇头到达的坐标处是否是食物
	if (NextIsFood(ps, pNext))
	{
		//吃掉食物
		EatFood(ps, pNext);
	}
	else
	{
		//不吃食物
		NoFood(ps, pNext);
	}

	//蛇是否撞墙死
	KillByWall(ps);

	//蛇是否自杀
	KillBySelf(ps);
}

5.4游戏结束后的善后处理

//游戏结束后的善后处理
void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出了游戏");
		break;
	case KILL_BY_SELF:
		printf("您自杀了");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了");
		break;
	}

	//一局游戏结束,释放蛇身的节点
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

	//释放食物节点
	free(ps->_pFood);
	ps->_pFood = NULL; 
}

补充:在dev c++里正常运行的方法

2024.2.26

上面的代码是在VS2022里面写的,在dev c++里运行可能有些问题,因为Dev-C++默认不支持Unicode字符,所以将源文件保存为UTF-8编码就可以了。下面演示一下方法。

  1. 随便在一个文件夹下新建一个文本文件
    在这里插入图片描述
  2. 打开文本文件将贪吃蛇代码拷贝到文本文件里面(下面我会提供一个无注释版本----因为转码后汉字都会乱码)
    在这里插入图片描述
  3. 点击文件->另存为,选择编码UTF-8保存
    在这里插入图片描述
    在这里插入图片描述
  4. 再用dev c++的打开方式打开此文本文件
    在这里插入图片描述
  5. 进入dev c++后,点击文件->另存为
    在这里插入图片描述
  6. 把原来.txt后缀替换为.c
    在这里插入图片描述
  7. 经过以上步骤后我们发现就能运行了,但里面的汉字都变成了乱码,这时就只需要把本文章末尾给的正常代码的汉字相关部分代码拷贝到dev c++里乱码部分即可。(119-130,267-268,416-426,446-451,515-523,549 ----- 这些是所需要替换的乱码的具体行数)
    在这里插入图片描述
    在这里插入图片描述
    所有乱码部分都替换完后就可以开始游戏啦!!!
    在这里插入图片描述

无注释版贪吃蛇代码:

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

WORD originalAttributes;


#define WALL L'■'    
#define BODY L'□'
#define FOOD L'●'

#define POS_X 24
#define POS_Y 5

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

enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

enum GAME_STATUS
{
	OK,
	END_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

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

typedef struct Snake
{
	pSnakeNode _pSnake;
	pSnakeNode _pFood;
	int _Score;
	int _FoodWeight;
	int _SleepTime;
	enum DIRECTION _Dir;
	enum GAME_STATUS _Status;
	int _HighScore;
	const char* _FilePath;
}Snake, * pSnake;

void GameStart(pSnake ps);

void SetPos(short x, short y);

void color(int c);

void restoreOriginalColor();

void WelComeToGame();

void CreateMap();

void InitSnake(pSnake ps);

void GameRun(pSnake ps);

void SnakeMove(pSnake ps);

int NextIsFood(pSnake ps, pSnakeNode pnext);

void EatFood(pSnake ps, pSnakeNode pnext);

void NoFood(pSnake ps, pSnakeNode pnext);

void KillByWall(pSnake ps);

void KillBySelf(pSnake ps);

void PrintHelpInfo();

void Pause();


void GameEnd(pSnake ps);

void color(int c)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	CONSOLE_SCREEN_BUFFER_INFO consoleInfo;
	GetConsoleScreenBufferInfo(hConsole, &consoleInfo);
	originalAttributes = consoleInfo.wAttributes;

	SetConsoleTextAttribute(hConsole, c);
}

void restoreOriginalColor()
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);

	SetConsoleTextAttribute(hConsole, originalAttributes);
}

void SetPos(short x, short y)
{
	COORD pos = { x, y };
	HANDLE hOutput = NULL;
	hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(hOutput, pos);
}

void WelComeToGame()
{
	color(3);

	SetPos(40, 14);
	printf("欢迎来到贪食蛇!");
	SetPos(40, 25);
	system("pause");
	system("cls");
	SetPos(20, 14);
	printf("使用 ↑.↓.←.→.分别控制蛇的移动, F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");
}

void CreateMap()
{
	color(FOREGROUND_RED | FOREGROUND_GREEN);
	SetPos(0, 0);
	int i = 0;
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	SetPos(0, 26);
	for (i = 0; i <= 56; i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (i = 1; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}

	restoreOriginalColor();
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->x = POS_X + 2 * i;
		cur->y = POS_Y;
		cur->next = NULL;

		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}

	color(FOREGROUND_GREEN);
	cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	restoreOriginalColor();

	ps->_Status = OK;
	ps->_Score = 0;
	ps->_pFood = NULL;
	ps->_FoodWeight = 10;
	ps->_SleepTime = 200;
	ps->_Dir = RIGHT;

	ps->_HighScore = 0;  
	FILE* file = fopen("highscore.txt", "r");
	if (file == NULL)
	{
		file = fopen("highscore.txt", "w");
		if (file != NULL)
		{
			fprintf(file, "%d", 0);  
			fclose(file);
		}
		else
		{
			perror("GameStart()::fopen()");
		}
	}
	else
	{
		fclose(file);
	}
	ps->_FilePath = "highscore.txt"; 
}

void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::mallloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;
	ps->_pFood = pFood;

	color(FOREGROUND_RED);
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	restoreOriginalColor();
}

void GameStart(pSnake ps)
{
	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);

	WelComeToGame();

	CreateMap();

	InitSnake(ps);

	CreateFood(ps);

	FILE* file = fopen(ps->_FilePath, "r");
	if (file != NULL)
	{
		fscanf(file, "%d", &ps->_HighScore);
		fclose(file);
	}
}

int NextIsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pFood->x == pnext->x && ps->_pFood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}


void EatFood(pSnake ps, pSnakeNode pnext)
{
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	color(FOREGROUND_GREEN);
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	restoreOriginalColor();

	free(ps->_pFood);
	ps->_Score += ps->_FoodWeight;

	CreateFood(ps);

}

void NoFood(pSnake ps, pSnakeNode pnext)
{

	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	color(FOREGROUND_GREEN);
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	restoreOriginalColor();
	free(cur->next);
	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 ((ps->_pSnake->x == (cur->x)) && (ps->_pSnake->y == (cur->y)))
		{
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
		cur = cur->next;
	}
}

void SnakeMove(pSnake ps)
{
	pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pNext == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	pNext->next = NULL;

	switch (ps->_Dir)
	{
	case UP:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pNext->x = ps->_pSnake->x;
		pNext->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pNext->x = ps->_pSnake->x - 2;
		pNext->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pNext->x = ps->_pSnake->x + 2;
		pNext->y = ps->_pSnake->y;
		break;
	}

	if (NextIsFood(ps, pNext))
	{
		EatFood(ps, pNext);
	}
	else
	{
		NoFood(ps, pNext);
	}

	KillByWall(ps);

	KillBySelf(ps);
}

void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("1.不能撞墙,不能咬自己");
	SetPos(64, 16);
	printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");
	SetPos(64, 17);
	printf("3.F3加速,F4减速");
	SetPos(64, 18);
	printf("4.ESC-退出,空格-暂停");

	SetPos(64, 20);
	printf("@摆烂的小z");
}

void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("得分:%5d", ps->_Score);
		SetPos(64, 11);
		printf("每个食物分数:%2d", ps->_FoodWeight);
		SetPos(64, 12);
		printf("历史最高分:%5d", ps->_HighScore);
		if (ps->_Score > ps->_HighScore)
		{
			ps->_HighScore = ps->_Score;

			FILE* file = fopen(ps->_FilePath, "w");
			if (file != NULL)
			{
				fprintf(file, "%d", ps->_HighScore);
				fclose(file);
			}
		}

		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_ESCAPE))
		{
			ps->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime <= 320)
			{
				ps->_SleepTime += 30;
				ps->_FoodWeight -= 2;
			}
		}
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);
}

void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NORMAL:
		printf("您主动退出了游戏");
		break;
	case KILL_BY_SELF:
		printf("您自杀了");
		break;
	case KILL_BY_WALL:
		printf("您撞墙了");
		break;
	}

	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	ps->_pSnake = NULL;

	free(ps->_pFood);
	ps->_pFood = NULL;
}

void test()
{
	char ch = 0;
	do
	{
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(20, 18);
		printf("再来一局?(Y/N):");
		ch = getchar(); 
		int c;
		while ((c = getchar()) != '\n' && c != EOF);
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}
int main()
{
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));

	test();
	return 0;
}
  • 21
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
好的,以下是一个简单的C语言实现贪吃蛇的代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <time.h> #include <conio.h> #include <windows.h> #define WIDTH 40 // 地图宽度 #define HEIGHT 20 // 地图高度 #define SNAKE_MAX_LENGTH 100 // 蛇的最大长度 int map[HEIGHT][WIDTH]; // 地图数组,0表示空格,1表示蛇身,2表示食物 int snake[SNAKE_MAX_LENGTH][2]; // 蛇身坐标数组,存储每个蛇节的横纵坐标 int snake_length = 3; // 蛇的长度 int food_x, food_y; // 食物的横纵坐标 int direction = 0; // 蛇的移动方向,0表示上,1表示右,2表示下,3表示左 // 初始化地图 void init_map() { for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { map[i][j] = 0; } } } // 初始化蛇 void init_snake() { snake[0][0] = HEIGHT / 2; snake[0][1] = WIDTH / 2; map[snake[0][0]][snake[0][1]] = 1; snake[1][0] = HEIGHT / 2 + 1; snake[1][1] = WIDTH / 2; map[snake[1][0]][snake[1][1]] = 1; snake[2][0] = HEIGHT / 2 + 2; snake[2][1] = WIDTH / 2; map[snake[2][0]][snake[2][1]] = 1; } // 生成食物 void generate_food() { int x, y; do { x = rand() % HEIGHT; y = rand() % WIDTH; } while (map[x][y] != 0); food_x = x; food_y = y; map[food_x][food_y] = 2; } // 显示地图 void show_map() { system("cls"); // 清屏 for (int i = 0; i < HEIGHT; i++) { for (int j = 0; j < WIDTH; j++) { if (i == food_x && j == food_y) { // 食物 printf("O"); } else if (map[i][j] == 1) { // 蛇身 printf("#"); } else { // 空格 printf(" "); } } printf("\n"); } } // 检测是否吃到食物 void check_eat_food() { if (snake[0][0] == food_x && snake[0][1] == food_y) { snake_length++; generate_food(); } } // 检测是否撞到墙或自己 void check_dead() { if (snake[0][0] < 0 || snake[0][0] >= HEIGHT || snake[0][1] < 0 || snake[0][1] >= WIDTH) { exit(0); } for (int i = 1; i < snake_length; i++) { if (snake[i][0] == snake[0][0] && snake[i][1] == snake[0][1]) { exit(0); } } } // 移动蛇 void move_snake() { // 计算蛇头新的位置 int new_head_x = snake[0][0], new_head_y = snake[0][1]; switch (direction) { case 0: new_head_x--; break; // 上 case 1: new_head_y++; break; // 右 case 2: new_head_x++; break; // 下 case 3: new_head_y--; break; // 左 } // 更新蛇身坐标数组和地图数组 for (int i = snake_length - 1; i > 0; i--) { snake[i][0] = snake[i - 1][0]; snake[i][1] = snake[i - 1][1]; map[snake[i][0]][snake[i][1]] = 1; } snake[0][0] = new_head_x; snake[0][1] = new_head_y; map[snake[0][0]][snake[0][1]] = 1; } // 键盘输入事件 void keyboard_event() { if (_kbhit()) { // 判断是否有键盘输入 char ch = _getch(); switch (ch) { case 'w': direction = 0; break; // 上 case 'd': direction = 1; break; // 右 case 's': direction = 2; break; // 下 case 'a': direction = 3; break; // 左 } } } int main() { // 初始化 srand(time(NULL)); // 设置随机数种子 init_map(); init_snake(); generate_food(); // 游戏循环 while (1) { show_map(); check_eat_food(); check_dead(); move_snake(); keyboard_event(); Sleep(100); // 等待一段时间,控制蛇的移动速度 } return 0; } ``` 该示例代码使用了Windows系统的控制台API,如果在其他操作系统下运行需要进行适当修改。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值