使用C语言实现简单的贪吃蛇(学不会来砍我兄弟)

纲要

基本功能

1,贪吃蛇地图绘制
2,蛇吃食物的功能
3,蛇撞墙死亡
4,蛇撞自身死亡
5,计算吃食物得分
6,蛇加速,减速移动
7,暂停游戏

技术要点

  • C语言基础语法
  • 链表
  • 枚举
  • 动态内存管理
  • 预处理指令
  • Win32 API部分指令

Win32 API

介绍

Win32 API(Windows API)是微软公司为其Windows操作系统开发的一组应用程序接口。它提供了一系列函数和数据结构,允许开发人员创建Windows应用程序并与操作系统进行交互。Win32 API包含了许多不同的功能,包括窗口管理、图形设备接口、文件和输入输出等。开发人员可以使用Win32 API编写原生的Windows应用程序,控制应用程序的行为以及与用户交互的方式。

控制台程序

1, 平时程序运行时打开的黑色框框就是控制台程序,我们可以通过cmd命令来控制控制台的大小。

mode con cols=100 lines=30
将控制台设置为列100行30的大小

2,更改窗口名字(结束后会变回原来的名字)

	title 贪吃蛇

如下:
在这里插入图片描述
3,控制台程序可以在C语言中使用system(“”)函数来运行,例如:

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

COORD类型声明

1,COORD是位于windows.h头文件中的结构体类。用于定位控制台坐标。(从0坐标开始)

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

2,控制台坐标如下:
在这里插入图片描述

API函数

GetStdHandle函数

GetStdHandle 是 Windows API 中的一个函数,用于获取标准输入、输出和错误流的句柄(handle)。它的声明如下:
HANDLE WINAPI GetStdHandle(
  _In_ DWORD nStdHandle
);

GetStdHandle 接受一个参数 nStdHandle,指定要获取的标准设备流的类型。这个参数可以是以下值之一

  • STD_INPUT_HANDLE (DWORD 值为 -10):获取标准输入流的句柄。
  • STD_OUTPUT_HANDLE (DWORD 值为 -11):获取标准输出流的句柄。
  • STD_ERROR_HANDLE (DWORD 值为 -12):获取标准错误流的句柄。

函数返回的是一个句柄(HANDLE 类型),表示对应标准流的句柄。通过这个句柄,程序可以直接操作标准输入、输出和错误流,例如读取输入、写入输出或者错误信息。

在Windows编程中,GetStdHandle 常常与其他输入输出函数(如 ReadFile、WriteFile 等)一起使用,用于对标准输入输出进行操作。

GetConsoleCursorInfo函数

GetConsoleCursorInfo 是 Windows API 中的一个函数,用于获取控制台光标的信息。它的声明如下:
BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE              hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

该函数接受两个参数

  • hConsoleOutput:控制台输出的句柄,通常可以使用 GetStdHandle(STD_OUTPUT_HANDLE) 来获取。
  • lpConsoleCursorInfo:一个指向 CONSOLE_CURSOR_INFO 结构体的指针,用于接收光标信息。

CONSOLE_CURSOR_INFO 结构体定义如下

typedef struct _CONSOLE_CURSOR_INFO {
  DWORD dwSize;    // 光标大小
  BOOL  bVisible;  // 光标是否可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

成功调用 GetConsoleCursorInfo 函数后,lpConsoleCursorInfo 将包含有关控制台光标的信息,包括光标大小和是否可见。

该函数返回一个 BOOL 类型的值,表示调用是否成功。若成功,则返回 TRUE,否则返回 FALSE。可以通过调用GetLastError 函数获取更多关于失败原因的信息。

通常情况下,程序员可以通过调用 GetConsoleCursorInfo 函数来获取控制台光标的信息,以便根据需要对光标进行修改或操作。

SetConsoleCursorInfo函数

SetConsoleCursorInfo 是 Windows API 中用于设置控制台光标信息的函数。其声明如下:
BOOL WINAPI SetConsoleCursorInfo(
  _In_ HANDLE              hConsoleOutput,
  _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

该函数接受两个参数

  • hConsoleOutput:控制台输出的句柄,通常可以使用
  • GetStdHandle(STD_OUTPUT_HANDLE) 来获取。
  • lpConsoleCursorInfo:一个指向 CONSOLE_CURSOR_INFO 结构体的指针,其中包含要设置的光标信息。

GetAsyncKeyState函数

GetAsyncKeyState 是 Windows API 中的一个函数,用于检查指定虚拟键的状态。其声明如下:
SHORT WINAPI GetAsyncKeyState(
  _In_ int vKey
);

该函数接受一个参数 vKey,表示要查询状态的虚拟键码。虚拟键码是一个表示键盘上某个按键的唯一标识符。例如,虚拟键码 0x41 对应于字母键 A。你也可以使用预定义的 VK_ 前缀常量来表示常见键,例如 VK_LEFT 表示左箭头键。

GetAsyncKeyState 返回一个 SHORT 类型的值,表示指定虚拟键的当前状态。如果指定的键按下,则最高位(位 15)将被设置为 1,否则为 0。如果指定的键在调用函数之前被按下,则返回值为负数。低 8 位(位 0 到 7)代表键的状态,其中最高位(位 7)表示该键是否处于被按下的状态,其他位则为保留位,始终为 0。

例如,如果某个键在调用 GetAsyncKeyState 时被按下,那么返回值的最高位将被设置为 1,表示该键当前处于按下状态。如果键在调用函数之前已经被按下,则返回值为负数,并且最高位仍然为 1。

通过检查返回值的最高位,程序可以判断某个特定的虚拟键是否被按下,从而实现键盘输入的检测功能。需要注意的是,GetAsyncKeyState 函数是异步的,它立即返回当前键盘状态,而不会等待用户的按键操作。

  • 键位按过之后最低位为1,否则为0,依次可以判断操作。

封装一个宏判断是否按过(函数也行)

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

封装一个函数移动光标位置

void SetPos(short x, short y)
{
	HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { x, y };

	SetConsoleCursorPosition(hout, pos);
}

本地化使用宽字符(打印墙与蛇身)

在软件开发中,"本地化"指的是根据特定地区或文化习惯进行适应和定制,以使软件在不同地区的用户中更易于理解和使用。宽字符(Wide Character)通常用于支持 Unicode 字符集,其中每个字符占据 16 位,以便支持各种语言的字符表示,包括非英语字母、符号和表意文字。

使用宽字符进行本地化的主要原因包括

1,支持多语言环境: 宽字符能够表示更多的字符,包括各种语言中的特殊字符、表意文字等。因此,在开发多语言环境下的软件时,使用宽字符可以更好地支持不同语言的显示和输入。

2,避免字符集问题: 在不同的地区和语言环境中,使用不同的字符集和编码方式是很常见的。使用宽字符可以避免在处理多语言文本时出现字符集不兼容或乱码等问题,因为 Unicode 是一种国际标准,几乎包含了世界上所有的字符。

3,文本显示的精确性: 对于一些语言,特别是东亚语言和中文,使用宽字符可以确保字符的显示精确,避免因字符编码问题而导致的显示异常或无法识别的字符。

4,易于扩展和维护: 使用宽字符可以为软件提供更强大和灵活的字符处理能力,使得软件更容易扩展到新的语言和地区,同时也更容易维护和管理。

如何本地化

设置本地化环境: 使用 setlocale 函数设置本地化环境,以指定使用的语言环境。语法如下:
char *setlocale(int category, const char *locale);

其中,category 参数指定了要设置的本地化的类别,可以是 LC_ALL、LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC 或 LC_TIME。locale 参数是一个字符串,指定了要使用的语言环境,如 “en_US.UTF-8” 或 “zh_CN.UTF-8”。
本地化:

#include<locale.h>
//设置本地环境
setlocale(LC_ALL, "");

宽字符的打印

1,宽字符前面必须加上L前缀表示宽字符,占位符为L “%lc” ,
,字符串为 L"%ls" 打印宽字符的函数为wprintf,用法与printf差不多。

2,宽字符的声明为 wchar_t。

例如:

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

核心逻辑实现

主逻辑分为3个过程

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

游戏开始实现要求

  • 控制台窗口名字的设置
  • ⿏标光标的隐藏
  • 打印欢迎界⾯
  • 创建地图
  • 初始化第蛇
  • 创建第⼀个⻝物

游戏运行要求

  • 蛇的移动(循环实现)
  • 蛇的碰撞判断
  • 方向判断
  • 吃食物与非吃食物操作

游戏运行结束要求

  • 资源的释放
  • 是否再来一局

游戏开始

宏定义

//整个界面大小
#define WIDTH 100
#define HEIGHT 40

//地图大小
#define X 60
#define Y 34
//蛇的身体,墙,食物宽字符
#define BODY L'●'
#define FOOD L'○'
#define WALL L'□'

游戏开始代码

//游戏开始
void Game_Start(snack* ps)
{
	//欢迎游戏界面
	WelcomeToGame();

	//打印地图
	CreateMap();

	//打印提示信息
	Help_Info();

	//蛇初始化
	Create_Snack(ps);
}

//界面设置
void SetInterface()
{
	//设置本地环境
	setlocale(LC_ALL, "");
	//设置界面大小, =两边没用空格
	system("mode con cols=100 lines=40");
	//给界面命名
	system("title 贪吃蛇");

	//获取输出设备句柄
	HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);

	//光标信息变量
	CONSOLE_CURSOR_INFO cursor_info;
	//从输出设备获取光标信息
	GetConsoleCursorInfo(hout, &cursor_info);
	//修改光标信息为不显示
	cursor_info.bVisible = false;
	SetConsoleCursorInfo(hout, &cursor_info);

}

//光标设置
void SetPos(short x, short y)
{
	HANDLE hout = GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { x, y };

	SetConsoleCursorPosition(hout, pos);
}


//欢迎界面
void WelcomeToGame()
{
	SetPos(WIDTH / 3 + 2, HEIGHT / 2 - 3);
	wprintf(L"欢迎来到贪吃蛇游戏!");
	SetPos(WIDTH / 2 - 5, 27);
	system("pause");
	system("cls");

	SetPos(WIDTH / 3 - 2, HEIGHT / 2 - 3);
	wprintf(L"使用↑ ↓ ← → 控制移动方向,F3加速,F4减速!");
	SetPos(WIDTH / 2 - 5, 27);
	system("pause");
	system("cls");
}

//创建地图
void CreateMap()
{
	SetPos(0, 0);
	for (int i = 0; i < X/2; i++)
	{
		wprintf(L"%c", WALL);
	}
	SetPos(0, Y);
	for (int i = 0; i < X/2; i++)
	{
		wprintf(L"%c", WALL);
	}
	for (int i = 1; i <= Y; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	for (int i = 1; i <= Y; i++)
	{
		SetPos(X - 2, i);
		wprintf(L"%lc", WALL);
	}
}

//操作提示
void Help_Info()
{
	SetPos(X + 5, Y / 2 + 3);
	wprintf(L"ESC键退出游戏,空格暂停游戏");
	SetPos(X + 5, Y / 2 + 5);
	wprintf(L"使用↑ ↓ ← → 控制移动方向");
	SetPos(X + 5, Y / 2 + 7);
	wprintf(L"F3加速,F4减速");
	SetPos(X + 5, Y / 2 + 9);
	wprintf(L"撞到蛇身或墙则结束游戏!");
}

游戏运行界面如下
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

游戏运行代码实现

代码声明

1,状态码与方向码使用枚举,更加直观方便,不用特别记忆。

2,将蛇节点与蛇分开定义,食物因为会衔接到蛇上,所以本质上也是蛇的节点。

typedef enum Game_Status
{
	OK,
	End_Normal,
	Kill_By_Wall,
	Kill_By_Self
}status;

typedef enum DIRECTION
{
	UP,
	DOWN,
	LEFT,
	RIGHT
}direction;

typedef struct SnackNode
{
	int x;
	int y;

	struct SnackNode* next;
}snacknode;

typedef struct Snack
{
	//蛇头
	snacknode* _pshead;
	//食物指针
	snacknode* _pfood;
	//游戏状态
	status _sta;
	//移动方向
	direction dir;
	//移动速递
	int sleep_time;
	//食物权重
	int food_weight;
	//总分
	int score;
}snack;



//设置游戏界面大小和本地模式以及光标显示
void SetInterface();

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

//游戏开始
void Game_Start(snack* ps);

//游戏开始界面设置
void WelcomeToGame();

//地图
void CreateMap();

//打印提示信息
void Help_Info();

//蛇的初始化创建
void Create_Snack(snack* ps);

//创建一个食物
void Create_Food(snack* ps);

//打印蛇
void Print_Snack(snack* ps);

//游戏运行
void Game_Run(snack* ps);

//暂停游戏
void Pause();

//蛇的移动
void snack_move(snack* ps);

//游戏结束
void Game_Over(snack*ps);

//撞到自己
void KillBySelf(snack* ps);

//撞到墙
void KillByWall(snack* ps);

//吃食物
void Eat_Food(snack* ps, snacknode** pn);

//非吃食物
void Not_Eat_Food(snack* ps, snacknode** pn);

蛇的运行代码

1, 蛇的创建初始化为四个节点,并创建食物打印出来。

//蛇的创建
void Create_Snack(snack* ps)
{
	ps->_pshead = new snacknode;
	if (ps->_pshead == NULL)
	{
		perror("Create_Snack::new");
	}
	ps->_pshead->x = 10;
	ps->_pshead->y = 5;
	ps->_pshead->next = NULL;

	snacknode* tail = ps->_pshead;
	for (int i = 0; i < 3; i++)
	{
		snacknode* cur = new snacknode;
		if (cur == NULL)
		{
			perror("Create_snack::new cur");
		}
		cur->x = tail->x - 2;
		cur->y = tail->y;
		tail->next = cur;
		cur->next = NULL;
		tail = tail->next;
	}

	ps->food_weight = 10;
	ps->score = 0;
	ps->_sta = OK;
	ps->sleep_time = 200;
	ps->dir = RIGHT;
	Create_Food(ps);

	Print_Snack(ps);
}

2,创建食物时不能与蛇身节点重合,也不难在边界上,需要判断。

void Create_Food(snack* ps)
{
	snacknode* food = new snacknode;
	if (food == NULL)
	{
		perror("Create_Food::new food");
	}
again:
	food->x = (rand() % ((X - 4) / 2) + 1) * 2;
	food->y = (rand() % (Y - 2)) + 1;
	food->next = nullptr;
	snacknode* cur = ps->_pshead;
	while (cur != NULL)
	{
		if (food->x == cur->x && food->y == cur->y)
		{
			goto again;
			break;
		}
		cur = cur->next;
	}

	ps->_pfood = food;
}

3,蛇每次移动一步都需要进行判断,前方是否撞到自身或者食物或者墙,从而进行不同操作,这里只要是状态OK是循环进行的。

void Print_Snack(snack* ps)
{
	snacknode* cur = ps->_pshead;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	SetPos(ps->_pfood->x, ps->_pfood->y);
	wprintf(L"%lc", FOOD);
}


#define Key_Press(VK) ((GetAsyncKeyState(VK) & 1) ? 1 : 0)
void Game_Run(snack* ps)
{
	while (ps->_sta == OK)
	{
		SetPos(X + 5, Y / 3 );
		wprintf(L"当前食物分数为:%2d", ps->food_weight);
		SetPos(X + 6, Y / 3 + 1 );
		wprintf(L"当前总分数为:%d", ps->score);
		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_UP) && ps->dir != DOWN)
		{
			ps->dir = UP;
		}
		else if (Key_Press(VK_DOWN) && ps->dir != UP)
		{
			ps->dir = DOWN;
		}
		else if (Key_Press(VK_SPACE))
		{
			Pause();
		}
		else if (Key_Press(VK_F3))
		{
			if (ps->sleep_time >= 60)
			{
				ps->sleep_time -= 20;
				ps->food_weight += 2;
			}
		}
		else if (Key_Press(VK_F4))
		{
			if (ps->sleep_time <= 400)
			{
				ps->sleep_time += 20;
				ps->food_weight -= 2;
			}
		}
		else if (Key_Press(VK_ESCAPE))
		{
			ps->_sta = End_Normal;
		}

		Sleep(ps->sleep_time);
		snack_move(ps);
	}
}

`4,暂停本质上就是休息,只不过是循环休息,当在此按下空格时打断。

//暂停游戏
void Pause()
{
	while (!Key_Press(VK_SPACE))
	{
		Sleep(300);
	}
}

5,蛇的移动先创建一个节点,判断下一个坐标位置,如果是空则链接上这个节点,释放最后一个节点,最后打印空格消除。

//蛇的移动
void snack_move(snack* ps)
{
	snacknode* pn = new snacknode;
	pn->next = NULL;
	if (ps->dir == LEFT)
	{
		pn->x = ps->_pshead->x - 2;
		pn->y = ps->_pshead->y;
	}
	else if (ps->dir == RIGHT)
	{
		pn->x = ps->_pshead->x + 2;
		pn->y = ps->_pshead->y;
	}
	else if (ps->dir == UP)
	{
		pn->x = ps->_pshead->x;
		pn->y = ps->_pshead->y - 1;
	}
	else if (ps->dir == DOWN)
	{
		pn->x = ps->_pshead->x ;
		pn->y = ps->_pshead->y + 1;
	}
	
	//检测是不是食物节点
	if (pn->x == ps->_pfood->x && pn->y == ps->_pfood->y)
	{
		Eat_Food(ps,&pn);
	}
	else
	{
		Not_Eat_Food(ps,&pn);
	}

	KillBySelf(ps);
	KillByWall(ps);
}

6, 移动后判断是否撞到自己或墙或者是吃食物操作。

//撞到自己判断
void KillBySelf(snack* ps)
{
	//检测是不是蛇身节点
	snacknode* cur = ps->_pshead->next;
	snacknode* pn = ps->_pshead;
	while (cur)
	{
		if (cur->x == pn->x && cur->y == pn->y)
		{
			ps->_sta = Kill_By_Self;
			return;
		}
		cur = cur->next;
	}
}

//撞墙判断
void KillByWall(snack* ps)
{
	snacknode* pn = ps->_pshead;
	//检测是不是墙
	if (pn->x == 0 || pn->y == 0 || pn->x == X - 2 || pn->y == Y)
	{
		ps->_sta = Kill_By_Wall;
		return;
	}
}

//吃食物操作
void Eat_Food(snack* ps, snacknode** pn)
{
	ps->score += ps->food_weight;
	ps->_pfood->next = ps->_pshead;
	ps->_pshead = ps->_pfood;

	snacknode* cur = ps->_pshead;
	snacknode* tail = ps->_pshead;

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

	free(*pn);
	*pn = NULL;

	Create_Food(ps);
	SetPos(ps->_pfood->x, ps->_pfood->y);
	wprintf(L"%lc", FOOD);
}

//不吃食物操作
void Not_Eat_Food(snack* ps, snacknode** pn)
{
	(*pn)->next = ps->_pshead;
	ps->_pshead = (*pn);

	snacknode* cur = ps->_pshead;
	snacknode* tail = ps->_pshead;

	while (cur->next->next)
	{
		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;
}

游戏结束代码实现

1,结束后判断如何结束,打印结束原因,并设置是否再来一局,释放上一句资源。

//游戏结束
void Game_Over(snack* ps)
{
	snacknode* cur = ps->_pshead->next;
	snacknode* del = ps->_pshead;
	while (cur)
	{
		free(del);
		del = cur;
		cur = cur->next;
	}
	free(cur);
	ps->_pshead = NULL;
	if (ps->_pfood != NULL)
	{
		free(ps->_pfood);
	}
	ps->_pfood = NULL;

	SetPos(0, Y + 1);
	if (ps->_sta == End_Normal)
	{
		wprintf(L"您正常退出了游戏!");
	}
	else if (ps->_sta == Kill_By_Self)
	{
		wprintf(L"您撞到了自己,游戏结束!");
	}
	else if (ps->_sta == Kill_By_Wall)
	{
		wprintf(L"您撞墙了,游戏结束!");
	}
}

2,主函数代码实现:

int main()
{
	srand((unsigned int)time(NULL));
	char con = 'n';
	while (true)
	{
		snack S;
		snack* psnack = &S;
		//设置jiem
		SetInterface();

		//游戏开始
		Game_Start(psnack);

		//游戏运行
		Game_Run(psnack);
		//testPrint(psnack);

		//游戏结束
		Game_Over(psnack);
		SetPos(0, Y + 2);
		wprintf(L"请问是否再来一次?(y/n):");
		scanf("%c", &con);
		getchar();
		if (con == 'y' || con == 'Y')
		{
			system("cls");
			continue;
		}
		else
		{
			system("cls");
			break;
		}
	}
	

	return 0;
}

看了之后不妨自己实现一下试试,可以先跟着敲慢慢理解,在自己来一次,可以自己添加想要的功能。

  • 23
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值