使用C语言在VS 环境下基本实现贪吃蛇游戏

一丶 实现前的准备工作

1. 设置vs运行环境为window控制台而非window终端

本项目实现环境是在window控制台下,因此需要多vs的运行环境进行设置

1. 正确的运行环境页面

window控制台页面在鼠标右键右击上边栏是这样的:

在这里插入图片描述

如果你的运行页面是这样的:

在这里插入图片描述
那么你就需要进行设置了。

2. 设置正确的运行环境

1.首先在该页面下鼠标右键右击上边栏,然后点击设置

在这里插入图片描述

2. 在启动的默认终端应用程序中切换成Window控制台主机并保存更改

在这里插入图片描述

3.正确的运行环境窗口

在这里插入图片描述

2. 了解句柄(下面代码能看明白会照葫芦画瓢用就行)

  GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle);
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
//STD_OUTPUT_HANDLE --标准设备
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

3. 利用system函数丶cmd命令设置window控制台窗口的尺寸

控制台程序:平常我们运⾏起来的⿊框程序其实就是控制台程序
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩和窗口的标题

	mode con cols=100 lines=30
	title 贪吃蛇

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

#include <stdio.h>
#include <stdlib.h>   //system函数在stdlib.h头文件中
int main()
{
	//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
	system("mode con cols=100 lines=30");
	//设置cmd窗⼝名称
	system("title 贪吃蛇");
	//设置system("pause") 让程序在结束前停下来 
	//这样就能看到我们设置的窗口信息了
	//设置暂停
	system("pause");
	return 0;
}
		运行后如下图:

在这里插入图片描述


4. 了解控制台屏幕上的坐标COORD和设置光标属性

1. COORD

COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标。

定义:
typedef struct _COORD {
	SHORT X;
	SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };

2. 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
	简单地讲,dwSize控制的是光标闪烁时的占比,基于一个字符的半分比1~100之间;bVisible用于控制光标是否可见(闪烁)。
CursorInfo.bVisible = false; //隐藏控制台光标

3. GetConsoleCursorInfo

GetConsoleCursorInfo函数用于检索有关指定控制台屏幕缓冲区的光标⼤⼩可⻅性的信息。

BOOL WINAPI GetConsoleCursorInfo(
  _In_  HANDLE               hConsoleOutput,
  _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

功能:
	获取光标相关信息
参数:
	hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
	lpConsoleCursorInfo 指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关控制台游标的信息。

返回值:
	如果函数成功,则返回值为非零值。
	如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
与句柄配合的实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

4. SetConsoleCursorInfo

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

BOOL WINAPI SetConsoleCursorInfo(
  _In_       HANDLE              hConsoleOutput,
  _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
功能:
	设置光标的属性
参数:
	hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
	lpConsoleCursorInfo 指向CONSOLE_CURSOR_INFO结构的指针,该结构为控制台屏幕缓冲区的游标提供新规范。

返回值:
	如果函数成功,则返回值为非零值。
	如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
代码实例:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

5. SetConsoleCursorPosition

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

BOOL WINAPI SetConsoleCursorPosition(
  _In_ HANDLE hConsoleOutput,
  _In_ COORD  dwCursorPosition
);

功能:
	设置光标的位置
	
参数:
	hConsoleOutput 控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_READ访问权限。
	dwCursorPosition 用于指定新的光标位置(以字符为单位)。坐标是屏幕缓冲区字符单元格的列和行。坐标必须位于控制台屏幕缓冲区的边界内。
	
返回值:
	如果函数成功,则返回值为非零值。
	如果函数失败,则返回值为零。要获取扩展错误信息,请调用GetLastError。
代码实例:
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);
}

5.了解键盘响应函数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 )

6. 宽字符的输出

这是我们游戏运行时的页面:
在这里插入图片描述
在游戏地图上,我们打印墙体使⽤宽字符:口,打印蛇头使用宽字符♛,打印蛇体使⽤宽字符●,打印⻝物使⽤宽字符★。普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。

如何使用C语言在VS环境下打印宽字符?
#include <stdio.h>
#include <locale.h>		//setlocale的头文件 用于本地化设置

int main()
{
	//想正确使用wprintf  需要使用setlocale设置本地化
	//setlocale(LC_ALL, "")表示采用当前地区的语言风格
	setlocale(LC_ALL, "");
	//在设置初值前加L 表示该字符为宽字节数据
	wchar_t ch1 = L'♛';
	wchar_t ch2 = L'●';
	wchar_t ch3 = L'口';
	wchar_t ch4 = L'★';
	wchar_t ch5 = L'天';
	wchar_t ch6 = L'下';
	wchar_t ch7 = L'无';
	wchar_t ch8 = L'双';
	//宽字体占两个字节 这里不纠结汉字的字节大小
	//因为汉字具体的字节大小跟编码有关
	
	//在打印前也需要加L 表示该字符为宽字节数据
	wprintf(L"%c\n", ch1);
	wprintf(L"%c\n", ch2);
	wprintf(L"%c\n", ch3);
	wprintf(L"%c\n", ch4);
	wprintf(L"%c\n", ch5);
	wprintf(L"%c\n", ch6);
	wprintf(L"%c\n", ch7);
	wprintf(L"%c\n", ch8);

	return 0;
}
运行效果:

在这里插入图片描述

setlocale还可以使用C语言模式,这里不做过多描述,有需要可自行查阅。

二丶 实现后的游戏效果

1.游戏的欢迎页面

在这里插入图片描述
在这里插入图片描述

2.开始游戏的页面

在这里插入图片描述

3.游戏结束的两种情况:撞墙和撞到自身

在这里插入图片描述
在这里插入图片描述

4.游戏中如果未撞到墙或自身,那么游戏会一直进行下去,我这里是这样设计的。

三丶 游戏的项目结构和逻辑大纲

1. 项目结构

1. 头文件snake.h用于存放蛇体结构体丶函数的声明和枚举
2. 源文件snake.c用于存放函数的具体实现
3. 源文件main.c用于存放菜单框架和测试程序

2. 游戏逻辑

  • 游戏欢迎页面,点击后进行开始游戏页面
  • 操作空格键开始和暂停游戏
  • ↑ .↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速
  • F3加速后单个食物分数会变高 F4减速后单个食物分数会降低
  • 撞到墙或撞到自身时游戏结束,否则将一直进行下去
  • 游戏结束页面可以输入来选择是否再来一局

3. 项目逻辑大纲

在这里插入图片描述


四丶 项目所需要实现的具体功能

先将蛇体结构体丶蛇体节点丶枚举和宏放到这里,方便下面的代码查阅

//宏定义 利用三目来接收敲击的情况 按了为1 否则为0
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

//方向	
//蛇的移动方向有四种  上下左右
enum DIRECION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//游戏状态
enum GAME_STATUS
{
	OK,				//游戏可以继续
	KILL_BY_WALL,	//撞到墙
	KILL_BY_SELF,	//撞到自身
	END_NOMAL		//正常结束
};

//墙 蛇体结点和食物的符号
#define WALL L'口'		//墙体
#define HEAD L'♛'		//蛇头
#define BODY L'⬤'		//蛇体
#define FOOD L'★'		//食物

//蛇出现的初始位置	
//同时也是蛇头的起始位置
#define POS_X 24
#define POS_Y 5

//蛇身结点-存放单个蛇体节点的坐标和下一个身体节点的指针
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 GameStart(pSnake ps)

void GameStart(pSnake ps)函数用于初始化游戏,它整合完成了很多功能:

  • 设置游戏窗口尺寸和标题
  • 设置欢迎页面-WelcomeToGame()
  • 设置游戏地图-CreatMap()
  • 设置初始化蛇的部分数据-InitSnake(ps)
  • 设置第一个食物-CreateFood(ps)
void GameStart(pSnake ps);
//游戏开始前的初始化
void GameStart(pSnake ps)
{
	//用system系统函数设置控制台尺寸和窗口名称
	//mode 为DOS命令
	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();

	//打印地图
	CreatMap();

	//初始蛇的相关数据
	InitSnake(ps);
	
	//创造第一个食物
	CreateFood(ps);
}

void WelcomeToGame()

void WelcomeToGame()用于设置游戏开始前的欢迎页面,它配合SetPos(short, short)使用,实现了在屏幕上的定位数据输出。

//欢迎界面
void WelcomeToGame()
{
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);
	system("pause");
	system("cls");
	SetPos(25, 12);
	printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速");
	SetPos(25, 13);
	printf("加速将能得到更高的分数。\n");
	SetPos(40, 25);
	//暂停
	system("pause");
	//清屏
	system("cls");

}

void CreateMap()

void CreateMap()用于地图的打印,它配合SetPos(short, short),在屏幕的上下左右四个方向打印墙体。

  • 上面的墙体:上( 0, 0)-( 56, 0)
  • 下面的墙体:下( 0, 26)-( 56, 26)
  • 左面的墙体:左( 0, 1)-( 0, 25)
  • 右面的墙体:右(56, 1)-( 56, 25)

这里有一个格外需要注意的两个点,一个是我们是用宽字符墙体进行打印地图,它单个字体占字节数为2,在winodw控制台下占两个单位坐标,这将会影响蛇的移动轨迹,这个在蛇的移动那块我们再谈;另一个是window控制台下的坐标系,基于window控制台窗口的坐标是从窗口的左上角为原点,横向为X轴,纵向为Y轴,而且单位距离下Y轴的实际长度要比X轴长,于是我们设计横纵向长度比2:1的比例(设置横向的28个口,占X轴单位长度56;纵向的28,占Y轴单位长度28),让游戏地图的布局等于正方形。

	window控制台下的坐标系

不要被上面地图墙体2:1的设置误导,误以为控制台X,Y轴单位长度是1:2,注意看下图X丶Y轴上的各个口之间是有间隙的,在X轴上间隙的长度和在Y轴上间隙的长度并不相同。

在这里插入图片描述

	void CreateMap();
//创建地图
void CreatMap()
{
	int i = 0;
	//上( 0, 0)-(56, 0)
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下( 0, 26)-(56, 26)
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左 需用SetPos定位行来设置墙体 由(  0,   1)-(   0,  25)
	// x从0开始设置过墙体了 所以y从1开始设置 设置到25处墙体正好
	for (i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右 用SetPos定位设置墙体 由(56,   1)-( 56,  25)
	//	x是56,y同样从1开始 到25
	for (i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

void InitSnake(pSnake ps)

void InitSnake(pSnake ps) 用于创建整条蛇,打印蛇体在地图内,并且初始化游戏的各项数据。
代码逻辑:

  • 初始创建5个蛇身节点,采用头插法。
  • 打印蛇体在地图中。
  • 默认蛇初始的移动方向向右,正常速度移动。
  • 初始化一部分游戏信息数据
//初始化蛇
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;
	int count = 1;
	while (cur)
	{
		if (count == 1)
		{
			count++;
			SetPos(cur->x, cur->y);
			wprintf(L"%c", HEAD);
			cur = cur->next;
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%c", BODY);
			cur = cur->next;
		}
	}

	//初始化贪吃蛇的数据
	ps->_SleepTime = 200;
	ps->_Score = 0;
	ps->_Status = OK;
	ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}

void CreateFood(pSnake ps)

void InitSnake(pSnake ps)用于创建食物
要求:

  • 食物的横坐标需是2的倍数。
//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	//参数的X坐标需是2的倍数,即与墙体不错位 
	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::malloc()");
		return;
	}
	else
	{
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%c", FOOD);
		ps->_pFood = pFood;
	}
}

void GameRun(pSnake ps)

void GameRun(pSnake ps)实现了控制台和键盘响应的交互,通过键盘的输入来控制游戏的运行以及蛇的移动,内层循环包裹实现了重复交互。
运行逻辑:

  • 打印游戏信息同时完成输入的接收
  • 实现蛇的移动-SnakeMove(ps)
	这里在输入的时候需要注意,蛇不能直接反向移动,比如蛇正在向左移动,键入右键是不管用的,因为KEY_PRESS(VK)会等于0,这属于游戏的默认规则。 
//游戏运行过程
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("您当前的游戏得分等分:%d分", ps->_Score);
		SetPos(64, 11);
		printf("每个食物可获得的分数:%d分", 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 = END_NOMAL;
			break;
		}
		//F3和F4控制加速和减速,但都有一定限度
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
				if (ps->_SleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//移动的间隙时间构成蛇的移动 时间越短 蛇的移动速度就越快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);

}

void SnakeMove(pSnake ps)

void SnakeMove(pSnake ps) 用于实现蛇体的移动。
它具体的编写逻辑是这样的:

  • 首先开辟一个临时内存作为新节点,通过移动方向的规则来给该节点赋值坐标
  • 其次它判定该节点对应的坐标是否也正是食物的坐标,根据不同的情况进行不同的操作
  • 最后判定蛇在连接这个新节点(蛇头位置更新)后是否合法,比如是否撞到了墙或自身

在这里需要说明一下蛇移动轨迹的限制了。
我们在上面是以(24, 5)为蛇头初始结点开始移动,我们使用的蛇头也好,蛇体,食物也好,都是使用的宽字符打印在地图上;墙体设置时横坐标时从0开始,每2个单位长度(x轴上)为一个墙体;那么蛇在水平方向上单次移动的单位长度应该是多少呢?

  • 这个问题答案归结于蛇体和墙体丶食物在碰到时会发生的效果。我们想要的是蛇能够在碰到食物时将整个食物吞入腹中,而不是吃掉半个;我们要求当蛇碰到墙体时游戏结束,而不是蛇头卡在一半的墙体时还可以继续游戏。
  • 这时蛇的移动轨迹限制应显而易见了,蛇头在水平方向上单次移动的距离应为2个在X轴上的单位长度,并且初始蛇头的X坐标也为偶数,食物每次出现时X坐标也为偶数,这样确保了当下一步无论是墙丶食物还是正常坐标,都不会出现卡位或游戏逻辑错误的情况,这都归功于我们规定了蛇的移动原则。
//蛇的移动
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;
		}
	}
	//上面的switch确定了下一步蛇头的坐标
	//此时需要确定下一次蛇头要达到的位置是不是食物
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}
	//同时还需要确定下一步的位置是否合法 
	//是否撞到了墙 是否撞到了自己
	KillByWall(ps);
	KillBySelf(ps);

}

int NextIsFood(pSnakeNode psn, pSnake ps)

int NextIsFood(pSnakeNode psn, pSnake ps)用于判定新节点是否为食物节点,若为食物则返回1,否则返回0;

  • 形参psn接收的实参是在SnakeMove中新开辟的节点
//在蛇移动的过程中 判断下一个结点是否为食物
int NextIsFood(pSnakeNode psn, pSnake ps)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

void EatFood(pSnakeNode psn, pSnake ps)

void EatFood(pSnakeNode psn, pSnake ps)用于执行蛇吃到食物的逻辑操作
它的具体逻辑是这样的:

  • 将食物作为新的头结点插入(头插法)
  • 在链入新节点后更新地图上的蛇体打印,并且更新游戏得分情况
  • 在打印完更新的蛇体后要消除原食物在地图上的打印,用两个空格在原食物的坐标下替代,然后要释放原食物节点
  • 最后创建出新食物,使得游戏继续

这里一定要注意:无论蛇的下一步移动是否是食物,下一步开辟的位置节点食物节点是独立的,在完成相应的操作后要注意是否需要释放内存,防止内存泄露

void EatFood(pSnakeNode psn, pSnake ps)
{
	//头插法  将食物作为头结点
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;
	//打印出更新后的蛇体
	int count = 1;
	while (cur)
	{
		if (count == 1)
		{
			count++;
			SetPos(cur->x, cur->y);
			wprintf(L"%c", HEAD);
			cur = cur->next;
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%c", BODY);
			cur = cur->next;
		}
	}
	//更新当前得分
	ps->_Score += ps->_foodWeight;
	//注意:我们在SnakeMove中创建的下一步蛇移动的位置坐标
	//		并且判定 如果与食物的坐标相同就将创建的新坐标
	//		链接到蛇身上  此时原先的食物节点开辟的内存已经
	//		没有意义了  需要释放
	//还需要将原先的尾节点打印为两个空格 否则吃了食物但还是会显示在上面
	SetPos(ps->_pFood->x, ps->_pFood->y);
	printf("  ");
	free(ps->_pFood);
	//每一次吃完食物后  都要重新创建出新的食物 让游戏可以继续
	CreateFood(ps);
}

void NoFood(pSnakeNode psn, pSnake ps)

void NoFood(pSnakeNode psn, pSnake ps)用于执行蛇的下一次移动到的坐标不是食物的操作
它的操作逻辑是这样的:

  • 将下一步坐标对应的内存节点链接到蛇头(头插法)
  • 将新的蛇体的尾节点对应在地图上的打印消除,同时释放尾节点
  • 在地图上打印出更新后的蛇体
	我这里是先打印后释放的,只要注意释放前消除相应的地图信息即可。
//蛇不吃食物
void NoFood(pSnakeNode psn, pSnake ps)
{
	//同样的逻辑 进行头插法  对于不是食物的节点
	//要进行蛇头移动 同时释放尾节点
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	//打印蛇体
	pSnakeNode cur = ps->_pSnake;
	int count = 1;
	while (cur->next->next)
	{
		if (count == 1)
		{
			count++;
			SetPos(cur->x, cur->y);
			wprintf(L"%c", HEAD);
			cur = cur->next;
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%c", BODY);
			cur = cur->next;
		}
	}
	SetPos(cur->next->x, cur->next->y);
	printf(" ");
	free(cur->next);
	cur->next = NULL;		
}

int KillByWall(pSnake ps)

int KillByWall(pSnake ps)用于蛇的撞墙检测
执行逻辑:

  • 当蛇头x或y坐标任一个与墙体重合时,代表蛇头移动到了墙体内
  • 修改当前的游戏状态,退出子程序KillByWall
//撞墙检测
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;
		Sleep(1000);
		return 1;
	}
	return 0;
}

int KillBySelf(pSnake ps)

int KillBySelf(pSnake ps)用于蛇的撞墙检测
执行逻辑:

  • 当蛇头坐标与任一个蛇体节点坐标重合时,代表蛇撞到了自身
  • 修改当前的游戏状态,退出子程序KillBySelf

结合实际情况容易知道,蛇头只可能从第四个蛇体节点及往后节点重合(从蛇头开始数,蛇头本身也算蛇体节点)

	这里为了简便,直接从第二个节点开始检测了
int KillBySelf(pSnake ps)
{
	//cur是遍历整条蛇的临时节点
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		//ps->_pSnake是蛇头 从第二个蛇体节点开始检测是否重合
		if ((ps->_pSnake->x == cur->x)
			&& (ps->_pSnake->y == cur->y))
		{
			ps->_Status = KILL_BY_SELF;
			Sleep(1000);
			return 1;
		}
		cur = cur->next;
	}
	return 0;
}

void GameEnd(pSnake ps)

void GameEnd(pSnake ps)用于判定游戏结束属于何种情况

  • 实际只有两个结束的情况:撞到墙体和撞到自身,主动退出游戏的相应函数没有实现,相应的case语句可以忽略
  • 释放蛇的所有结点,释放当前的食物结点,此时游戏结束,将进入用户输入选择
//游戏结束
void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙上,游戏结束!\n");
		break;
	}

	while (cur)
	{
		pSnake del = cur;
		cur = cur->next;
		free(del);
	}
	
	free(ps->_pFood);
	ps->_pFood = NULL;
}

void SetPos(short x, short y)

void SetPos(short x, short y)用于定位控制台的光标坐标

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

void pause()

void pause()实现空格暂停响应操作

//暂停响应
void pause()
{
	while (1)
	{
		//相当于每0.3秒检测一次空格键的响应
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

五丶 源代码

1.snake.h

snake.h

#pragma once

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

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

//方向
enum DIRECION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//游戏状态
enum GAME_STATUS
{
	OK,
	KILL_BY_WALL,
	KILL_BY_SELF,
	END_NOMAL
};

//墙 蛇体结点和食物的符号
#define WALL L'口'
#define HEAD L'♛'
#define BODY L'⬤'
#define FOOD L'★'

//蛇出现的初始位置
#define POS_X 24
#define POS_Y 5

//蛇身结点
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 GameStart(pSnake ps);

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

//游戏结束
void GameEnd(pSnake ps);

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

//欢迎界面
void WelcomeToGame();

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

//创建地图
void CreatMap();

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

//创建食物
void CreateFood(pSnake ps);

//暂停响应
void pause();

//在蛇移动的过程中 判断下一个结点是否为食物
int NextIsFood(pSnakeNode psn, pSnake ps);

//蛇吃食物
void EatFood(pSnakeNode psn, pSnake ps);

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

//撞墙检测
int KillByWall(pSnake ps);

//撞到自身检测
int KillBySelf(pSnake ps);

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

2.snake.c

snake.c

#include "snake.h"

//游戏开始前的初始化
void GameStart(pSnake ps)
{
	//用system系统函数设置控制台尺寸和窗口名称
	//mode 为DOS命令
	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();

	//打印地图
	CreatMap();

	//初始蛇的相关数据
	InitSnake(ps);
	
	//创造第一个食物
	CreateFood(ps);
}

//游戏运行过程
void GameRun(pSnake ps)
{
	PrintHelpInfo();
	do
	{
		SetPos(64, 10);
		printf("您当前的游戏得分等分:%d分", ps->_Score);
		SetPos(64, 11);
		printf("每个食物可获得的分数:%d分", 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 = END_NOMAL;
			break;
		}
		else if (KEY_PRESS(VK_F3))
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;
				ps->_foodWeight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;
				ps->_foodWeight -= 2;
				if (ps->_SleepTime == 350)
				{
					ps->_foodWeight = 1;
				}
			}
		}
		//移动的间隙时间构成蛇的移动 时间越短 蛇的移动速度就越快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);
	} while (ps->_Status == OK);

}

//游戏结束
void GameEnd(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	SetPos(24, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您主动退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞上自己了,游戏结束!\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙上,游戏结束!\n");
		break;
	}

	while (cur)
	{
		pSnake del = cur;
		cur = cur->next;
		free(del);
	}
	
	free(ps->_pFood);
	ps->_pFood = NULL;
}

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

//欢迎界面
void WelcomeToGame()
{
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(40, 25);
	system("pause");
	system("cls");
	SetPos(25, 12);
	printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速");
	SetPos(25, 13);
	printf("加速将能得到更高的分数。\n");
	SetPos(40, 25);
	system("pause");
	system("cls");

}

//打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 15);
	printf("不能穿墙,不能咬到自己");
	SetPos(64, 16);
	printf("⽤↑.↓.←.→分别控制蛇的移动.");
	SetPos(64, 17);
	printf("F3 为加速, F4 为减速");
	SetPos(64, 18);
	printf("ESC :退出游戏.space:暂停游戏.");
	SetPos(64, 20);
	printf("加油 干巴嘚!");

}

//创建地图
void CreatMap()
{
	int i = 0;
	//上( 0, 0)-(56, 0)
	SetPos(0, 0);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//下( 0, 26)-(56, 26)
	SetPos(0, 26);
	for (i = 0; i < 58; i += 2)
	{
		wprintf(L"%c", WALL);
	}
	//左 需用SetPos定位行来设置墙体 
	// x从0开始设置过墙体了 所以y从1开始设置 设置到25处墙体正好
	for (i = 1; i < 26; ++i)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//右 用SetPos定位设置墙体
	//	x是56,y同样从1开始 到25
	for (i = 1; i < 26; ++i)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}

}

//初始化蛇
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;
	int count = 1;
	while (cur)
	{
		if (count == 1)
		{
			count++;
			SetPos(cur->x, cur->y);
			wprintf(L"%c", HEAD);
			cur = cur->next;
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%c", BODY);
			cur = cur->next;
		}
	}

	//初始化贪吃蛇的数据
	ps->_SleepTime = 200;
	ps->_Score = 0;
	ps->_Status = OK;
	ps->_Dir = RIGHT;
	ps->_foodWeight = 10;
}

//创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;

again:
	//产生的左边必须是2的倍数  否则当蛇头撞墙时会出现卡位现象
	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::malloc()");
		return;
	}
	else
	{
		pFood->x = x;
		pFood->y = y;
		SetPos(pFood->x, pFood->y);
		wprintf(L"%c", FOOD);
		ps->_pFood = pFood;
	}
}

//暂停响应
void pause()
{
	while (1)
	{
		Sleep(300);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//在蛇移动的过程中 判断下一个结点是否为食物
int NextIsFood(pSnakeNode psn, pSnake ps)
{
	return (psn->x == ps->_pFood->x) && (psn->y == ps->_pFood->y);
}

//蛇吃食物
void EatFood(pSnakeNode psn, pSnake ps)
{
	//头插法  将食物作为头结点
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	pSnakeNode cur = ps->_pSnake;
	//打印出更新后的蛇体
	int count = 1;
	while (cur)
	{
		if (count == 1)
		{
			count++;
			SetPos(cur->x, cur->y);
			wprintf(L"%c", HEAD);
			cur = cur->next;
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%c", BODY);
			cur = cur->next;
		}
	}
	//更新当前得分
	ps->_Score += ps->_foodWeight;
	//注意:我们在SnakeMove中创建的下一步蛇移动的位置坐标
	//		并且判定 如果与食物的坐标相同就将创建的新坐标
	//		链接到蛇身上  此时原先的食物节点开辟的内存已经
	//		没有意义了  需要释放
	//还需要将原先的尾节点打印为两个空格 否则吃了食物但还是会显示在上面
	SetPos(ps->_pFood->x, ps->_pFood->y);
	printf("  ");
	free(ps->_pFood);
	//每一次吃完食物后  都要重新创建出新的食物 让游戏可以继续
	CreateFood(ps);
}

//蛇不吃食物
void NoFood(pSnakeNode psn, pSnake ps)
{
	//同样的逻辑 进行头插法  对于不是食物的节点
	//要进行蛇头移动 同时释放尾节点
	psn->next = ps->_pSnake;
	ps->_pSnake = psn;
	//打印蛇体
	pSnakeNode cur = ps->_pSnake;
	int count = 1;
	while (cur->next->next)
	{
		if (count == 1)
		{
			count++;
			SetPos(cur->x, cur->y);
			wprintf(L"%c", HEAD);
			cur = cur->next;
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%c", BODY);
			cur = cur->next;
		}
	}
	//在连接新节点后需要将尾节点释放 同时要消除尾节点在屏幕上的显示
	//先找到未节点坐标 将其打印为两个空格
	SetPos(cur->next->x, cur->next->y);
	printf(" ");
	//释放  指针置空
	free(cur->next);
	cur->next = NULL;			
}

//撞墙检测
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;
		Sleep(1000);
		return 1;
	}
	return 0;
}

//撞到自身检测
int KillBySelf(pSnake ps)
{
	//cur是遍历整条蛇的临时节点
	pSnakeNode cur = ps->_pSnake->next;
	while (cur)
	{
		//ps->_pSnake是蛇头 从第二个蛇体节点开始检测是否重合
		if ((ps->_pSnake->x == cur->x)
			&& (ps->_pSnake->y == cur->y))
		{
			ps->_Status = KILL_BY_SELF;
			Sleep(1000);
			return 1;
		}
		cur = cur->next;
	}
	return 0;
}

//蛇的移动
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;
		}
	}
	//上面的switch确定了下一步蛇头的坐标
	//此时需要确定下一次蛇头要达到的位置是不是食物
	if (NextIsFood(pNextNode, ps))
	{
		EatFood(pNextNode, ps);
	}
	else
	{
		NoFood(pNextNode, ps);
	}
	//同时还需要确定下一步的位置是否合法 
	//是否撞到了墙 是否撞到了自己
	KillByWall(ps);
	KillBySelf(ps);

}

3.test.c

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "snake.h"
#include <locale.h>

void test()
{
	int ch = 0;
	srand((unsigned int)time(NULL));

	do
	{
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);
		SetPos(20, 15);
		printf("再来一局?(Y/N):");
		ch = getchar();
		getchar();
	} while (ch == 'Y'||ch == 'y');
	//此光标设置是控制好控制台打印程序结束信息的位置
	SetPos(0, 27);
	
}


int main()
{
	setlocale(LC_ALL, "");

	test();
	
	return 0;
}

本博客仅供个人参考,如有错误请多多包含。
Aruinsches-项目日志-在VS环境下利用C语言基本实现贪吃蛇-3/30/2024
  • 38
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值