【C语言】贪吃蛇游戏超详解(包含音效、颜色、封装成应用等)

【C语言】贪吃蛇游戏超详解(包含音效、颜色、封装成应用等)

前言

往期,我们设计了一个扫雷游戏

详见此处:
【C语言】扫雷游戏详解(包含递归,变色,记录时间等)

现在学了自定义函数、枚举类型、数据结构、动态内存管理

(完成贪吃蛇要具备的基础知识)

我们可以着手来设计一个经典游戏贪吃蛇项目来实战

本文内容较长,建议收藏慢慢看

代码内均有注释可查看

最终代码放在结尾(大家自行复制)
(注意:建议简单看看目录,有些设置要改)

项目演示

现在给大家看看成品演示:

贪吃蛇的游戏演示

控制台设置更改(必要!!!)

需要注意
小编用是在VS2022中运行
且游戏展示在Windows 控制台主机中(不是Windows终端!!!)(不然显示会错误)

控制台设置更改:

1.右击窗口进行设置

2. 将默认终端应用程序改为Windows 控制台主机,并保存退出

一、Win 32 API

我们来思考一下,如果要实现用户用键盘上下左右操作蛇

那该怎么办呢?好像我们之前的学习中没有涉猎

所以在讲代码实现前,我们先要学习一个新的东西
它就是Win 32 API

由于小编在之前的文章中讲过了Win 32 API,本文就不赘诉,大家可自行观看

链接: 【C语言】Win 32 API------一部分内容详解!!!

二、EasyX图形库(要音效的看过来!!!)

当我们在写游戏项目时,游戏没有音效将会有些许无聊
所以,我们可以依靠EasyX图形库来实现音效

由于小编在之前的文章中讲过了Win 32 API,本文就不赘诉,大家可自行观看

链接:
【C语言】EasyX图形库------实现游戏音效(详解)(要游戏音效的看过来!!!)

且要十分注意一点:

当我们在播放短音乐,就比如播放之后的吃东西音效(音乐会播完)
一定要在音乐每次播放完毕和下次要开始播放中间加入close关闭音乐
将音乐播放重置到起始位置,不然音乐播放不出来,会被视为结束播放

三、游戏简介

想要设计游戏,肯定要知道游戏规则

贪吃蛇游戏的规则如下:

玩家使用上下左右键来操控蛇的移动,目的是在游戏地图上吃掉出现的豆子。

每当蛇吃到豆子,它的身体就会变长

如果蛇在移动过程中撞到地图的边界或者自身身体,游戏就会结束。

玩家需要尽量避免碰撞,同时尽可能多地吃到食物以增加分数。

当然,我这里增加了一条规则,可以随意加速减速
加速吃东西得的分多,而减速吃东西得的分少

这里放置了一个网页版的链接,大家可以自行游玩:
贪吃蛇 - 经典贪吃蛇游戏免费在线玩

OK,知道了游戏规则,那么该如何设计游戏呢?

四、游戏设计思路

下面是贪吃蛇游戏的答题思路:

  1. 设计游戏欢迎界面
  2. 设计地图
  3. 设计关于蛇的信息
  4. 设计食物
  5. 操作:用户上下左右操作蛇
  6. 直到游戏结束,记下总分

当然其中的细节处还有很多,带着你一步一步实现。

五、代码的逐步实现

1.创建头文件以及源文件

在我们写代码时,若代码很长很复杂
可以设置多个文件来拆分代码

扫雷是如此,贪吃蛇也是如此

我这里创造了三个文件:
头文件---snake.h:

用来放一些声明和一些库函数的头文件
源文件--snake.c:

用来放函数的定义

(游戏代码的主体)
源文件--test.c:

用来放主函数

(游戏整个框架)

当然,当声明写在snake.h中时,不要忘记在源文件调用snake.h
在源文件顶部写上#include"snake.h"就行啦

如下:

c 复制代码
#include"snake.h"

2.设计游戏窗口

(1)删除光标函数

首先,如果光标一闪一闪,那么会影响游戏画面
所以先要使光标的可见性变为不可见

代码演示:(内有注释,不懂就看)

c 复制代码
//删除光标
void CursorErase(pSnake ps)
{
	//获取句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//光标信息结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	//获取光标信息
	GetConsoleCursorInfo(houtput, &cursor_info);
	//设置光标不可见
	cursor_info.bVisible = false;
	//最后设置
	SetConsoleCursorInfo(houtput, &cursor_info);
}

(2)设置窗口大小和标题

我们还可以设置窗口大小和标题

代码演示:

c 复制代码
//设置窗口大小
system("mode con cols=100 lines=30");
//设置标题
system("title  ZORE_C 的贪吃蛇");

3.设计游戏界面

(1)封装定位光标函数

要想设计游戏欢迎界面,肯定要定位光标到想要打印的地方,故我们会多次定位光标
所以可以封装成一个定位光标函数函数,仅需传两个坐标就可以定位光标,方便使用

代码如下:(内有注释,不懂就看)

c 复制代码
//定位光标
void SetPos(short x, short y)
{
	//获取句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//获取STD_OUTPUT_HANDLE标准输出的句柄
	
	//在`COODR结构体`中存放要设置的光标
	COORD pos = { x,y };

	//调用`SetConsoleCursorPosition`函数设置光标位置
	SetConsoleCursorPosition(houtput, pos);
}

这样仅需传两个坐标 ( x , y ) 就可以定位光标,方便使用

(2)封装改变字体颜色函数

在打印游戏界面的时候,单一的颜色肯定不好看
所以我们可以来封装改变字体颜色函数来变色

代码如下:

c 复制代码
//封装颜色函数
void setColor(int color)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleTextAttribute(hConsole, color);
}
void green() { setColor(10); }
void yellow() { setColor(14); }
void blue() { setColor(9); }
void purple() { setColor(13); }
void cyan() { setColor(11); }
void white() { setColor(15); }
void reset() { setColor(7); }
void red() { setColor(12); }

这样,当我们要调用的时候仅需写 red( ) ;
(将字体颜色改为红色)

(3)打印欢迎界面以及规则

当我们做完上面的准备工作时,就可以开始我们的打印了
在打印的时候,我们需要用到
system("pause");以及system("cls");
system("pause");表示暂停,当用户按下任意一个键时继续
system("cls");表示清除屏幕上的内容

代码演示:
大家也可以自己决定打印什么东西

c 复制代码
//打印欢迎界面以及规则
void WelcomeToGame()
{
	SetPos(37, 13);
	yellow();
	wprintf(L"欢迎来到我的贪吃蛇小游戏");
	SetPos(40, 16);
	wprintf(L"祝大家游玩愉快!!!");

	SetPos(41, 20);
	reset();
	system("pause");

	system("cls");

	SetPos(44, 7);
	red();
	wprintf(L"规 则 说 明");
	
	SetPos(35, 10);
	cyan();
	wprintf(L"1、用 w、a、s、d来控制蛇的移动");
	SetPos(35, 12);
	wprintf(L"2、按 j 加速,按 k 减速");
	SetPos(35, 14);
	wprintf(L"3、按esc退出游戏,按空格暂停游戏");
	SetPos(35, 16);
	wprintf(L"4、注意不要撞墙或咬到自己哦");
	SetPos(35, 18);
	wprintf(L"5、加速能够得到更多的分数");

	SetPos(35, 20);
	yellow();
	wprintf(L"游戏中还有小编给大家留下的彩蛋哦QwQ");

	SetPos(40, 24);
	reset();
	system("pause");

	system("cls");
}

4.设计地图

在打印完我们的欢迎界面以及规则,接下来就是我们的地图设计了

现在我们思考一个问题,怎么表达墙体、食物、蛇身等等东西?

我们可以使用 ■、●、★ 等等符号

但是当我们尝试去打印这些符号时我们会发现

这些符号打印不出来,这是为什么呢?
这就要涉及到setlocale()本地化了

(1)C语言的国际化发展

这里再简单的讲一下C语言的国际化相关的知识,过去C语言并不适合非英语国家(地区)使用,C语言最初假定字符都是自己的,但是这些假定并不是在世界的任何地方都适用。就比如刚刚的 ■、●、★ 等等符号就不能使用

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

而刚刚的 ■、●、★ 等等符号就是属于宽字符wchar_t
要想打印出这些字符首先要将地区进行setlocale()本地化

(2)<locale.h>本地化

在头文件<locale.h>中,提供了许多函数用于控制标准库中对不同地区产生不一样行为的部分
主要有下列几种不同:

  1. 数字量格式
  2. 货币量格式
  3. 字符集
  4. 日期和时间的表示形式

(3)setlocale函数

  • 简介

功能:
设置当前程序使用的区域信息,改变整个区域或部分区域
语法:
char* setlocale (int category, const char* locale);

  • 参数

category:类项
修改地区会影响库的很多,故C语言支持针对不同的类项进行修改
下面的一个宏指定一项 :

  1. LC_ALL: 所有类项

  2. LC_COLLATE:字符串比较函数strcoll()strxfrm()

  3. LC_CTYPE: 影响字符处理函数以及多字节和宽字符函数

  4. LC_MONETARY: 影响货币格式信息

  5. LC_NUMERIC: printf()的数字格式

  6. LC_TIME: 影响时间格式strftime()wcsftime()
    locale:地点
    只有两种可能:

  7. " C ":(正常模式)

  8. " ":(本地模式)

所以,要想打印出这些字符首先要将地区进行setlocale()本地化
代码如下:

c 复制代码
//本地化
setlocale(LC_ALL, "");

(4)宽字符的打印

最后就回到了如何打印■、●、★ 等等字符了

打印■、●、★ 等等宽字符有几个要注意的点:

  1. 宽字符字面量前面得加上前缀L
  2. 宽字符打印用wprintf
  3. 宽字符的占位符为%lc,%ls
    (单字符和字符串)

演示:(内有注释,不懂就看)

c 复制代码
#include <stdio.h>
#include<locale.h>

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

	//宽字符字面量前面得加上前缀L
	wchar_t ch1 = L'■';
	wchar_t ch2 = L'●';
	wchar_t ch3 = L'★';

	//宽字符的占位符为%lc,%ls
	//宽字符字面量前面得加上前缀L
	wprintf(L"%lc %lc %lc\n\n", ch1, ch2, ch3);

	return 0;
}

运行结果:

这样,宽字符的打印就完成啦
然后我们就可以开始设计地图了

(5)地图的设计

这里小编将地图的大小设置为27行58列的规格
且用的是 ' ■ '来打印,并且改变了颜色

由于之前讲过的:

在我们的控制台上的坐标系中,有一个很重要的点:
X轴的每个单位长度为1个普通字符长度
而Y轴的每个单位长度为2个普通字符长度

又因为一个宽字符占两个普通字符长度,所以在上和下边界打印29个' ■ ',在左右边界打印27个' ■ '
故打印27*29的边框

代码演示:

c 复制代码
//打印地图
void CreateMap()
{
	int g;
	//上
	cyan();
	for (g = 1; g <= 29; g++)
	{
		wprintf(L"%lc", L'■');
	}
	//左
	for (g = 0; g <= 26; g++)
	{
		SetPos(0, g);
		wprintf(L"%lc", L'■');
	}
	//下
	for (g = 1; g < 29; g++)
	{
		wprintf(L"%lc", L'■');
	}
	//右
	for (g = 0; g <= 26; g++)
	{
		SetPos(56, g);
		wprintf(L"%lc", L'■');
	}
	reset();
}

5.设计蛇

(1)用链表表示

在打印完地图后,就要开始初始化游戏了

首先就是初始化蛇,那我们该如何来设计蛇呢?

首先我们会想到链表

一 是链表动态增减节点方便(吃食物/移动)

二 是链表天然符合蛇身的物理连接特性

前进就头部新增节点,然后尾部删除节点(为了保持长度)

(2)创建蛇的链表

创建蛇的链表时,成员要有节点的坐标(x,y),还要有指向下一节点的指针

代码演示:(内有注释,不懂就看)

c 复制代码
//蛇身的节点类型
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一节点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//前面的表示将该结构体重命名为 SnakeNode
//后面的表示将该结构体指针重命名为 pSnakeNode

6.初始化游戏

设计完了蛇本身后,游戏中有很多其他的信息要初始化
比如:
蛇的起始位置、食物位置、蛇的方向、食物的权重、总成绩、蛇的速度

我们可以将这些信息归纳成一个结构体把关于蛇的所有信息写入
代码演示:(内有注释,不懂就看)

(内容待会会说,先看个大概)
在头文件<snake.h>中声明结构体类型:

c 复制代码
//蛇的信息
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头指针
	pSnakeNode _pFood;//指向食物指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//蛇的状态
	int _food_weight;//食物的权重
	int _score;      //总成绩
	int _sleep_time;//间隔时间(速度)
}Snake,*pSnake;

然后就是对游戏信息一一进行初始化了:
在头文件<snake.c>中初始化游戏内容:

(内容待会会说,先看个大概)

c 复制代码
//初始化游戏
void InitGame(pSnake ps)
{
	ps->_dir = RIGHT;
	ps->_status = OK;
	ps->_food_weight = 20;
	ps->_score = 0;
	ps->_sleep_time = 75;
	//初始化蛇
	InitSnake(ps);
	//初始化食物
	InitFood(ps);
}

(1)食物

我们可以发现,食物和蛇的节点很像
所以,可以直接创建一个蛇的节点类型命名为食物
pSnakeNode _pFood;//指向食物指针

(2)蛇的方向

蛇一共就4个方向,我们可以用枚举的方法来创建

c 复制代码
//蛇的方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

我们这里初始化小蛇的移动方向是向右的

(3)蛇的状态

蛇的状态一共也就4种:

  1. 正常
  2. 撞墙
  3. 撞到自己
  4. 正常退出

故同理,用枚举来表示

c 复制代码
//蛇的状态
enum GAME_STATUS
{
	OK,//正常
	KILL_BY_WALL,//撞墙
	KILL_BY_SELF,//撞到自己
	END_NORMAL//正常退出
};

(4)食物的权重

当然,我这里增加了一条规则,可以随意加速减速
加速吃东西得的分多,而减速吃东西得的分少

这里小编给的初始值是20分

每加速一次就加5分,减速一次就扣5分

(5)总成绩

这个没什么好说的,后面统计就行

(6)间隔时间

为什么有间隔时间呢?

首先我们要知道小蛇不断运动的原理,就是每隔一段时间就打印一次蛇

当你打印间隔的时间越短,就说明小蛇运动得更快

当你打印间隔的时间越长,就说明小蛇运动得更慢
前面我们说了有不同的速度,也就是不同的间隔时间

这里小编给的初始间隔时间是75ms

后续加速一次减25ms最多加速俩次

(7)初始化蛇

初始化蛇分为两步,一是为蛇的节点开辟空间,二是打印出蛇的每个节点

为蛇的节点开辟空间:

这里小编设定初始的小蛇节点数为5个,且起始位置从(6,6)开始

创建完头节点后,为了将剩下的节点连接上,可以将节点一个接一个进行头插,并且之后每个节点的x轴坐标加2,而y轴坐标不发生改变
打印出蛇的每个节点:

打印没什么好说的,直接遍历链表进行打印

但我这里改变了颜色,使蛇头和蛇身的颜色分开,给更好区分

代码演示:(内有注释,不懂就看)

c 复制代码
//初始化蛇
void InitSnake(pSnake ps)
{
	//为蛇的节点开辟空间
	pSnakeNode cur;
	for (int g = 0; g < 5; g++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("malloc");
			return;
		}
		//初始化节点,赋值
		cur->next = NULL;
		cur->x = 6 + 2 * g;
		cur->y = 6;

		//进行链接
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//开始打印头节点
	cur = ps->_pSnake;
	SetPos(cur->x, cur->y);
	red();
	wprintf(L"%lc", L'●');
	cur = cur->next;
	
	//单独打印完头节点后遍历链表打印
	while (cur)
	{
		green();
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
		reset();
	}
}

(8)初始化食物

生成在地图上的食物肯定是随机的,那就肯定要用到随机数的生成

所以初始化食物也分为两步,一是随机生成食物的坐标,二是为食物的节点开辟空间,三是打印出食物

我在之前的文章中讲过,本文就不赘述,想看的自行观看
链接: 【C语言】rand函数的应用(随机数的生成)

随机生成食物的坐标:

因为蛇身的字符'●'是宽字符,占两普通字符长度,故在坐标系中,蛇身的x轴坐标只能为2,4,6,8,10...等偶数
所以为了蛇能够吃到食物,食物的x轴坐标也只能为2,4,6,8,10...等偶数
然后,食物不能出现在墙上或是蛇上,所以要遍历来判断是否符合
为食物的节点开辟空间:

为食物的节点开辟空间,并且给其赋值
最后打印出食物

代码演示:

c 复制代码
//初始化食物
void InitFood(pSnake ps)
{
	int x = 0, y = 0;
	//生成随机种子
	srand((unsigned)time(NULL));
	
again:
	//生成2-54的随机偶数
	x = (rand() % (27 - 1 + 1) + 1) * 2;
	//生成1-25的随机数
	y = rand() % (25 - 1 + 1) + 1;

	pSnakeNode cur = ps->_pSnake;
	while (cur != NULL)
	{
		if (cur->x == x && cur->y == y)
		{
			//若不符合就回到again去
			goto again;
		}
		cur = cur->next;
	}

	//开辟空间
	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pfood == NULL)
	{
		perror("malloc");
		return;
	}
	pfood->x = x;
	pfood->y = y;
	pfood->next = NULL;

	yellow();
	SetPos(x, y);
	wprintf(L"%lc", L'★');
	reset();

	ps->_pFood = pfood;
}

7.打印帮助信息

这个没什么好说的,让用户进行游戏时也可以看到说明信息大家在写的时候可以自行修改
代码演示:

c 复制代码
//打印帮助信息
void PritnfHelpInfo()
{
	cyan();
	SetPos(68, 10);
	wprintf(L"%ls", L"规 则 说 明");
	yellow();
	SetPos(63, 12);
	wprintf(L"%ls", L"1、不能撞墙,不能咬到自己");
	SetPos(63, 14);
	wprintf(L"%ls", L"2、用 w、a、s、d来控制蛇的移动");
	SetPos(63, 16);
	wprintf(L"%ls", L"3、按 j 加速,按 k 减速");
	SetPos(63, 18);
	wprintf(L"%ls", L"4、加速可以获得更多分数哦");
	SetPos(63, 20);
	wprintf(L"%ls", L"5、按esc退出游戏,按空格暂停游戏");
	cyan();
	SetPos(80, 24);
	wprintf(L"%ls", L"------zore_c大制作");
	reset();
}

8.蛇的移动

创建一个函数来判断蛇在移动时的可能

方向向哪就往哪里走,若吃到食物该怎么办,撞到墙要怎么办,撞到自己要怎么办,这些都写在里面
注意:
在中间小编插入了一些音效,在前面已经讲过了,可以去目录翻翻

代码演示:

(后面还有一些分函数)

c 复制代码
//蛇的移动
int SnakeMove(pSnake ps, int g)
{
	pSnakeNode pnewnode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pnewnode == NULL)
	{
		perror("SnakeMove()::malloc");
		return 0;
	}
	switch (ps->_dir)
	{
	case UP:
		pnewnode->x = ps->_pSnake->x;
		pnewnode->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pnewnode->x = ps->_pSnake->x;
		pnewnode->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pnewnode->x = ps->_pSnake->x - 2;
		pnewnode->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pnewnode->x = ps->_pSnake->x + 2;
		pnewnode->y = ps->_pSnake->y;
		break;
	}
	if (FindFood(pnewnode, ps))
	{
		YesFood(pnewnode, ps);
		//播放吃东西的音效并将g置为0
		g = 0;
		mciSendString(L"play 吃东西.mp3", 0, 0, 0);
	}
	else if (FindWall(pnewnode))
	{
		ps->_status = KILL_BY_WALL;
	}
	else if (FindSelf(pnewnode, ps))
	{
		ps->_status = KILL_BY_SELF;
	}
	else
	{
		NoFood(pnewnode, ps);
	}
	return g;
}

(1)判断是否是食物

直接拿蛇头节点与食物节点进行对比,并返回 1 / 0

c 复制代码
//是否是食物
int FindFood(pSnakeNode pn, pSnake ps)
{
	return ps->_pFood->x == pn->x && ps->_pFood->y == pn->y;
}

(2)判断是否是墙

直接拿蛇头节点与墙坐标进行对比,并返回 1 / 0

c 复制代码
//是否是墙
int FindWall(pSnakeNode pn)
{
	return pn->x == 0 || pn->x == 56 || pn->y == 0 || pn->y == 26;
}

(3)判断是否是蛇身

直接拿蛇头节点与遍历的蛇身进行对比,并返回 1 / 0

c 复制代码
//是否是蛇身
int FindSelf(pSnakeNode pn, pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	while (cur != NULL)
	{
		if (cur->x == pn->x && cur->y == pn->y)
		{
			return 1;
		}
		cur = cur->next;
	}

	return 0;
}

(4)当有食物时

首先销毁那个食物
然后头插一个新节点并打印作为增长的长度
最后重新调用生成食物的函数再生成一个函数

c 复制代码
//当有食物时
void YesFood(pSnakeNode pn, pSnake ps)
{
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	free(pn);
	pn = NULL;

	pSnakeNode cur = ps->_pSnake;

	red();
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'●');
	cur = cur->next;
	reset();

	while (cur)
	{
		green();
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
		reset();
	}
	ps->_score = ps->_score + ps->_food_weight;

	InitFood(ps);
}

(4)当没食物时

首先头插一个新节点
然后遍历链表将尾节点销毁,做到蛇长度不变

这里还有一点:
当我们打印新的蛇时,新头节点直接打印
而尾部节点处要打印两个空格把原来的尾节点顶替掉

防止出现尾节点一直在的问题

c 复制代码
//没食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;

	red();
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'●');
	cur = cur->next;
	reset();

	while (cur->next->next != NULL)
	{
		green();
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
		reset();
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	free(cur->next);
	cur->next = NULL;
}

9.检测按键

前面我们提到了GetAsyncKeyState函数,现在来封装一个宏来判断按键是否被按下

c 复制代码
//检测有无按键
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

(要用到时给KEY_PRESS传虚拟密钥代码就行,若按键按下则返回1,否则返回0)

而在游戏中,我们可以做一个循环,并在这个循环中加入休眠来表示蛇的每一步,当蛇没死亡时就继续检测按键,并作出行动;蛇死亡时就跳出循环

代码演示:

c 复制代码
//检测有无按键
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

//检测按键
void TestKey(pSnake ps)
{
	int g = 0;
	do
	{
		//实时打印信息
		SetPos(64, 1);
		cyan();
		printf("目前总分数:");
		yellow();
		printf("%d\n", ps->_score);
		
		cyan();
		SetPos(64, 3);
		printf("当前每个食物分数:");
		yellow();
		printf("%d\n", ps->_food_weight);

		cyan();
		SetPos(64, 5);
		printf("当前速度:");
		yellow();
		//打印蛇当前速度
		PrintSpeed(ps);
		
		reset();
		if (KEY_PRESS(87) && ps->_dir != DOWN)
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(83) && ps->_dir != UP)
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(65) && ps->_dir != RIGHT)
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(68) && ps->_dir != LEFT)
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(74))
		{
			Quick(ps);
		}
		else if (KEY_PRESS(75))
		{
			Slow(ps);
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_status = END_NORMAL;
		}
		g = SnakeMove(ps,g);
		//休眠
		Sleep(ps->_sleep_time);
		//设立一个g,每10次关闭一次音效来重置音效的位置到起始位置
		//且当吃到食物时,重置g=0,循环
		g++;
		if (g == 10)
		{
			g = 0;
			mciSendString(L"close 吃东西.mp3", 0, 0, 0);
		}
	} while (ps->_status == OK);
}

//打印蛇当前速度
void PrintSpeed(pSnake ps)
{
	if (ps->_sleep_time == 75)
	{
		wprintf(L"正常");

	}
	else if (ps->_sleep_time == 100)
	{
		wprintf(L"慢 ");
		SetPos(62, 7);
		wprintf(L"                           ");
	}
	else if (ps->_sleep_time == 125)
	{
		wprintf(L"超慢");
		red();
		SetPos(62, 7);
		wprintf(L"这么慢你有啥用啊?");
	}
	else if (ps->_sleep_time == 50)
	{
		wprintf(L"快 ");
		SetPos(62, 7);
		wprintf(L"                          ");
	}
	else
	{
		wprintf(L"超快");
		red();
		SetPos(62, 7);
		wprintf(L"这么快你玩得明白吗?");
	}
}

10.游戏结束

当运行到这里时,就说明在检测按键中因为蛇的死亡跳出了循环,现在游戏结束了,故可以打印出游戏结束的原因等等东西

代码演示:

c 复制代码
//游戏结束
void GameEnd(pSnake ps)
{
	SetPos(19, 12);
	red();
	switch (ps->_status)
	{
	case KILL_BY_WALL:
		printf("您撞到墙了,游戏结束");
		mciSendString(L"play 死亡.mp3", 0, 0, 0);
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束");
		mciSendString(L"play 死亡.mp3", 0, 0, 0);
		break;
	case END_NORMAL:
		printf("您主动退出,游戏结束");
		mciSendString(L"play 死亡.mp3", 0, 0, 0);
		break;
	}
	SetPos(20, 16);
	reset();
	system("pause");

	SetPos(22, 20);
	yellow();
	printf("游戏已退出");
}

11.释放链表

当游戏结束时,不要忘记了释放链表,虽然这里后面没有程序,但为了养成良好的代码习惯为了以后到公司敲代码,小编还是建议大家主动释放不用的动态内存

直接遍历链表销毁即可
代码演示:

c 复制代码
//释放链表
void DestroyList(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	while (cur != NULL)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}
	free(ps->_pFood);
	ps = NULL;
}

六、贪吃蛇完整代码

1.snake.h

在snake.h头文件写到:
(头文件的声明)
(以及用来放函数的声明和一些库函数的头文件)

c 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<easyx.h>
#include<sperror.h>
#include<mmsystem.h>
#pragma comment(lib,"winmm.lib")

//检测有无按键
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&1)?1:0)

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

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

//蛇身的节点类型
typedef struct SnakeNode
{
	//坐标
	int x;
	int y;
	//指向下一节点的指针
	struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//前面的表示将该结构体重命名为 SnakeNode
//后面的表示将该结构体指针重命名为 pSnakeNode

//蛇的信息
typedef struct Snake
{
	pSnakeNode _pSnake;//指向蛇头指针
	pSnakeNode _pFood;//指向食物指针
	enum DIRECTION _dir;//蛇的方向
	enum GAME_STATUS _status;//蛇的状态
	int _food_weight;//食物的权重
	int _score;      //总成绩
	int _sleep_time;//间隔时间(速度)
}Snake,*pSnake;

//删除光标
void CursorErase(pSnake ps);

//定位光标
void SetPos(short x, short y);

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

//打印地图
void CreateMap();

//初始化游戏
void InitGame(pSnake ps);

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

//检测按键
void TestKey(pSnake ps);

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

//释放链表
void DestroyList(pSnake ps);

2.snake.cpp

在snake.cpp源文件中写到:
(游戏主要代码)

cpp 复制代码
#include"snake.h"

//定义颜色函数
void setColor(int color)
{
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleTextAttribute(hConsole, color);
}
void green() { setColor(10); }
void yellow() { setColor(14); }
void blue() { setColor(9); }
void purple() { setColor(13); }
void cyan() { setColor(11); }
void white() { setColor(15); }
void reset() { setColor(7); }void red() { setColor(12); }

//删除光标
void CursorErase(pSnake ps)
{
	//获取句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//光标信息结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };
	//获取光标信息
	GetConsoleCursorInfo(houtput, &cursor_info);
	//设置光标不可见
	cursor_info.bVisible = false;
	//最后设置
	SetConsoleCursorInfo(houtput, &cursor_info);
}

//定位光标
void SetPos(short x, short y)
{
	//获取句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//设置坐标
	COORD pos = { x,y };
	//定位光标
	SetConsoleCursorPosition(houtput, pos);
}

//打印欢迎界面
void WelcomeToGame()
{
	SetPos(37, 13);
	yellow();
	wprintf(L"欢迎来到我的贪吃蛇小游戏");
	SetPos(40, 16);
	wprintf(L"祝大家游玩愉快!!!");

	SetPos(41, 20);
	reset();
	system("pause");

	system("cls");

	SetPos(44, 7);
	red();
	wprintf(L"规 则 说 明");
	
	SetPos(35, 10);
	cyan();
	wprintf(L"1、用 w、a、s、d来控制蛇的移动");
	SetPos(35, 12);
	wprintf(L"2、按 j 加速,按 k 减速");
	SetPos(35, 14);
	wprintf(L"3、按esc退出游戏,按空格暂停游戏");
	SetPos(35, 16);
	wprintf(L"4、注意不要撞墙或咬到自己哦");
	SetPos(35, 18);
	wprintf(L"5、加速能够得到更多的分数");

	SetPos(35, 20);
	yellow();
	wprintf(L"游戏中还有小编给大家留下的彩蛋哦QwQ");

	SetPos(40, 24);
	reset();
	system("pause");

	system("cls");
}

//打印地图
void CreateMap()
{
	int g;
	//上
	cyan();
	for (g = 1; g <= 29; g++)
	{
		wprintf(L"%lc", L'■');
	}
	//左
	for (g = 0; g <= 26; g++)
	{
		SetPos(0, g);
		wprintf(L"%lc", L'■');
	}
	//下
	for (g = 1; g < 29; g++)
	{
		wprintf(L"%lc", L'■');
	}
	//右
	for (g = 0; g <= 26; g++)
	{
		SetPos(56, g);
		wprintf(L"%lc", L'■');
	}
	reset();
}

//初始化蛇
void InitSnake(pSnake ps)
{
	//为蛇的节点开辟空间
	pSnakeNode cur;
	for (int g = 0; g < 5; g++)
	{
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("malloc");
			return;
		}
		//初始化节点,赋值
		cur->next = NULL;
		cur->x = 6 + 2 * g;
		cur->y = 6;

		//进行链接
		if (ps->_pSnake == NULL)
		{
			ps->_pSnake = cur;
		}
		else
		{
			cur->next = ps->_pSnake;
			ps->_pSnake = cur;
		}
	}
	//开始打印头节点
	cur = ps->_pSnake;
	SetPos(cur->x, cur->y);
	red();
	wprintf(L"%lc", L'●');
	cur = cur->next;
	
	//单独打印完头节点后遍历链表打印
	while (cur)
	{
		green();
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
		reset();
	}
}

//初始化食物
void InitFood(pSnake ps)
{
	int x = 0, y = 0;
	//生成随机种子
	srand((unsigned)time(NULL));
	
again:
	//生成2-54的随机偶数
	x = (rand() % (27 - 1 + 1) + 1) * 2;
	//生成1-25的随机数
	y = rand() % (25 - 1 + 1) + 1;

	pSnakeNode cur = ps->_pSnake;
	while (cur != NULL)
	{
		if (cur->x == x && cur->y == y)
		{
			//若不符合就回到again去
			goto again;
		}
		cur = cur->next;
	}

	//开辟空间
	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pfood == NULL)
	{
		perror("malloc");
		return;
	}
	pfood->x = x;
	pfood->y = y;
	pfood->next = NULL;

	yellow();
	SetPos(x, y);
	wprintf(L"%lc", L'★');
	reset();

	ps->_pFood = pfood;
}

//初始化游戏
void InitGame(pSnake ps)
{
	ps->_dir = RIGHT;
	ps->_food_weight = 20;
	ps->_score = 0;
	ps->_sleep_time = 75;
	ps->_status = OK;
	//初始化蛇
	InitSnake(ps);
	//初始化食物
	InitFood(ps);
}
 
//打印帮助信息
void PritnfHelpInfo()
{
	cyan();
	SetPos(68, 10);
	wprintf(L"%ls", L"规 则 说 明");
	yellow();
	SetPos(63, 12);
	wprintf(L"%ls", L"1、不能撞墙,不能咬到自己");
	SetPos(63, 14);
	wprintf(L"%ls", L"2、用 w、a、s、d来控制蛇的移动");
	SetPos(63, 16);
	wprintf(L"%ls", L"3、按 j 加速,按 k 减速");
	SetPos(63, 18);
	wprintf(L"%ls", L"4、加速可以获得更多分数哦");
	SetPos(63, 20);
	wprintf(L"%ls", L"5、按esc退出游戏,按空格暂停游戏");
	cyan();
	SetPos(80, 24);
	wprintf(L"%ls", L"------zore_c大制作");
	reset();
}

//暂停
void Pause()
{
	while (1)
	{
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

//加速
void Quick(pSnake ps)
{
	if (ps->_sleep_time >= 50)
	{
		ps->_sleep_time = ps->_sleep_time - 25;
		ps->_food_weight = ps->_food_weight + 5;
	}
}

//减速
void Slow(pSnake ps)
{
	if (ps->_sleep_time <= 100)
	{
		ps->_sleep_time = ps->_sleep_time + 25;
		ps->_food_weight = ps->_food_weight - 5;
	}
}

//有食物
void YesFood(pSnakeNode pn, pSnake ps)
{
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	free(pn);
	pn = NULL;

	pSnakeNode cur = ps->_pSnake;

	red();
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'●');
	cur = cur->next;
	reset();

	while (cur)
	{
		green();
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
		reset();
	}
	ps->_score = ps->_score + ps->_food_weight;

	InitFood(ps);
}

//没食物
void NoFood(pSnakeNode pn, pSnake ps)
{
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	pSnakeNode cur = ps->_pSnake;

	red();
	SetPos(cur->x, cur->y);
	wprintf(L"%lc", L'●');
	cur = cur->next;
	reset();

	while (cur->next->next != NULL)
	{
		green();
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", L'●');
		cur = cur->next;
		reset();
	}
	SetPos(cur->next->x, cur->next->y);
	printf("  ");

	free(cur->next);
	cur->next = NULL;
}

//是否是食物
int FindFood(pSnakeNode pn, pSnake ps)
{
	return ps->_pFood->x == pn->x && ps->_pFood->y == pn->y;
}

//是否是墙
int FindWall(pSnakeNode pn)
{
	return pn->x == 0 || pn->x == 56 || pn->y == 0 || pn->y == 26;
}

//是否是蛇身
int FindSelf(pSnakeNode pn, pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	while (cur != NULL)
	{
		if (cur->x == pn->x && cur->y == pn->y)
		{
			return 1;
		}
		cur = cur->next;
	}

	return 0;
}

//蛇的移动
int SnakeMove(pSnake ps, int g)
{
	pSnakeNode pnewnode = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pnewnode == NULL)
	{
		perror("SnakeMove()::malloc");
		return 0;
	}
	switch (ps->_dir)
	{
	case UP:
		pnewnode->x = ps->_pSnake->x;
		pnewnode->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		pnewnode->x = ps->_pSnake->x;
		pnewnode->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		pnewnode->x = ps->_pSnake->x - 2;
		pnewnode->y = ps->_pSnake->y;
		break;
	case RIGHT:
		pnewnode->x = ps->_pSnake->x + 2;
		pnewnode->y = ps->_pSnake->y;
		break;
	}
	if (FindFood(pnewnode, ps))
	{
		YesFood(pnewnode, ps);
		g = 0;
		//播放吃东西的音效
		mciSendString(L"play 吃东西.mp3", 0, 0, 0);
	}
	else if (FindWall(pnewnode))
	{
		ps->_status = KILL_BY_WALL;
	}
	else if (FindSelf(pnewnode, ps))
	{
		ps->_status = KILL_BY_SELF;
	}
	else
	{
		NoFood(pnewnode, ps);
	}
	return g;
}

//打印蛇当前速度
void PrintSpeed(pSnake ps)
{
	if (ps->_sleep_time == 75)
	{
		wprintf(L"正常");

	}
	else if (ps->_sleep_time == 100)
	{
		wprintf(L"慢 ");
		SetPos(62, 7);
		wprintf(L"                           ");
	}
	else if (ps->_sleep_time == 125)
	{
		wprintf(L"超慢");
		red();
		SetPos(62, 7);
		wprintf(L"这么慢你有啥用啊?");
	}
	else if (ps->_sleep_time == 50)
	{
		wprintf(L"快 ");
		SetPos(62, 7);
		wprintf(L"                          ");
	}
	else
	{
		wprintf(L"超快");
		red();
		SetPos(62, 7);
		wprintf(L"这么快你玩得明白吗?");
	}
}

//检测按键
void TestKey(pSnake ps)
{
	int g = 0;
	do
	{
		//打印
		SetPos(64, 1);
		cyan();
		printf("目前总分数:");
		yellow();
		printf("%d\n", ps->_score);
		
		cyan();
		SetPos(64, 3);
		printf("当前每个食物分数:");
		yellow();
		printf("%d\n", ps->_food_weight);

		cyan();
		SetPos(64, 5);
		printf("当前速度:");
		yellow();
		PrintSpeed(ps);
		
		reset();
		if (KEY_PRESS(87) && ps->_dir != DOWN)
		{
			ps->_dir = UP;
		}
		else if (KEY_PRESS(83) && ps->_dir != UP)
		{
			ps->_dir = DOWN;
		}
		else if (KEY_PRESS(65) && ps->_dir != RIGHT)
		{
			ps->_dir = LEFT;
		}
		else if (KEY_PRESS(68) && ps->_dir != LEFT)
		{
			ps->_dir = RIGHT;
		}
		else if (KEY_PRESS(74))
		{
			Quick(ps);
		}
		else if (KEY_PRESS(75))
		{
			Slow(ps);
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_status = END_NORMAL;
		}
		g = SnakeMove(ps,g);
		Sleep(ps->_sleep_time);
		//设立一个g,每10次关闭一次音效来重置音效的位置到起始位置
		//且当吃到食物时,重置g=0,循环
		g++;
		if (g == 10)
		{
			g = 0;
			mciSendString(L"close 吃东西.mp3", 0, 0, 0);
		}
	} while (ps->_status == OK);
}

//游戏结束
void GameEnd(pSnake ps)
{
	SetPos(19, 12);
	red();
	switch (ps->_status)
	{
	case KILL_BY_WALL:
		printf("您撞到墙了,游戏结束");
		mciSendString(L"play 死亡.mp3", 0, 0, 0);
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束");
		mciSendString(L"play 死亡.mp3", 0, 0, 0);
		break;
	case END_NORMAL:
		printf("您主动退出,游戏结束");
		mciSendString(L"play 死亡.mp3", 0, 0, 0);
		break;
	}
	SetPos(20, 16);
	reset();
	system("pause");

	SetPos(22, 20);
	yellow();
	printf("游戏已退出");
}

//释放链表
void DestroyList(pSnake ps)
{
	pSnakeNode cur = ps->_pSnake;
	while (cur != NULL)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}
	free(ps->_pFood);
	ps = NULL;
}

//void Judge()
//{
//	cyan();
//	SetPos(19, 14);
//	printf("是否继续游戏:( Y / N )");
//	red();
//	SetPos(19, 16);
//	printf("注意:输入前先删除多余字符!!!");
//	SetPos(19, 18);
//	yellow();
//	printf("请输入:");
//	reset();
//}

3. text.cpp

在text.cpp源文件中写到:
(游戏主体框架)

cpp 复制代码
#include"snake.h"

//游戏准备阶段
void GameStart(pSnake ps)
{
	//删除光标
	CursorErase(ps);

	//设置窗口大小
	system("mode con cols=100 lines=30");
	//设置标题
	system("title  ZORE_C 的贪吃蛇");

	//打印信息
	WelcomeToGame();

	//打印地图
	CreateMap();

	//初始化游戏
	InitGame(ps);
}

//游戏运行阶段
void GameRun(pSnake ps)
{
	//打印帮助信息
	PritnfHelpInfo();

	//检测按键
	TestKey(ps);
}

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

	//初始化链表
	Snake snake = { 0 };

	//打开音乐
    mciSendString(L"open 吃东西.mp3", 0, 0, 0);
	mciSendString(L"open 死亡.mp3", 0, 0, 0);
	mciSendString(L"open 背景音乐.mp3", 0, 0, 0);
	//播放背景音乐
	mciSendString(L"play 背景音乐.mp3", 0, 0, 0);

	//游戏准备阶段
	GameStart(&snake);

	//游戏运行阶段
	GameRun(&snake);

	//游戏结束
	GameEnd(&snake);

	//释放链表
	DestroyList(&snake);

	SetPos(0, 27);

	return 0;
}

七、封装成应用

当我们完成这个贪吃蛇项目后,由于配备了音效等东西
要想发送给你的好友玩时直接发送.exe文件不会有音效(好友的电脑没有音效文件)

那么该怎么办呢?
这就需要封装我们的一整个项目打包发送给好友了
这个小编也是去检索的,连接放下面了,大家自行观看

链接: Microsoft Visual Studio 2022 项目打包详细步骤

结语

OK,本期的贪吃蛇代码到这里就结束了

大家可自己游玩,也可改进我的代码

本文有若有不足之处,希望各位兄弟们能给出宝贵的意见。谢谢大家!!!

新人,本期制作不易希望各位兄弟们能动动小手,三连走一走!!!
支持一下(三连必回QwQ)

本期资料来自于:

https://legacy.cplusplus.com/

以及:

https://learn.microsoft.com/zh-cn/windows/win32/api/

相关推荐
一起养小猫8 小时前
《Java数据结构与算法》第四篇(二)二叉树的性质、定义与链式存储实现
java·数据结构·算法
意法半导体STM328 小时前
【官方原创】使用STM32N6测试Helium指令 LAT1567
stm32·单片机·嵌入式硬件·mcu·stm32开发
晨晖217 小时前
单链表逆转,c语言
c语言·数据结构·算法
清风一徐18 小时前
禅道从18.3升级到21.7.6版本
笔记
Jack___Xue18 小时前
LangChain实战快速入门笔记(六)--LangChain使用之Agent
笔记·langchain·unix
零度@18 小时前
SQL 调优全解:从 20 秒到 200 ms 的 6 步实战笔记(附脚本)
数据库·笔记·sql
im_AMBER19 小时前
Leetcode 78 识别数组中的最大异常值 | 镜像对之间最小绝对距离
笔记·学习·算法·leetcode
其美杰布-富贵-李19 小时前
HDF5文件学习笔记
数据结构·笔记·学习
LYFlied19 小时前
【每日算法】LeetCode 25. K 个一组翻转链表
算法·leetcode·链表