C语言实现 贪吃蛇

本文介绍了使用C语言开发贪吃蛇游戏的详细过程。包括游戏准备工作,如控制台设置、光标信息处理、按键获取、字符打印准备等;核心逻辑实现,涵盖主函数、GameStart、GameRun和GameEnd部分;最后总结了程序结构。后续还会对程序进行扩展。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 明确蛇通过什么方式表示移动

当然是打印字符!蛇尾走过的位置要打印空字符,掩盖身体。

所以光标的位置会根据蛇的位置时刻改变。所以更新光标位置、判断蛇的走向在贪吃蛇中很关键

游戏的准备工作

1. 游戏开始前

地图的绘制。

提示信息:得分、游戏按键:加速减速、暂停、退出等。

2. 技术要点

以下功能需要用到 Win32 API的各项服务,需要包含头文件

#include <windows.h>

控制台的设置

我们要对控制台的窗口进行调整:需要用到以下方法:

mode 命令mode con cols=100 lines=30

//这是用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列。

title 命令title 贪吃蛇

这些能在控制台窗口执行的命令,也可以调用C语⾔函数system来执行。例如:

//设置控制台
system("mode con cols=100 lines=40");
system("title 贪吃蛇");
//光标

光标的信息的获取和修改

控制台的坐标 COORD

COORD 是 Windows API 中定义的一个结构体,用于表示控制台屏幕缓冲区上的字符坐标。它包含两个成员变量,分别是 X 和 Y,分别表示水平和垂直方向上的坐标位置。

在控制台屏幕缓冲区中,坐标系的原点位于左上角,即顶部左侧单元格,坐标值从 (0, 0) 开始,向右和向下递增。

COORD 类型的声明

typedef struct _COORD {
 SHORT X;
 SHORT Y;
} COORD, *PCOORD;
给坐标赋值
COORD pos = { 10, 15 };
 获取句柄信息和修改

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

对于句柄,我们可以将其理解为工具的把手,我们需要先有这个把手才能继续使用工具
(就是我们现在要使用的设备,屏幕)
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息, 以下是 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; //隐藏控制台光标

 设置好信息,接下来信息设置对应到我们指定的句柄

下面是SetConsoleCursorInfo函数的结构:

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

这时候我们可以结合坐标的设置来更新光标的位置:

第一个参数要接受指定的句柄,第二个参数是光标的位置。

 实例:
//光标
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);//将修改后的光标信息重新设置到控制台窗口中

 当然,我们也可以直接定义CONSOLE_CURSOR_INFO CursorInfo 后 ,修改其信息再句柄信息赋值给 handle

因为游戏中打印时用得到,我们要封装一个函数来设置光标位置:

void SetPos(short x, short y)
{
	COORD pos = { x, y };//这里要用 '{'   '}'
	HANDLE hOutPut = NULL;
	hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
	SetConsoleCursorPosition(hOutPut, pos);
}

获取按键情况

GetAsyncKeyState 的函数原型如下:
SHORT GetAsyncKeyState(
 int vKey
);

vKey 是对应按键的键码

GetAsyncKeyState 的返回值
是short类型,在上⼀次调用  GetAsyncKeyState 函数后,如果返回的16位的short数据中, 最高位是1,说明按键的状态是按下,
如果最高是0,说明按键的状态是抬起;
如果最低位被置为1则说明,该按键被按过,否则为0。
由于我们需要判断一个键是否被按过,可以检测  GetAsyncKeyState 的返回值的 最低位是否为 1,宏定义:
 #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

函数的解释:

GetAsyncKeyState 函数返回的键的状态只是一个瞬时状态,并不会影响键的状态本身。因此,即使之前某个键被按下并导致最低位为 1,在下一次调用 GetAsyncKeyState 函数时,如果没有该键被按下,它会返回 0。因此,下一次判断即使没有按下键,也会返回 0。

 游戏中会用到的虚拟键码

常数 :                                        value:                                        说明:

• 左

VK_LEFT0x25LEFT ARROW 键

• 上

VK_UP0x26UP ARROW 键

• 右

VK_RIGHT0x27RIGHT ARROW 键

• 下

VK_DOWN0x28DOWN ARROW 键

• 空格

VK_SPACE0x20空格键

• ESC

VK_ESCAPE0x1BESC 键

• F3 (用来判断加速)
VK_F30x72F3 键
• F4 (用来判断减速)
VK_F40x73F4 键
键码的使用实例

 下面是对一部分键码的应用:

//此处的_Dir是维护蛇的状态结构体中的方向

//上
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;
}

……(其它按键的检测和对应的状态更新)

3.字符打印前的准备

3.1 <locale.h>本地化

<locale.h> 提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分。
在标准中,依赖地区的部分有以下几项:
数字量的格式
货币量的格式
字符集
•  期和时间的表示形式

当涉及到字符编码时,有两种主要的表示方式:宽字符和本地化的字符集。

  1. 宽字符

    • 宽字符通常使用 Unicode 编码,每个字符通常占据 16 位或 32 位内存空间。
    • 在 Windows 平台上,宽字符通常使用 wchar_t 类型表示。
    • 宽字符能够支持全球范围内的字符集,因为 Unicode 包含了几乎所有语言的字符。
    • 宽字符是在一些需要处理多语言和国际化的应用程序中常用的编码方式,比如在处理各种语言的文本时。
  2. 没有本地化的字符

    • 没有本地化的字符通常使用单字节编码,如 ASCII 编码或 ISO-8859 编码,每个字符通常占据 1 个字节的内存空间。
    • 这种编码方式只能表示一部分字符集,通常只能用于处理一种语言的文本,而不支持多语言的处理。
    • 由于没有本地化的字符只能处理有限的字符集,因此在处理多语言或者需要考虑到非英文字符时,它的使用可能会受限制。

 

3.2 类项

 

地区设置可以影响程序中的各个方面,包括字符串比较、字符处理、货币格式、数字格式和时间格式等。

C 语言提供了一系列宏用于针对不同的类项(category)进行修改。下面是这些宏及其对应的类项:

  • LC_COLLATE:影响字符串比较函数 strcoll()strxfrm(),用于设置字符串排序规则。
  • LC_CTYPE:影响字符处理函数的行为,比如 toupper()tolower() 等函数,用于处理字符的大小写转换等。
  • LC_MONETARY:影响货币格式,用于设置货币的显示格式。
  • LC_NUMERIC:影响数字格式,主要影响 printf()scanf() 等函数的数字格式。
  • LC_TIME:影响时间格式,主要影响 strftime()wcsftime() 等函数的时间格式。
  • LC_ALL:这个宏用于同时设置所有类项,将以上所有类别设置为给定的语言环境。

通过修改这些类项,程序可以根据不同地区的要求进行定制,以提供更好的用户体验和更适合当地文化的应用程序。

 这里我们主要对于字符的需要,为了可以打印不局限于ASCII编码的字符集,这里我们要打印宽字符,首先就要改变到本地的环境

setlocale函数原型 :

char* setlocale (int category, const char* locale);
用于修改当前地区:

第一个参数是类项的选择,(可以是所有项)

第二个参数是模式,C标准给第二个参数仅定义了两种可能取值:

"C"(正常模式) 和 ""(本地模式),这里注意不可以留有空格

setlocale(LC_ALL, "");//切换到本地环境

现在的模式就支持宽字符了(汉字和其他占两个字节的字符等)

3.3宽字符打印

<wchar.h> 是 C 标准库中的头文件,它提供了对宽字符处理的支持。包含这个头文件的作用是引入了一系列宽字符处理所需的函数和类型的声明,比如 wchar_t 类型以及一些宽字符处理函数,如 wprintf()wscanf() 等。

 以下是宽字符打印的实例:

#include <stdio.h>
#include <wchar.h>
#include <locale.h>

int main() {

    setlocale(LC_ALL, "");//切换到本地环境

    // 打印单个宽字符
    wchar_t wide_char = L'宽';
    wprintf(L"单个宽字符:%lc\n", wide_char);

    // 打印宽字符串
    wchar_t wide_str[] = L"宽字符串";
    wprintf(L"宽字符串:%ls\n", wide_str);

    getchar();
    return 0;
}

 

3.4 宏定义游戏需要的字符,便于后边直接使用和标识 

表示蛇身体

#define BODY L'●'

• 表示食物

#define FOOD L'★'

表示墙体

#define WALL L'□'

3.5 地图的绘制

普通字符和宽字符打印出宽度的展示如下:

所以打印完一个宽字符需要对x,y作不同的移动 

如下,我们要打印以宽字符为单位的 27 * 58 ,对应行和列:

地图绘制的代码实现:
void CreateMap() //打印一个27*25的贪吃蛇地图
{
	//上
	SetPos(0, 0);
	int i = 0;
	for (i = 0;i < 58;i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0;i < 58;i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1;i < 26;++i)
	{
		SetPos(0, i);
		wprintf(L"%lc\n", WALL);
	}
	//右
	for (i = 1;i < 26;++i)
	{
		SetPos(56, i);
		wprintf(L"%lc\n", WALL);
	}
}

 3.6 食物打印

 注意

1. 为了能使得蛇可以正常吃掉食物,食物的x坐标必须要是2的倍数

2. 食物的出现不能在蛇的结点上

在这之前,我们要先有初始化后的蛇结点,才能对后续的食物结点初始化做判断。

3.7 游戏状态和初始化 

3.7.1 蛇身结点结构
typedef struct SnakeNode
{ 
    int x;
    int y;
    struct SnakeNode* next;//连接下一个结点的结构体指针
}SnakeNode, * pSnakeNode;
 3.7.2 维护贪吃蛇
//管理蛇的状态和维护
typedef struct Snake
{
	pSnakeNode _pSnake;//维护整条蛇的头结点指针
	pSnakeNode _pFood;//维护食物的指针
	enum DIRECTION _Dir;//蛇头的方向,默认向右
	enum GAME_STATUS _Status;//游戏状态
	int _Score;//累计得分数
	int _FoodWeight;//默认初始食物10分
	int _SleepTime;//睡眠时间
}Snake, * pSnake;

这样我们需要一个指向这个结构体的“蛇头”,来对游戏中的各种情况做出对状态的更新

 各种值的枚举

方便管理和识别

//方向
enum  DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
//游戏状态
enum GAME_STATUS
{
	OK,
	KILL_BY_WALL,
	KILL_BY_SELF,
	PAUSE,
	ESC
};

 

到此,我们的准备工作几乎完成(帮助信息打印并没有放在上面~),进入游戏核心功能的实现:

核心逻辑实现分析:

图示分析:

主逻辑分为3个过程:
游戏开始(GameStart)完成游戏的初始化
游戏运行(GameRun)完成游戏运行逻辑的实现
游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

主函数 

#define _CRT_SECURE_NO_WARNINGS 1
#include  "snake.h"

void test()
{	
	int ch = 0;//EOF返回值为int
	srand((unsigned int)time(0));

	do
	{	
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(27, 15);
		printf("再来一局吗?( Y / N ) :");
		SetPos(27, 47);
		fflush(stdout);
		ch = getchar();

		while (getchar() != '\n'); // 清空输入缓冲区
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 28);
}

int main()
{	
	setlocale(LC_ALL, "");//切换到本地环境

	test();
	return 0;
}

GameStart部分

控制台窗口大小的设置
控制台窗口名字的设置
标光标的隐藏
打印欢迎界面
创建地图
初始化第蛇
创建第⼀个食物
void GameStart(pSnake ps)
{	
	//设置控制台
	system("mode con cols=100 lines=40");
	system("title 贪吃蛇");
	//光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	//GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);//将修改后的光标信息重新设置到控制台窗口中
	//打印欢迎界面
	WelcomeToGame();
	//打印地图
	CreateMap();
	//初始化蛇
	InitSnake(ps);
	//创造第一个食物
	CreateFood(ps);
}
 打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 13);
	printf("欢迎来到贪吃蛇游戏!");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");//清空控制台
	SetPos(25, 12);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(25, 13);
	printf("加速将能得到更的分数。\n");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");//清空屏幕,变为空白
}
初始化蛇

这里我们要自定义蛇头的位置,保证初始化后打印的位置是正常的,同样x要为2的倍数

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->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		//头插法
		if (ps->_pSnake == NULL)//第一个结点
		{
			ps->_pSnake = cur;
		}
		else//剩下的结点
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}

		//打印蛇身结点
		cur = ps->_pSnake;
		while (cur)
		{	
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", BODY);
			cur = cur->next;
		}
	}
	
	//初始化贪吃蛇数据
	ps->_Dir = RIGHT;//默认向右
	ps->_Score = 0;
	ps->_FoodWeight = 10;
	ps->_Status = OK;
	ps->_SleepTime = 200;
}
打印食物

此时,我们有了维护蛇状态的结构体指针 pSnake snake;接下来可以确定食物的坐标了

void CreateFood(pSnake ps)
{
	//x:2~54 y:1~25
	int x, y;
again:

	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;

	} while (x % 2 != 0);//食物x为偶数
	pSnakeNode cur;
	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::malloc()");
		return;
	}
	else
	{
		pFood->x = x;
		pFood->y = y;
		pFood->next = NULL;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%lc", FOOD);
		ps->_pFood = pFood;
	}
}

其中,我们用伪随机,需要用到:包含在<stdlib.h>中的rand() 和<time.h>中的time()来增加随机性,用食物的结点x , y坐标遍历蛇的每一个结点,直到满足要求,完成食物结点的创建。

GameRun部分

• 运行,循环的主体 :这里可以改变蛇的方向状态

{        

        • 打印帮助信息

        • 移动 :根据蛇的方向,计算下一个结点的坐标

        • 判断 :判断是否是食物,IS or NOT

        • 判断蛇的运行状态 :是否死亡

}

打印帮助信息 
void PrintHelpInfo()
{
	//打印提示信息
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	SetPos(64, 18);
	printf("ESC :退出游戏.space:暂停游戏.");
}
 运行
//运行
void GameRun(pSnake ps)
{	
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("得分:%02d ", ps->_Score);
		printf("每个食物得分:%02d分", ps->_FoodWeight);

		//上
		if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = ESC;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;//一个食物分数最高是20分
			}
		}
		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 SnakeMove(pSnake ps)
{	

	pSnakeNode pnextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if(pnextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	//确定下一个结点的坐标,根据蛇头方向确定
	switch (ps->_Dir)
	{
	case UP:
	{
		pnextNode->x = ps->_pSnake->x;
		pnextNode->y = ps->_pSnake->y - 1;
	}
		break;
	case DOWN:
	{
		pnextNode->x = ps->_pSnake->x;
		pnextNode->y = ps->_pSnake->y + 1;
	}
		break;
	case LEFT:
	{
		pnextNode->x = ps->_pSnake->x - 2;
		pnextNode->y = ps->_pSnake->y;
	}
		break;
	case RIGHT:
	{
		pnextNode->x = ps->_pSnake->x + 2;
		pnextNode->y = ps->_pSnake->y;
	}
		break;
	}
	//判断
	if (NextIsFood(ps, pnextNode))
	{
		EatFood(ps, pnextNode);
	}
	else//如果没有食物
	{
		NoFood(ps, pnextNode);
	}
	KillByWall(ps);
	KillBySelf(ps);
}
 食物判断
//判断下一个结点是否为食物
int NextIsFood(pSnake ps, pSnakeNode psn)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
是食物:
void EatFood(pSnake ps, pSnakeNode psn)
{	
	//头插
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_Score += ps->_FoodWeight;//吃掉食物加分
	//释放食物节点
	free(ps->_pFood);
	//创建新的食物
	CreateFood(ps);
}
 不是食物:
//不吃
void NoFood(pSnake ps, pSnakeNode psn)
{
	//头插
	pSnakeNode cur = ps->_pSnake;
	psn->next = cur;
	ps->_pSnake = psn;

	//打印蛇身
	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("  ");
	free(cur->next);
	cur->next = NULL;
}

GameEnd部分

对失败原因的打印

堆空间的结点释放

void GameEnd(pSnake ps)
{
	 pSnakeNode cur = ps->_pSnake;
	 SetPos(24, 12);
	 switch (ps->_Status)
	 {
	 case ESC:
		 printf("您主动退出游戏\n");
		 break;
	 case KILL_BY_SELF:
		 printf("您撞上自己了 ,游戏结束!\n");
		 break;
	 case KILL_BY_WALL:
		 printf("您撞墙了,游戏结束!\n");
		 break;
	 }
	//释放蛇的节点
	 while (cur)
	 {
		 pSnakeNode del = cur;
		 cur = cur->next;
		 free(del);
	 }
}

 程序总结:

snake.h 函数声明和字符定义等

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

#define BODY L'●'
#define FOOD L'★'
#define WALL L'□'
#define POS_X 28
#define POS_Y 10
//判断一个键是否被按过,可以检测GetAsynckeyState返回值的最低为是否为1
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

//方向
enum  DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
//游戏状态
enum GAME_STATUS
{
	OK,
	KILL_BY_WALL,
	KILL_BY_SELF,
	PAUSE,
	ESC
};
//蛇身节点
typedef struct SnakeNode
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//管理蛇的状态和维护
typedef struct Snake
{
	pSnakeNode _pSnake;//维护整条蛇的头结点指针
	pSnakeNode _pFood;//维护食物的指针
	enum DIRECTION _Dir;//蛇头的方向,默认向右
	enum GAME_STATUS _Status;//游戏状态
	int _Score;//累计得分数
	int _FoodWeight;//默认初始食物10分
	int _SleepTime;//睡眠时间
}Snake, * pSnake;
//位置
void SetPos(short x, short y);
//游戏开始
void GameStart();
//打印欢迎界面
void WelcomeToGame();
//打印地图
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//打印食物
void CreateFood();
//游戏运行
void GameRun(pSnake ps);
//移动
void SnakeMove(pSnake ps);
//结束打印信息
void GameEnd(pSnake ps);
//
void EatFood(pSnake ps, pSnakeNode psn);
//
void NoFood(pSnake ps, pSnakeNode psn);

snake.c 函数实现

#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void SetPos(short x, short y)
{
	COORD pos = { x, y };//这里要用 '{'   '}'
	HANDLE hOutPut = NULL;
	hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
	SetConsoleCursorPosition(hOutPut, pos);
}
void WelcomeToGame()
{
	SetPos(40, 13);
	printf("欢迎来到贪吃蛇游戏!");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");//清空控制台
	SetPos(25, 12);
	printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(25, 13);
	printf("加速将能得到更的分数。\n");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");//清空屏幕,变为空白
}
void CreateMap() //打印一个27*25的贪吃蛇地图
{
	//上
	SetPos(0, 0);
	int i = 0;
	for (i = 0;i < 58;i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//下
	SetPos(0, 26);
	for (i = 0;i < 58;i += 2)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (i = 1;i < 26;++i)
	{
		SetPos(0, i);
		wprintf(L"%lc\n", WALL);
	}
	//右
	for (i = 1;i < 26;++i)
	{
		SetPos(56, i);
		wprintf(L"%lc\n", WALL);
	}
}
void PrintHelpInfo()
{
	//打印提示信息
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己\n");
	SetPos(64, 16);
	printf("用↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速,F4 为减速\n");
	SetPos(64, 18);
	printf("ESC :退出游戏.space:暂停游戏.");
}
void pause()//暂停
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}
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->next = NULL;
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		//头插法
		if (ps->_pSnake == NULL)//第一个结点
		{
			ps->_pSnake = cur;
		}
		else//剩下的结点
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}

		//打印蛇身结点
		cur = ps->_pSnake;
		while (cur)
		{	
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", BODY);
			cur = cur->next;
		}
	}
	
	//初始化贪吃蛇数据
	ps->_Dir = RIGHT;//默认向右
	ps->_Score = 0;
	ps->_FoodWeight = 10;
	ps->_Status = OK;
	ps->_SleepTime = 200;
}
void CreateFood(pSnake ps)
{
	//x:2~54 y:1~25
	int x, y;
again:

	srand(time(0));
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;

	} while (x % 2 != 0);//食物x为偶数
	pSnakeNode cur;
	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::malloc()");
		return;
	}
	else
	{
		pFood->x = x;
		pFood->y = y;
		pFood->next = NULL;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%lc", FOOD);
		ps->_pFood = pFood;
	}
}
void GameStart(pSnake ps)
{	
	//设置控制台
	system("mode con cols=100 lines=40");
	system("title 贪吃蛇");
	//光标
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	CONSOLE_CURSOR_INFO CursorInfo;
	//GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息
	CursorInfo.bVisible = false;
	SetConsoleCursorInfo(handle, &CursorInfo);//将修改后的光标信息重新设置到控制台窗口中
	//打印欢迎界面
	WelcomeToGame();
	//打印地图
	CreateMap();
	//初始化蛇
	InitSnake(ps);
	//创造第一个食物
	CreateFood(ps);
}

int 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;
		return 1;
	}
	return 0;
}
int KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake->next->next;//蛇头一定是在蛇身第四个结点开始有可能碰撞
	while (cur)
	{
		if ((cur->x == ps->_pSnake->x) && (cur->y == ps->_pSnake->y))
		{
			ps->_Status = KILL_BY_SELF;
			return 1;
		}
			cur = cur->next;
	}
	return 0;
}
//判断下一个结点是否为食物
int NextIsFood(pSnake ps, pSnakeNode psn)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}
//吃
void EatFood(pSnake ps, pSnakeNode psn)
{	
	//头插
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}
	ps->_Score += ps->_FoodWeight;//吃掉食物加分
	//释放食物节点
	free(ps->_pFood);
	//创建新的食物
	CreateFood(ps);
}
//不吃
void NoFood(pSnake ps, pSnakeNode psn)
{
	//头插
	pSnakeNode cur = ps->_pSnake;
	psn->next = cur;
	ps->_pSnake = psn;

	//打印蛇身
	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("  ");
	free(cur->next);
	cur->next = NULL;
}
//移动
void SnakeMove(pSnake ps)
{	

	pSnakeNode pnextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if(pnextNode == NULL)
	{
		perror("SnakeMove()::malloc()");
		return;
	}
	//确定下一个结点的坐标,根据蛇头方向确定
	switch (ps->_Dir)
	{
	case UP:
	{
		pnextNode->x = ps->_pSnake->x;
		pnextNode->y = ps->_pSnake->y - 1;
	}
		break;
	case DOWN:
	{
		pnextNode->x = ps->_pSnake->x;
		pnextNode->y = ps->_pSnake->y + 1;
	}
		break;
	case LEFT:
	{
		pnextNode->x = ps->_pSnake->x - 2;
		pnextNode->y = ps->_pSnake->y;
	}
		break;
	case RIGHT:
	{
		pnextNode->x = ps->_pSnake->x + 2;
		pnextNode->y = ps->_pSnake->y;
	}
		break;
	}
	//判断
	if (NextIsFood(ps, pnextNode))
	{
		EatFood(ps, pnextNode);
	}
	else//如果没有食物
	{
		NoFood(ps, pnextNode);
	}
	KillByWall(ps);
	KillBySelf(ps);
}
//运行
void GameRun(pSnake ps)
{	
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("得分:%02d ", ps->_Score);
		printf("每个食物得分:%02d分", ps->_FoodWeight);

		//上
		if (KEY_PRESS(VK_UP) && ps->_Dir != DOWN)
		{
			ps->_Dir = UP;
		}
		//下
		else if (KEY_PRESS(VK_DOWN) && ps->_Dir != UP)
		{
			ps->_Dir = DOWN;
		}
		//左
		else if (KEY_PRESS(VK_LEFT) && ps->_Dir != RIGHT)
		{
			ps->_Dir = LEFT;
		}
		//右
		else if (KEY_PRESS(VK_RIGHT) && ps->_Dir != LEFT)
		{
			ps->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_Status = ESC;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 80)
			{
				ps->_SleepTime -= 30;
				ps->_FoodWeight += 2;//一个食物分数最高是20分
			}
		}
		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)
{
	 pSnakeNode cur = ps->_pSnake;
	 SetPos(24, 12);
	 switch (ps->_Status)
	 {
	 case ESC:
		 printf("您主动退出游戏\n");
		 break;
	 case KILL_BY_SELF:
		 printf("您撞上自己了 ,游戏结束!\n");
		 break;
	 case KILL_BY_WALL:
		 printf("您撞墙了,游戏结束!\n");
		 break;
	 }
	//释放蛇的节点
	 while (cur)
	 {
		 pSnakeNode del = cur;
		 cur = cur->next;
		 free(del);
	 }
}

test.c 应用

#define _CRT_SECURE_NO_WARNINGS 1
#include  "snake.h"

void test()
{	
	int ch = 0;//EOF返回值为int
	srand((unsigned int)time(0));

	do
	{	
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(27, 15);
		printf("再来一局吗?( Y / N ) :");
		SetPos(27, 47);
		fflush(stdout);
		ch = getchar();

		while (getchar() != '\n'); // 清空输入缓冲区
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 28);
}

int main()
{	
	setlocale(LC_ALL, "");//切换到本地环境

	test();
	return 0;
}

后面我会对程序作更为全面的扩展,敬请期待~

感谢本期小伙伴们的陪伴,我们下期再见!

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值