【C语言】C语言经典小游戏:贪吃蛇(下)

发布于:2025-06-08 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、游戏前准备

我们首先需要创建三个文件:
Snake.h:结构体、枚举的定义,方法的声明。
Snake.c:方法的实现。
test.c:方法的测试。

首先先在Snake.h文件中定义贪吃蛇的结构体、枚举、以及包含的头文件和各种方法的声明:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<stdbool.h>
#include<time.h>

#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

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

//蛇的状态
enum GAME_STATUS
{
	OK,//正常运行  0
	KILL_BY_WALL,//撞墙 1
	KILL_BY_SELF,//咬到自己 2
	END_NORMAL//正常结束 3
};

//蛇身的节点
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 _food_weight;//一个食物的分数
	int _score;//总成绩
	int _sleep_time;//休息时间,时间越短,速度越快
}Snake, * pSnake;


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

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

//欢迎界面
void WelcomeToGame();

//创建地图
void CreateMap();

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

//游戏运行的逻辑
void GameRun(pSnake ps);

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

//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn,pSnake ps);

//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);

//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);

//检测蛇是否撞墙
void KillByWall(pSnake ps);

//检测蛇是否撞到自己
void KillBySelf(pSnake ps);

//游戏善后的工作
void GameEnd(pSnake ps);

接着我们再在Snake.c文件中编写定位光标位置的方法:

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

然后再进入test.c文件设置程序⽀持本地模式,并进⼊游戏的主逻辑。
主逻辑分为3个过程:

  • 游戏开始(GameStart):完成游戏的初始化。
  • 游戏运⾏(GameRun):完成游戏运⾏逻辑的实现。
  • 游戏结束(GameEnd):完成游戏结束的说明,实现资源释放。
#include<locale.h>
#include"Snake.h"

void test()
{
	int ch = 0;
	do
	{
		system("cls");
		//创建贪吃蛇
		Snake snake = { 0 };
		//初始化游戏
		//1.打印环境界面和功能介绍
		//2.绘制地图
		//3.创建蛇
		//4.创建食物
		//5.设置游戏的相关信息
		GameStart(&snake);

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

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

		SetPos(20, 15);
		printf("再来一局吗!(Y/N):");
		ch = getchar();
		while (getchar() != '\n');//清理\n
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();

	return 0;
}

二、游戏开始

1、游戏开始函数(GameStart)

这个模块完成游戏的初始化任务:

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

我们先在Snake.c文件中创建游戏开始的函数GameStart

void GameStart(pSnake ps)
{
	//设置窗口大小和名字
	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();

	//绘制地图
	CreateMap();

	//创建蛇
	InitSnake(ps);

	//创建食物
	CreateFood(ps);
}

然后再在test.c中进行测试:

void test()
{
	Snake snake = { 0 };
	GameStart(&snake);	
	
	getchar();//暂停程序执行,等待用户输入
}

int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述
可以看到此时窗口的大小和名字已经被成功设置,并且光标也被隐藏了。

接着就可以在GameStart函数中调用其他函数的方法。

1)打印欢迎界⾯(WelcomeToGame)

在游戏正式开始之前,做⼀些功能提醒。

欢迎界面函数:

void WelcomeToGame()
{
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇小游戏\n");

	SetPos(42, 20);
	system("pause");//等待用户按任意键

	system("cls"); //清屏

	SetPos(25, 14);
	wprintf(L"用↑.↓.←.→来控制蛇的移动,按F3加速,F4减速\n");

	SetPos(25, 15);
	wprintf(L"加速能够得到更高的分数\n");

	SetPos(42, 20);
	system("pause");

	system("cls");
}

再进行测试:

void test()
{
	Snake snake = { 0 };
	GameStart(&snake);	

	getchar();
}

int main()
{
	//设置为本地环境
	setlocale(LC_ALL,"");
	test();
	return 0;
}

运行结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2)创建地图(CreateMap)

创建地图就是将墙打印出来,因为是宽字符打印,所以使⽤wprintf函数,打印格式串前使⽤L。打印地图的关键是要算好坐标,才能在想要的位置打印墙体。

墙体打印的宽字符先在Snake.h文件中进行宏定义。

#define WALL L'□' 

易错点: 就是坐标的计算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)

创建地图函数:

void CreateMap()
{
	//上
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);//一个宽字符占2格横坐标
	}
	//下
	SetPos(0, 26);
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}
	//左
	for (int i = 1; i <= 25; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	//右
	for (int i = 1; i <= 25; i++)
	{
		SetPos(56, i);
		wprintf(L"%lc", WALL);
	}
}

再进行测试:

void test()
{
	Snake snake = { 0 };
	GameStart(&snake);	

	getchar();
}

int main()
{
	//设置为本地环境
	setlocale(LC_ALL,"");
	test();
	return 0;
}

运行结果:
在这里插入图片描述

3)初始化蛇⾝(InitSnake)

蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。

  • 创建5个节点,然后将每个节点存放在链表中进⾏管理。
  • 创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
  • 蛇的初始位置从(24,5)开始

需要将初始位置在Snake.h文件中进行宏定义:

#define POS_X 24
#define POS_Y 5

设置蛇的属性:

  • 游戏状态是:OK
  • 蛇的移动速度:200毫秒
  • 蛇的默认⽅向:RIGHT
  • 初始成绩:0
  • 每个⻝物的分数:10

蛇⾝打印的宽字符也在Snake.h文件中进行宏定义:

#define BODY L'●' 

初始化蛇⾝函数:

void InitSnake(pSnake ps)
{
	//当前开辟的蛇节点的指针
	pSnakeNode cur = NULL;

	//开辟5个节点
	for (int i = 0; i < 5; i++)
	{
		//开辟空间
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		//如果开辟失败
		if (cur == NULL)
		{
			perror("InitSnake()::malloc()");
			return;
		}
		cur->next = NULL;
		cur->x = POS_X + 2 * i;
		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->_status = OK;
	ps->_sleep_time = 200;//毫秒
	ps->_dir = RIGHT;
	ps->_score = 0;
	ps->_food_weight = 10;
}

再进行测试:

void test()
{
	Snake snake = { 0 };
	GameStart(&snake);	

	getchar();
}

int main()
{
	//设置为本地环境
	setlocale(LC_ALL,"");
	test();
	return 0;
}

运行结果:
在这里插入图片描述

4)创建⻝物(CreateFood)

  • 先随机⽣成⻝物的坐标
    • x坐标必须是2的倍数
    • ⻝物的坐标不能和蛇⾝任意节点的坐标重复
  • 创建⻝物节点,打印⻝物

⻝物打印的宽字符在Snake.h文件中进行宏定义:

#define FOOD L'★'

创建⻝物的函数:

void CreateFood(pSnake ps)
{
	//先随机生成食物的坐标
	int x = 0;
	int y = 0;
	again:
	do
	{
		x = rand() % 53 + 2;  //x:2~54
		y = rand() % 25 + 1;  //y:1~25

	} while (x % 2 != 0);

	//食物坐标不能和蛇身体坐标冲突
	pSnakeNode cur = ps->_pSnake;//指向蛇头的指针
	while (cur)
	{
		//随机生成的坐标冲突了就重新生成
		if (x == cur->x && y == cur->y)
		{
			goto again;
		}
		cur = cur->next;
	}

	//创建食物节点
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		perror("CreateFood()::malloc()");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	//打印食物节点
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

	ps->_pFood = pFood;//食物节点地址赋值给_pFood
}

再进行测试:

void test()
{
	Snake snake = { 0 };
	GameStart(&snake);	

	getchar();
}

int main()
{
	//设置为本地环境
	setlocale(LC_ALL,"");
	test();
	return 0;
}

运行结果:
在这里插入图片描述

三、游戏运⾏

在完成了游戏开始部分后,现在我们开始进入游戏运行的部分。

1、准备工作

1)帮助信息(PrintHelpInfo)

游戏运⾏期间,右侧需要打印帮助信息,提⽰玩家。

帮助信息函数:

void PrintHelpInfo()
{
	SetPos(64, 14);
	wprintf(L"%ls", L"不能穿墙,不能咬到自己");

	SetPos(64, 15);
	wprintf(L"%ls", L"用↑.↓.←.→来控制蛇的移动");

	SetPos(64, 16);
	wprintf(L"%ls", L"按F3加速,F4减速");

	SetPos(64, 17);
	wprintf(L"ls", L"按Esc退出游戏,按空格暂停游戏");

	SetPos(64, 19);
	wprintf(L"ls", L"《怀旧》制作");
}

运行结果:
在这里插入图片描述

2)判断按键状态(宏KEY_PRESS)

根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。

因此我们还需要判断按键使用情况,通过宏定义来实现:

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

需要的虚拟按键的罗列:

  • 上:VK_UP
  • 下:VK_DOWN
  • 左:VK_LEFT
  • 右:VK_RIGHT
  • 空格:VK_SPACE
  • ESC:VK_ESCAPE
  • F3:VK_F3
  • F4:VK_F4

3)暂停(Pause)

当我们按空格键之后,游戏要能达到暂停的效果。

暂停函数:

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

2、游戏运行函数(GameRun)

接着再在Snake.c文件中创建游戏运行的函数GameRun

void GameRun(pSnake ps)
{
	//打印帮助信息
	PrintHelpInfo();

	//游戏主体
	do
	{
		//打印总分数和食物分
		SetPos(64, 10);
		printf("总分数:%d\n", ps->_score);
		SetPos(64, 11);
		printf("当前食物的分数:%2d\n", ps->_food_weight);

		//检测按键
		//上
		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();
		}
		//Esc(正常退出游戏)
		else if (KEY_PRESS(VK_ESCAPE))
		{
			ps->_status = END_NORMAL;
		}
		//F3(加速)
		else if (KEY_PRESS(VK_F3))
		{
			//休息时间如果大于80  休息时间减少30   一个食物的分数加2
			if (ps->_sleep_time > 80)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		//F4(减速)
		else if (KEY_PRESS(VK_F4))
		{
			//一个食物的分数如果大于2  休息时间增加30  一个食物的分数-2
			if (ps->_food_weight > 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}

		//蛇的移动
		SnakeMove(ps);

		//休眠时间
		Sleep(ps->_sleep_time);

	} while (ps->_status == OK);
}

再进行测试:

void test()
{
	Snake snake = { 0 };
	GameStart(&snake);	

	GameRun(&snake);
	getchar();
}

int main()
{
	//设置为本地环境
	setlocale(LC_ALL,"");
	srand((unsigned int)time(NULL));//确保食物产生的坐标是真正随机的
	test();
	return 0;
}

运行结果:
在这里插入图片描述

1)蛇移动(SnakeMove)

  • 先创建下⼀个节点,根据移动⽅向和蛇头的坐标,得到蛇移动到下⼀个位置的坐标。
  • 看下⼀个位置是否是⻝物,是⻝物就做吃⻝物处理。
  • 如果不是⻝物则做前进⼀步的处理。
  • 蛇⾝移动后,判断是否会撞墙或者撞上⾃⼰蛇⾝,从⽽影响游戏的状态。

蛇移动函数:

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(pNextNode, ps))
	{
		//是食物
		EatFood(pNextNode, ps);
	}
	else
	{
		//不是食物
		NoFood(pNextNode, ps);
	}

	//检测蛇是否撞墙
	KillByWall(ps);

	//检测蛇是否撞到自己
	KillBySelf(ps);
}

1.a)下一个节点是否是食物(NextIsFood)

判断传入的节点 pn的坐标是否与食物坐标相同。

下一个节点是否是食物函数:

int NextIsFood(pSnakeNode pn, pSnake ps)
{
	return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
1.b)是食物(EatFood)

是食物函数:

void EatFood(pSnakeNode pn, pSnake ps)
{
	//头插法
	//食物节点成为新的蛇头
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	//释放pn节点(下一个位置)
	free(pn);
	pn = NULL;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur);
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//吃掉单个食物的分数加到总分
	ps->_score += ps->_food_weight;

	//重新创建食物
	CreateFood(ps);
}
1.c)不是食物(NoFood)

下⼀个节点成为新的蛇头,并将之前蛇⾝最后⼀个节点打印为空格,并释放掉。

注意: 释放最后⼀个结点后,还得将结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。

不是食物函数:

void NoFood(pSnakeNode pn, pSnake ps)
{
	//头插法  
	//下一个节点成为新的蛇头
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	//打印蛇身
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	//蛇身最后一个节点打印为空白(2个空格)
	SetPos(cur->next->x, cur->next->y);
	printf(" ");

	//再释放掉并置为NULL
	free(cur->next);
	cur->next = NULL;
}
1.d)撞墙(KillByWall)

判断蛇头的坐标是否和墙的坐标冲突。

撞墙函数:

void 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;
	}
}
1.e)撞到自身(KillBySelf)

判断蛇头的坐标是否和蛇⾝体的坐标冲突。

撞到自身函数:

void KillBySelf(pSnake ps)
{
	//记录除蛇头外的第一个蛇身节点
	pSnakeNode cur = ps->_pSnake->next;
	//遍历除蛇头的所有蛇身节点坐标
	while (cur)
	{
		if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

四、游戏结束

1、游戏结束函数(GameEnd)

当游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。

Snake.c文件中创建游戏运行的函数GameRun

void GameEnd(pSnake ps)
{
	//判断游戏状态
	SetPos(24, 12);
	switch (ps->_status)
	{
	//正常结束
	case END_NORMAL:
		wprintf(L"您主动结束游戏\n");
		break;
	//撞墙
	case KILL_BY_WALL:
		wprintf(L"您撞到墙上,游戏结束\n");
		break;
	//咬到自身
	case KILL_BY_SELF:
		wprintf(L"您咬到了自己,游戏结束\n");
		break;
	}

	//释放蛇身链表
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
}

当完成了上面的所有方法后,我们就可以在test.c文件中来实现整个游戏了。

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

void test()
{
	int ch = 0;
	do
	{
		Snake snake = { 0 };
		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);

		SetPos(20, 15);
		printf("再来一局吗!(Y/N):");
		ch=getchar();
		while (getchar() != '\n');
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 27);
}

int main()
{
	//设置为本地环境
	setlocale(LC_ALL,"");
	srand((unsigned int)time(NULL));//产生随机的食物
	test();
	return 0;
}

运行结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

点击查看:贪吃蛇源代码


网站公告

今日签到

点亮在社区的每一天
去签到