贪吃蛇(C)

        游戏背景:贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。

       总: 游戏设计大纲

        使⽤C语⾔Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇。

        实现的基本功能

        1、贪吃蛇地图绘制。

        2、蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)。

        3、 蛇撞墙死亡。

        4、蛇撞⾃⾝死亡。

        5、 计算得分。

        6、 蛇⾝加速、减速。

        7、 暂停游戏、退出游戏。

        一、Win32 API介绍

        1、1win32 API

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

        下面我们用vs2022演示,需要包含头文件<windows.h>。

        以下函数都是在window.h中的,我们只需要用即可。 

        1、2控制台程序

        平常我们运⾏起来的⿊框程序其实就是控制台程序。我们可以设计窗口的大小和title

        system在<stdlib.h>中

	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

        1、3控制台屏幕上的坐标COORD

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

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

        我们就可以控制一个字符在控制台屏幕上出现的位置。

COORD pos = { 10, 15 };

        1、4 GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle);

        这里我们使用标准输出STD_OUTPUT_HANDLE

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

        1、5 GetConsoleCursorInfo(获取光标信息)

        检索(查看)有关指定控制台屏幕缓冲区的光标⼤⼩可⻅性的信息。

BOOL WINAPI GetConsoleCursorInfo(
 HANDLE hConsoleOutput,
 PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

        使用:

HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息

        1、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。(我们会把它设为false,来让光标不在屏幕上出现)。

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

        1、6 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);//设置控制台光标状态

        1、7 SetConsoleCursorPosition

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

BOOL WINAPI SetConsoleCursorPosition(
 HANDLE hConsoleOutput,
 COORD pos
);

        使用:

COORD pos = { 10, 5};
 //获取标准输出的句柄(⽤来标识不同设备的数值)
 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
 //设置标准输出上光标的位置为pos
 SetConsoleCursorPosition(handle, pos);

        1、8将以上所对光标进行的操作进行封装SetPos()

        封装⼀个设置光标位置的函数

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

        想要对光标进行其他操作,也可以加入里面,这里只是举例。 

        1、9 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 )

        二、地图绘制

        这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道 该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。 控制台窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓

        2、1宽字符 

         在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。为什么我们要使用宽字符呢?我们在控制台可以看见,x轴的2长度才和y轴的1长度相当,所以我们为了让界面好看工整,我们使用宽字符。

        为了使C语⾔适应国家化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊和宽字符的类型 wchar_t 宽字符的输⼊和输出函数,加⼊<locale.h>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

        2、1、1<locale.h>本地化

        <locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。 在标准可以中,依赖地区的部分有以下⼏项:

        数字量的格式 , 货币量的格式 , 字符集 , ⽇期和时间的表⽰形式

        2、1、2 类项

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

        LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()

        LC_CTYPE:影响字符处理函数的行为

        LC_MONETARY:影响货币格式

        LC_NUMERIC:影响printf()的数字格式

        LC_TIME:影响时间格式strftime()和wcsftime()

        LC_ALL - 针对所有类项修改。将以上所有类别设置为给定的语言环境。

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

        2、1、4 宽字符打印

        那如果想在屏幕上打印宽字符,怎么打印呢?

        宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类项处理。前缀“L”在单引号前,表示宽字符,对应wprintf()的占位符为%lc;“L“在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls

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

        2、2地图坐标

        我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙。最终行= 25,列 = 54 = 宽字符27列。(x轴的长度一定得是2的倍数,否则由于宽字符的原因,后面可能会出现蛇身的某个结点或者食物一半在内,一半在墙上的问题。)

         2、3蛇身和食物

        初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现 蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数(这里指蛇的每个结点的左边一定得对应x上2的倍数),否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。

        关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。

        三、数据结构的设计

        在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏, 所以舍蛇节点结构如下:

typedef struct SnakeNode
{
	int x;//(x,y)坐标
	int y;
	struct SnakeNode* next;//蛇身结点的下一个结点
}SnakeNode, *pSnakeNode;
//*pSnakeNode 等价于 typedef struct SnakeNode* pSnakeNode
// 则pSnakeNode = SnakeNode*
//即也定义结构体指针

        要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

//方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
//蛇的状态
enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//ESC退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//撞到自身
};
//整个游戏过程都在维护这条蛇,即多个结点构成的链表
typedef struct Snake
{
	pSnakeNode _pSnakeHead;//指向蛇的头结点的指针
	pSnakeNode _pFood;//指向食物的指针
	//食物本质上也就是蛇身的结点,只不过打印方式不同

	int _CurrentScore;//记录当前分数
	int _FoodScore;//记录每个食物多少分,默认十分
	int _SleepTime;//每走一步的休眠时间,即蛇走的速度
	//休眠越少,速度越快;反之越慢。

	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//游戏状态:正常,退出,撞墙,撞到自己
}Snake, *pSnake;

        四、游戏流程

        

        五、代码实现

        5、1Snake.h

#define _CRT_SECURE_NO_WARNINGS -1
#pragma once 
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<windows.h>
#include<locale.h>
#include<time.h>

#define WALL "□"
#define BODY "●"
#define FOOD "★"

#define BEGIN_X 24
#define BEGIN_Y 5

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

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

//蛇的状态
enum GAME_STATUS
{
	OK,//正常运行
	END_NORMAL,//ESC退出
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF//撞到自身
};

typedef struct SnakeNode
{
	int x;//(x,y)坐标
	int y;
	struct SnakeNode* next;//蛇身结点的下一个结点
}SnakeNode, *pSnakeNode;
//*pSnakeNode 等价于 typedef struct SnakeNode* pSnakeNode
// 则pSnakeNode = SnakeNode*
//即也定义结构体指针


//整个游戏过程都在维护这条蛇,即多个结点构成的链表
typedef struct Snake
{
	pSnakeNode _pSnakeHead;//指向蛇的头结点的指针
	pSnakeNode _pFood;//指向食物的指针
	//食物本质上也就是蛇身的结点,只不过打印方式不同

	int _CurrentScore;//记录当前分数
	int _FoodScore;//记录每个食物多少分,默认十分
	int _SleepTime;//每走一步的休眠时间,即蛇走的速度
	//休眠越少,速度越快;反之越慢。

	enum DIRECTION _Dir;//描述蛇的方向
	enum GAME_STATUS _Status;//游戏状态:正常,退出,撞墙,撞到自己
}Snake, *pSnake;


//----游戏开始---- - 完成游戏初始化
void GameStart(pSnake psnake);

//游戏欢迎界面
void WelComeToGame();
//定位光标
void SetPos(short x, short y);
//打印地图
void CreatMap();
//初始化蛇
void InitSnake(pSnake psnake);
//创建食物
void CreatFood(pSnake psnake);



//----游戏运行---- - 游戏的正常运行过程
void GameRun(pSnake psnake);

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

//游戏暂停和恢复
void Pause();

//蛇动起来
void SnakeMove(pSnake psnake);

//判断移动之后是否是食物
bool NextIsFood(pSnake psnake, pSnakeNode pnext);

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

//撞墙
void KillByWall(pSnake psnake);

//撞到自己
void KillBySelf(pSnake psnake);




//-----游戏结束---- - 善后工作(释放资源)
void GameEnd(pSnake psnake);

        5、2Snake.c

#include"snake.h"
//----游戏开始---- - 完成游戏初始化

//定位光标
void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle_out, pos);//设置光标位置
}

//打印欢迎界面
void WelComeToGame()
{
	//定位光标
	//来到中间,打印”欢迎....“
	SetPos(40,15);
	printf("欢迎来到贪吃蛇小游戏\n");
	SetPos(40, 25);
	system("pause");//暂停命令

	system("cls");//清理屏幕信息
	SetPos(25, 15);
	printf("使用 ↑ ↓ ← → 分别控制蛇的移动,F3是加速,F4是减速");
	SetPos(40, 25);
	system("pause");
	system("cls");

}

//打印地图
void CreatMap()
{
	//打印 col = 58,row = 27的棋盘
	//宽字符横向占两个字节
	//上:[0,0]到[56,0],下:[0,26]到[56,26]
	//左:[0,1]到[0,25],右:[56,1]到[56,25]

	//上
	SetPos(0, 0);
	for (int i = 0; i <= 56; i += 2)
	{
		printf(WALL);
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i <= 56; i += 2)
	{
		printf(WALL);
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		printf(WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		printf(WALL);
	}
	SetPos(60, 25);
	//system("pause");
}

//初始化蛇
void InitSnake(pSnake psnake)
{
	pSnakeNode cur = NULL;
	//默认开始蛇有五个结点
	for (int i = 0; i < 5; i++)
	{
		pSnakeNode cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake malloc fail\n");
			exit(-1);
		}
		cur->x = BEGIN_X+2*i, cur->y = BEGIN_Y;//横着连续放置每个结点
		cur->next = NULL;

		//来一个单链表头插法,将“蛇头”指到最右边那个
		if (psnake->_pSnakeHead == NULL)
		{
			psnake->_pSnakeHead = cur;
		}
		else
		{
			cur->next = psnake->_pSnakeHead;
			psnake->_pSnakeHead = cur;
		}
	}

	//打印蛇身
	cur = psnake->_pSnakeHead;
	while (cur != NULL)
	{
		SetPos(cur->x, cur->y);
		printf(BODY);
		cur = cur->next;
	}

	psnake->_Status = OK;
	psnake->_CurrentScore = 0;
	psnake->_FoodScore = 10;
	psnake->_pFood = NULL;
	psnake->_SleepTime = 200;//200ms
	psnake->_Dir = RIGHT;//因为上面的蛇初始化,所以这里蛇初始向右。
	
	SetPos(60, 25);
	//system("pause");
}

//创建食物
void CreatFood(pSnake psnake)
{
	//坐标应该是随机生成,但是随机是有约束的
	//首先不能出墙,其次x坐标必须是2的倍数,最后食物不能和蛇的身体冲突
	int x = 0;
	int y = 0;
	
	//墙中间范围是:x[2,54],y[1,25]
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);//x的坐标必须是2的倍数

	//坐标不能和蛇身冲突
	pSnakeNode cur = psnake->_pSnakeHead;
	while (cur != NULL)
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;//冲突就重新执行
		}
		cur = cur->next;
	}

	pSnakeNode pFoodNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFoodNode == NULL)
	{
		perror("CreatFood malloc fail\n");
		exit(-1);
	}
	pFoodNode->x = x, pFoodNode->y = y;
	pFoodNode->next = NULL;
	psnake->_pFood = pFoodNode;

	SetPos(x, y);
	printf(FOOD);

	SetPos(60, 25);
	//system("pause");
}

void GameStart(pSnake psnake)
{
	//控制台窗口的设置
	system("mode con cols=100 lines=30");
	system("title 贪吃蛇");

	//隐藏光标
	HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE);//获得句柄
	CONSOLE_CURSOR_INFO CursorInfo;
	GetConsoleCursorInfo(handle_out, &CursorInfo);//获取光标信息
	CursorInfo.bVisible = false;//隐藏光标
	SetConsoleCursorInfo(handle_out, &CursorInfo);//设置光标状态

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

	//创建地图
	CreatMap();

	//初始化蛇
	InitSnake(psnake);

	//创建食物
	CreatFood(psnake);
}





//----游戏运行---- - 游戏的正常运行过程

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

	SetPos(60, 25);
	//system("pause");
}

//游戏暂停和恢复
void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//判断移动之后是否是食物
bool NextIsFood(pSnake psnake, pSnakeNode pnext)
{
	if (psnake->_pFood->x == pnext->x && psnake->_pFood->y == pnext->y)
		return true;
	return false;
}

//吃掉食物
void EatFood(pSnake psnake, pSnakeNode pnext)
{
	//头插
	pnext->next = psnake->_pSnakeHead;
	psnake->_pSnakeHead = pnext;
	//打印蛇
	pSnakeNode cur = psnake->_pSnakeHead;
	while (cur != NULL)
	{
		SetPos(cur->x, cur->y);
		printf(BODY);
		cur = cur->next;
	}

	//释放食物结点
	free(psnake->_pFood);
	//加成绩
	psnake->_CurrentScore += psnake->_FoodScore;
	//新创建食物
	CreatFood(psnake);
}
//不吃食物
void NotEatFood(pSnake psnake, pSnakeNode pnext)
{
	//头插
	pnext->next = psnake->_pSnakeHead;
	psnake->_pSnakeHead = pnext;

	//打印蛇身,不打印最后一个结点,并找到最后一个结点
	pSnakeNode cur = psnake->_pSnakeHead;
	while (cur->next->next != NULL)
	{
		SetPos(cur->x, cur->y);
		printf(BODY);
		cur = cur->next;
	}
	//将原本最后一个结点处的身体打印为空格
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	//释放最后一个结点
	free(cur->next);
	cur->next = NULL;
}

//撞墙
void KillByWall(pSnake psnake)
{
	//撞墙
	if (psnake->_pSnakeHead->x == 0 || psnake->_pSnakeHead->x == 56
		|| psnake->_pSnakeHead->y == 0 || psnake->_pSnakeHead->y == 26)
		psnake->_Status = KILL_BY_WALL;
}

//撞到自己
void KillBySelf(pSnake psnake)
{
	//蛇头与蛇身的坐标重合就是撞到了
	pSnakeNode cur = psnake->_pSnakeHead->next;
	while (cur != NULL)
	{
		if (psnake->_pSnakeHead->x == cur->x && psnake->_pSnakeHead->y == cur->y)
		{
			psnake->_Status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

//蛇动起来
void SnakeMove(pSnake psnake)
{
	//创建移动的下一个位置结点
	pSnakeNode pnext = (pSnakeNode)malloc(sizeof(SnakeNode));
	{
		if (pnext == NULL)
		{
			perror("pnext malloc fail\n");
			exit(-1);
		}
	}
	pnext->next = NULL;

	switch (psnake->_Dir)
	{
	case UP:
		pnext->x = psnake->_pSnakeHead->x;
		pnext->y = psnake->_pSnakeHead->y - 1;
		break;
	case DOWN:
		pnext->x = psnake->_pSnakeHead->x;
		pnext->y = psnake->_pSnakeHead->y + 1;
		break;
	case LEFT:
		pnext->x = psnake->_pSnakeHead->x - 2;
		pnext->y = psnake->_pSnakeHead->y;
		break;
	case RIGHT:
		pnext->x = psnake->_pSnakeHead->x + 2;
		pnext->y = psnake->_pSnakeHead->y;
		break;
	}

	//判断移动之后,是否是食物,或者撞墙,或者撞到自己

	//食物
	if (NextIsFood(psnake, pnext))
	{
		//吃掉食物
		EatFood(psnake,pnext);
	}
	else
	{
		//不吃
		NotEatFood(psnake,pnext);
	}
	
	//撞墙
	KillByWall(psnake);

	//撞到自己
	KillBySelf(psnake);

}


void GameRun(pSnake psnake)
{
	//右侧打印帮助信息
	PrintHelpInfo();

	//打印当前已经获得分数和每个食物的分数
	//并检测按键
	do
	{
		SetPos(66, 10);
		printf("得分:%05d", psnake->_CurrentScore);
		SetPos(66, 11);
		printf("每个食物分数:%2d", psnake->_FoodScore);
	
		//检测按键
		//按上时,并且蛇前一秒并不是往下走,就可以向上走;其他也一样
		if (KEY_PRESS(VK_UP) && psnake->_Dir != DOWN)
		{
			psnake->_Dir = UP;
		}
		else if (KEY_PRESS(VK_DOWN) && psnake->_Dir != UP)
		{
			psnake->_Dir = DOWN;
		}
		else if (KEY_PRESS(VK_LEFT) && psnake->_Dir != RIGHT)
		{
			psnake->_Dir = LEFT;
		}
		else if (KEY_PRESS(VK_RIGHT) && psnake->_Dir != LEFT)
		{
			psnake->_Dir = RIGHT;
		}
		else if (KEY_PRESS(VK_ESCAPE))//ESC
		{
			psnake->_Status = END_NORMAL;
			break;
		}
		else if (KEY_PRESS(VK_SPACE))//空格
		{
			Pause();
		}
		else if (KEY_PRESS(VK_F3))//F3,加速
		{
			if (psnake->_SleepTime>=80)//最快50,食物最高分20
			{
				psnake->_SleepTime -= 30;//每次缩短20ms
				psnake->_FoodScore += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))//F4,减速
		{
			if (psnake->_SleepTime < 320)//最慢320,食物最低分2
			{
				psnake->_SleepTime += 30;
				psnake->_FoodScore -= 2;
			}
		}
		//蛇休眠(暂停)
		Sleep(psnake->_SleepTime);
		//蛇动起来
		SnakeMove(psnake);
	
	} while (psnake->_Status == OK);
}


//-----游戏结束---- - 善后工作(释放资源)
void GameEnd(pSnake psnake)
{
	SetPos(20, 12);
	switch (psnake->_Status)
	{
	case END_NORMAL:
		printf("您主动退出游戏,Game Over");
		break;
	case KILL_BY_SELF:
		printf("游戏自杀,Game Over");
		break;
	case KILL_BY_WALL:
		printf("对不起,您撞墙了,Game Over");
		break;
	}


	SetPos(0, 27);
	//释放蛇身的结点
	pSnakeNode cur = psnake->_pSnakeHead;
	while (cur != NULL)
	{
		pSnakeNode tmp = cur->next;
		free(cur);
		cur = tmp;
	}
	free(cur);
	psnake->_pSnakeHead = NULL;
}

        5、3Test.c

#include"snake.h"

void Test()
{
	int ch = 0;
	do
	{
		//创建贪吃蛇
		Snake snake = { 0 };

		//1、游戏开始 - 初始化游戏
		GameStart(&snake);

		//2、游戏运行 - 游戏的正常运行过程
		GameRun(&snake);

		//3、游戏结束 - 善后工作(释放资源)
		GameEnd(&snake);

		SetPos(20, 14);
		printf("再来一次吗?[ Y / N ]:");
		ch = getchar();
		getchar();//清理掉\n
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);

}


int main()
{
	setlocale(LC_ALL, "");//本地化

	srand((unsigned int)time(NULL));
	Test();
	return 0;
}

        六、总结

        贪吃蛇游戏作为经典的小游戏,实现难度并不是很大。不过这个作为我们今后设计项目来训练思维也是非常不错的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/342110.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Whale 帷幄创始人叶生晅荣获亿欧 2023 中国泛人工智能优秀人物 TOP 20

近日&#xff0c;亿欧在 WIM 2023&#xff08;World Innovators Meet&#xff0c;世界创新者年会&#xff09;上发布 2023 世界创新奖「2023 中国泛人工智能优秀人物 TOP 20」&#xff0c;表彰那些过去一年中在泛人工智能领域做出突出贡献的领导者、开拓者。「Whale 帷幄」创始…

用ChatGPT教学、科研!亚利桑那州立大学与OpenAI合作

亚利桑那州立大学&#xff08;简称“ASU”&#xff09;在官网宣布与OpenAI达成技术合作。从2024年2月份开始&#xff0c;为所有学生提供ChatGPT企业版访问权限&#xff0c;主要用于学习、课程作业和学术研究等。 为了帮助学生更好地学习ChatGPT和大语言模型产品&#xff0c;AS…

图像分割实战-系列教程18:MaskRCNN项目介绍与配置

&#x1f341;&#x1f341;&#x1f341;图像分割实战-系列教程 总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在Pycharm中进行 本篇文章配套的代码资源已经上传 Mask R-CNN for Object Detection and Segmentation MaskRCNN是一个通用的物体检测框架&#xff…

MySQL学习(1):centos7安装MySQL

1.安装自己系统对应的MySQL版本 1.1查看自己系统的内核版本 cat /etc/redhat-release 可以看到我的系统版本是centos7.6 1.2去官网下载对应的MySQL安装文件 MySQL官网&#xff1a; https://dev.mysql.com/downloads/ 点击MYSQL Community Server 然后可以在索引的位置选…

PWM调光 降压恒流LED芯片FP7127:为照明系统注入新能量(台灯、GBR、调光电源、汽车大灯)

目录 一、降压恒流LED芯片FP7127 二、降压恒流LED芯片FP7127具有以下特点&#xff1a; 三、降压恒流LED芯片FP7127应用领域&#xff1a; LED照明和调光的新纪元随着LED照明技术的不断发展&#xff0c;人们对于照明调光的需求也越来越高。PWM调光技术作为一种常用的调光方法&…

获取货币供应量

用bs库&#xff1a; import baostock as bs import pandas as pd# 登陆系统 lg bs.login() # 显示登陆返回信息 print(login respond error_code:lg.error_code) print(login respond error_msg:lg.error_msg)# 获取货币供应量 rs bs.query_money_supply_data_month(start_…

App各大应用商城的排名被哪些因素影响着?(小米/vivo篇)

小米&#xff1a; ①关键词设置&#xff1a; 小米应用商店允许在后台设置关键词&#xff0c;8个关键词&#xff0c;每个词不超过5个字&#xff0c;权重从左到右逐渐降低。 关键词内最好不要填写应用名称里面已有的关键词&#xff0c;不叠加权重&#xff0c;浪费位置。 ②应…

5G+物联网:连接万物,重塑智慧社区,开启未来生活新纪元,助力智慧社区的革新与发展

一、5G与物联网&#xff1a;技术概述与基础 随着科技的飞速发展&#xff0c;第五代移动通信技术&#xff08;5G&#xff09;和物联网&#xff08;IoT&#xff09;已经成为当今社会的热门话题。这两项技术作为现代信息社会的核心基础设施&#xff0c;正深刻地改变着人们的生活和…

宿舍安全用电监模块

学校宿舍安全用电监测模块是针对 0.4kV 以下的 TT、TN 系统设计的智能电力装置&#xff0c;具有单、三相交流电测量、四象限电能计量、谐波分析、开关量输入、继电器输出功能&#xff0c;以及 RS485 通讯或 GPRS 无线通讯功能&#xff0c;通过对配电回路的剩余电流、导线温度等…

教师转行适合做什么工作

当教师转型成为社会话题时&#xff0c;无数同仁都开始思考&#xff1a;我要转行吗&#xff1f;转到哪里去呢&#xff1f;作为一位曾经的教师&#xff0c;我想说&#xff0c;转行不是盲目地跳出教育界&#xff0c;而是基于自身优势和兴趣的理性选择。 作为教师&#xff0c;我们…

k8s集群异常恢复

前提、我自己的k8s采用的是单master节点两个从节点部署&#xff0c;我针对单master情况进行恢复说明 场景一&#xff1a;正常开关虚拟机&#xff0c;可直接重启kubelet进行恢复 1、1、一般重启后三个节点都需要检查&#xff0c;输入命令检查kubelet&#xff1a; systemctl s…

gitlab设置/修改克隆clone地址端口

最近由于公司要停测试库云服务器? 什么?要停测试库服务器??? 是的! 你没听错。 真是醉了,多大的集团,为了省钱,也真是拼了, 作为开发人员,没有测试服务器,犹如断臂之人。 所以,在之前搭建环境的时候都没有写文档,今天算是弥补上,以后都可以作为参考了, …

数据结构:完全二叉树(递归实现)

如果完全二叉树的深度为h&#xff0c;那么除了第h层外&#xff0c;其他层的节点个数都是满的&#xff0c;第h层的节点都靠左排列。 完全二叉树的编号方法是从上到下&#xff0c;从左到右&#xff0c;根节点为1号节点&#xff0c;设完全二叉树的节点数为sum&#xff0c;某节点编…

C++提高编程——STL:string容器、vector容器

本专栏记录C学习过程包括C基础以及数据结构和算法&#xff0c;其中第一部分计划时间一个月&#xff0c;主要跟着黑马视频教程&#xff0c;学习路线如下&#xff0c;不定时更新&#xff0c;欢迎关注。 当前章节处于&#xff1a; ---------第1阶段-C基础入门 ---------第2阶段实战…

计算机网络-物理层基本概念(接口特性 相关概念)

文章目录 总览物理层接口特性星火模型给出的相关概念解释&#xff08;仅供参考&#xff09; 总览 求极限传输速率&#xff1a;奈氏准则&#xff0c;香农定理&#xff08;背景环境不一样&#xff09; 编码&#xff1a;数据变成数字信号 调制&#xff1a;数字信号变成模拟信号 信…

得帆信息连续两年荣获“最佳企业级低代码”称号

近日&#xff0c;由政企市场专业媒体企业网D1Net、信众智(CIO智力输出及社交平台)和中国企业数字化联盟共同举办的“2023 CEIA中国企业IT大奖”重磅揭晓。 得帆信息凭借近年来在独立低代码市场的第一占有率、超过500家头部大型企业的实际实践经验、持续向上的产品能力&#xff…

【网络安全 -> 防御与保护】信息安全概述

目录 一、信息安全现状及挑战 二、信息安全脆弱性及常见安全攻击 1、网络环境的开放性 2、协议栈的脆弱性及常见攻击 3、操作系统的脆弱性及常见攻击 4、终端的脆弱性及常见攻击 5、其他常见攻击 三、信息安全要素 四、整体安全解决方案 一、信息安全现状及挑战 &…

Linux切换jdk版本

参考文献&#xff1a;Linux 多个JDK的版本 脚本切换 - C小海 - 博客园 (cnblogs.com)

TeamViewer的安装教程和使用方法

第一步&#xff1a;进官网下载软件 点击打开官网&#xff1a;https://www.teamviewer.cn/cn/ 注&#xff1a;如果你是控制端&#xff08;控制其他电脑&#xff09;&#xff0c;就顺便注册一个账号&#xff0c;必须有账号才能控制其他电脑。被控制端不用注册账号。 非商用的情…

Python源码49:海龟画图turtle画美国旗

---------------turtle源码集合--------------- Python教程91&#xff1a;关于海龟画图&#xff0c;Turtle模块需要学习的知识点 Python源码45&#xff1a;海龟画图turtle画雪容融 Python源码44&#xff1a;海龟画图turtle&#xff0c;画2022卡塔尔世界杯吉祥物 Python教程…
最新文章