C语言实现贪吃蛇小游戏

本文基于C语言和Win32API实现贪吃蛇游戏。介绍游戏功能,如用方向键控制、F2加速等。阐述蛇身链表结构,将游戏分为GameStart、GameRun、GameEnd三部分,详细说明各部分功能及实现细节。还提及代码结构、控制台问题及解决方案,最后给出改进方向。

回顾Win32API

  • 上一篇博客中我们对Win32API进行了简略讲解,完成了我么们对光标隐匿,和坐标定位的需求,接下来我们将正式进入贪吃蛇游戏的讲解

游戏功能说明

  • 使用 ↑.↓.←.→ 控制方向
  • 按F2键加速,F3键减速
  • 按空格暂停,ESC退出游戏
  • 蛇不能撞墙,也不能撞到自己
  • 吃到食物会增加蛇身长度
  • 加速或减速后,食物分数也会对应变化

游戏效果演示

视频操作水平有限,大家见谅,大家也可以支点招帮我优化优化

贪吃蛇演示

蛇身结构

  • 蛇身结构是链表
  • 我们定义了两个结构体SnakeNodeSnake
  • SnakeNode
    • 这个结构体中有三个参数,x和y用来存储节点的坐标
    • 还有一个指针连接下一个节点
typedef struct SnakeNode//蛇身节点的结构体
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
  • Snake
    • 该结构体有八个参数,其中包括两个枚举类型
    • 使用这两个枚举类型增加代码可读性
    • 方便游戏状态即逻辑的管理
enum DIRECTION//蛇移动方向
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

enum GAME_STATUS//游戏状态
{
	OK = 1,
	END_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

typedef struct Snake//蛇相关信息的结构体
{
	pSnakeNode _psnake;//用来维护整条蛇的指针,指向头部
	pSnakeNode _pfood;//用来维护食物节点的指针,指向食物
	int _score;//得分
	int _foodweight;//食物分数
	int _sleeptime;//调用Sleep函数,控制蛇的速度
	int _hightestscore;//历史最高分
	enum DIRECTION _dir;//蛇方向的枚举类型
	enum GAMES_TATUS _status;//游戏状态的枚举类型
}Snake, * pSnake;
  • 上面的*Snake ,*pSnake是typedef的重命名,其完整为struct Snake *pSnake,struct SnakeNode *pSnakeNode,之后我们就可以直接使用Snake,和pSnakeNode结构体指针了
  • 后面我们的墙,蛇头,蛇身,食物采用的都是宽字符,可以先宏定义一下
    • 符号前面的L可不要少了,否则识别不出是宽字符
    • 打印宽字符也建议使用wprintf,格式如下

在这里插入图片描述

#define WALL L'□'
#define HEAD L'★'      
#define BODY L'●'
#define FOOD L'☆'

游戏框架

  • 我们将整个游戏分为三部分
  • 第一部分为 GameStart,其主要功能有
    • 对控制台大小,名字进行设置
    • 将光标隐匿
    • 打印欢迎界面的提示信息
    • 打印地图
    • 对蛇的相关信息进行初始化
    • 导入历史最高分
    • 创建食物

在这里插入图片描述

  • 第二部分为GameRun,这部分是蛇移动实现,也是最为重要的部分,功能有

    • 功能信息的打印
    • 获取移动方向
    • 往指定方向移动
    • 判断蛇移动后的状态
      在这里插入图片描述
  • 第三部分为GameEnd,其功能为游戏退出状态提醒和游戏资源是释放

    • 退出信息提示
    • 保存历史最高分
    • 资源释放

GameStart

坐标定位及欢迎信息打印

  • 对控制台大小,名字进行设置,将光标隐匿已经在API函数里介绍了,这里对信息打印与坐标关系进行说明
  • 由于打印信息的坐标默认是在左上角的,所以我们想要在指定位置打印信息,就需要使用上篇博客介绍的SetPos函数,其第一个参数为x坐标,第二个为y坐标,使用一次SetPos只能更改一次坐标
void WelcomeToGame()
{
	SetPos(80, 20);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(85, 40);
	system("pause");//会暂停程序,直到按任意键继续
	system("cls");//清除当前屏幕信息
	SetPos(65, 20);
	printf("使用 ↑.↓.←.→ 控制方向,按F2键加速,F3键减速");
	SetPos(80, 40);
	system("pause");
	system("cls");
}

打印地图

  • 地图的打印也和信息的打印是一样,都是在我们计算好的坐标位置打印对应图标即可
  • 需要注意的是坐标的起始位置及X,Y轴的方向,还需要注意坐标的长宽是不等长的如下图
  • 了解这些以后大家就可以按照自己的需求去设置地图的大小了(控制台对应的大小也要对应设置)

参考地图

在这里插入图片描述

void CreateMap()
{
	//上
	for (size_t i = 2; i <= 160; i += 2)
	{
		SetPos(i, 1);
		wprintf(L"%lc", WALL);
	}
	//下
	for (size_t i = 2; i <= 160; i += 2)
	{
		SetPos(i, 44);
		wprintf(L"%lc", WALL);
	}
	//左
	for (size_t i = 2; i <= 43; i++)
	{
		SetPos(2, i);
		wprintf(L"%lc", WALL);

	}
	//右
	for (size_t i = 2; i <= 43; i++)
	{
		SetPos(160, i);
		wprintf(L"%lc", WALL);
	}

}

对蛇的相关信息进行初始化

  • 由于蛇是由链表组成的,我们malloc一个蛇身节点以后,就要对该节点的参数进行设置,将x,y坐标设置好。
  • 蛇身节点连接,采用头插的方式将节点连接,用Snake结构体里面的_psnake指针指向蛇头,维护整条蛇。
  • 打印蛇身
    • 蛇的形状及移动本质上都是在对应坐标位置打印蛇身图标,所以我们要从蛇头节点开始遍历,设置好每一个节点对应的坐标将其打印
pSnakeNode ListBuyNode()
{
	pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (node == NULL)
	{
		perror("ListBuyNode()::malloc()");
		exit;
	}
	return node;
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (size_t i = 0; i < SNAKELEN; i++)//建议最少设置3个节点
	{
		cur = ListBuyNode();
		cur->x = POS_X + i * 2;//初始蛇的位置
		cur->y = POS_Y;
		cur->next = NULL;

		if (ps->_psnake == NULL)//连接蛇身
		{
			ps->_psnake = cur;
		}
		else//头插
		{
			cur->next = ps->_psnake;
			ps->_psnake = cur;
		}
	}
	ps->_dir = RIGHT;//默认方向为右
	ps->_foodweight = 10;
	ps->_score = 0;
	ps->_hightestscore = 0;
	ps->_sleeptime = 200;//单位是毫秒
	ps->_status = OK;//游戏状态
	//打印蛇身
	cur = ps->_psnake;
	while (cur)
	{
		if (cur == ps->_psnake)//区分头和身体
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", BODY);
		}
		cur = cur->next;
	}
}

导入历史最高分

  • 导入历史最高分可以增加游戏的趣味性和功能性,同时也可以激发游戏的胜负欲
  • 该功能会先自动创建一个HightestScore.txt文本,用来记录历史分数,该文本存在于该项目的文件夹里面
    在这里插入图片描述
  • 程序写好第一次启动时由于没有记录最高分的对应文本就执行了文件操作中fopen的r模式,就会导致程序终止,这里我们不用担心,这是正常的(因为我们没有r模式要打开的对应文本,系统便会出错)。在此之后就会帮我们自动创建一个对应的文本,之后就不会有任何问题了
    在这里插入图片描述
void LoadScore(pSnake ps)
{
	int min = 0;
	FILE* pf = fopen("HightestScore.txt", "r");
	if (pf == NULL)//自动创建一个HightestScore.txt文件
	{
		FILE* pf = fopen("HightestScore.txt", "w");
		if (pf == NULL)
		{
			perror("LoadScore()::fopen()");
			exit;
		}
		fprintf(pf, "%d", min);//第一次打开时设置为0
		fclose(pf);
		pf = NULL;
		pf = fopen("HightestScore.txt", "r");
	}
	fscanf(pf,"%d", &ps->_hightestscore);
	fclose(pf);
	pf = NULL;

}

创建食物

  • 食物也是一个节点,要想让食物随机出现,我们就需要用rand来随机生成坐标。
  • 随机生成食物的坐标需要注意以下几点
    • 食物的X坐标需要是2的倍数,因为图标使用的是宽字符(X为2,Y为1),如果食物的X坐标不是2的倍数,食物就有可能卡在强里面
    • 食物坐标需要在墙里面,这里就需要我们去计算X,Y的最小值和最大值了。只要对着设置好的地图边界计算即可,记住宽字符在X轴占2,Y轴占1
    • 食物的坐标也不能和蛇身冲突
  • 最后将食物打印
void CreateFood(pSnake ps)
{
	int x;
	int y;
again:
	do
	{
		x = rand() % 155 + 4;//4-158 墙边宽2-160
		y = rand() % 40 + 3;//3-42   墙边长2-43
	} while (x % 2 != 0);//x坐标要为2的倍数,不然打印时可能卡墙里

	pSnakeNode cur = ps->_psnake;
	while (cur)//防止与蛇身冲突
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode food = ListBuyNode();
	ps->_pfood = food;
	food->x = x;
	food->y = y;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

}

GameRun

功能信息的打印

  • 这部分内容是固定的,只要设置好要打印位置的坐标直接打印即可
  • 还有一个实时分数的打印我们放在循环里实现
void PrintHelpInfo(pSnake ps)//固定信息
{
	SetPos(164, 9);
	printf("历史最高分:%-5d", ps->_hightestscore);
	SetPos(164, 20);
	printf("使用 ↑.↓.←.→ 控制方向");
	SetPos(164, 21);
	printf("按F2键加速,F3键减速");
	SetPos(164, 22);
	printf("按空格暂停,ESC退出游戏");
}

获取移动方向

蛇的移动即在移动方向上打印出新的蛇身。蛇是由链表构成的,每一次移动都需要连接新的节点(SnakeNode),这个新节点的里面的x,y即使要打印位置的坐标,所以需要获取蛇移动的方向,进而设置移动方向的坐标,完成蛇的移动

  • 用键盘控制蛇的移动,就需要用到上文提到的GetAsyncKeyState这个API函数。定义一个宏来获取键盘信息
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) &0x1) ? 1:0)
  • 我们使用 ↑.↓.←.→ 控制方向,并且蛇在向上时不能原地掉头往下吧,左右也一样。
  • 获取键盘的移动信息后,记得修改蛇的默认方向
  • 蛇的方向和游戏的状态都是通过改变两个枚举类型来实现的
void GameRun(pSnake ps)
{
	PrintHelpInfo(ps);
	do
	{
		SetPos(164, 10);
		printf("当前得分 %-5d", ps->_score);
		SetPos(164, 11);
		printf("食物分数 %-2d", ps->_foodweight);//初始为10分,打印时应占2位
		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_ESCAPE))//ESC键
		{
			ps->_status = END_NORMAL;//正常退出游戏
		}
		else if (KEY_PRESS(VK_SPACE))//空格
		{
			Pause();//暂停
		}
		else if (KEY_PRESS(0x71))//F2键为加速功能
		{
			if (ps->_sleeptime > 80)//控制次数
			{
				ps->_sleeptime -= 30;//减少休眠时间达到加速效果
				ps->_foodweight += 2;//加速加分
			}

		}
		else if (KEY_PRESS(0x72))//F3键为减速功能
		{
			if (ps->_sleeptime < 320)
			{
				ps->_sleeptime += 30;//增加修眠时间达到减速效果
				ps->_foodweight -= 2;//减速减分
			}
		}
		Sleep(ps->_sleeptime);//改变速度
		SnakeMove(ps);

	} while (ps->_status == OK);
}
  • 这里的暂停是由Pause函数实现的,通过检测空格是否被按过来实现游戏的暂停与恢复
void Pause()
{
	while (1)
	{
		Sleep(100);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
	}
}

往指定方向移动

  • 在获取移动方向后,我们已经把移动方向设置好了,接下来就进入SnakeMove函数实现蛇的移动
  • 蛇一移动就需要头插一个新节点,这个新节点的x,y坐标就和我们获取的方向有关,上下方向的移动会改变y,左右改变x,
	pSnakeNode pnext = ListBuyNode();
	switch (ps->_dir)//设置新连接节点的x,y坐标
	{//宽字符x为2,y为1
	case UP:
		pnext->x = ps->_psnake->x;
		pnext->y = ps->_psnake->y - 1;
		break;
	case DOWN:
		pnext->x = ps->_psnake->x;
		pnext->y = ps->_psnake->y + 1;
		break;
	case LEFT:
		pnext->x = ps->_psnake->x - 2;
		pnext->y = ps->_psnake->y;
		break;
	case RIGHT:
		pnext->x = ps->_psnake->x + 2;
		pnext->y = ps->_psnake->y;
		break;
	}//新的节点要么是食物,要么不是

判断蛇移动后的状态

  • 蛇移动的每一步我们都需要去判断蛇是否吃到食物,是否撞到自己,是否撞墙
    • 蛇每一次移动都会头插一个新的节点,判断是否吃到食物,我们只需对比食物节点的x,y坐标与蛇头的x,y坐标是否相等即可。我们通过IsFood这个函数判断是否为食物
int IsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pfood->x == pnext->x && ps->_pfood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}
  • 是食物
    • 是食物,蛇身会变长,分数会增加
    • 吃完食物后别忘记再创建新食物让游戏继续
void EatFood(pSnake ps, pSnakeNode pnext)
{
	pnext->next = ps->_psnake;//先将新节点与蛇身连接起来
	ps->_psnake = pnext;

	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		if (cur == ps->_psnake)
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			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 pnext)//不吃食物,节点数量不变
{
	pnext->next = ps->_psnake;//链接新节点
	ps->_psnake = pnext;

	pSnakeNode cur = ps->_psnake;
	while (cur->next->next)//增加了新节点,但要保持数量不变,则需要找到倒数第二个节点释放最后一个节点
	{
		if (cur == ps->_psnake)
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			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;//切记要置空

}

与是否撞墙和是否撞到自己相关的是游戏状态这个枚举类型,我们通过枚举变量来控制游戏是否结束
在这里插入图片描述

  • 是否撞墙
    • 判断是否撞墙,将蛇头的x,y坐标与墙的坐标比对即可,这与我们设置的地图大小有关。
    • 如果撞墙,则将游戏状态更改为:KILL_BY_WALL
void KillByWall(pSnake ps)
{
	if (ps->_psnake->x == 2 ||
		ps->_psnake->x == 160 ||
		ps->_psnake->y == 1 ||
		ps->_psnake->y == 44)
	{
		ps->_status = KILL_BY_WALL;
	}
}
  • 是否撞到自己
    • 将蛇头的x,y坐标与蛇头以后节点的x,y坐标比对
    • 如果撞到,则将游戏状态改为:KILL_BY_SELF
void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_psnake->next;
	while (cur)
	{
		if (ps->_psnake->x == cur->x && ps->_psnake->y == cur->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

GameEnd

退出信息提示

  • 当游戏结束时我们对为何结束的原因说明
SetPos(70, 20);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("退出游戏成功");
		break;
	case KILL_BY_WALL:
		printf("撞墙了");
		break;
	case KILL_BY_SELF:
		printf("撞到自己了");
		break;
	}

保存历史最高分

  • 每把游戏结束时我们看看能否刷新最高分数
  • 这里需要注意的是fopen为w模式时,只要一打开文件,无论是否对文本数据进行操作,都会对原有数据进行覆盖,所以当历史最高分刷新时要将新分数覆盖进去,否则仍然保存当前历史最高分
void SaveScore(pSnake ps)
{
	FILE* pf = fopen("HightestScore.txt", "w");
	if (pf == NULL)
	{
		perror("Error opening file");
		return;
	}
	if (ps->_score > ps->_hightestscore)
	{
		fprintf(pf, "%d", ps->_score);
	}
	else
	{
		fprintf(pf, "%d", ps->_hightestscore);
	}
	fclose(pf);
	pf = NULL;
}

资源释放

  • 游戏结束后,将malloc开辟的节点释放
pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	//ps->_psnake = NULL;
	ps = NULL;
  • 至此,游戏主体框架完成

主函数部分

  • 这部分内容为使用setlocale进行本地初始化
  • 随机数rand需要搭配使用srand
  • 重复运行游戏的循环
void game()
{
	//Snake snake = { 0 };
	int ch=0;
	do
	{
		Snake snake = { 0 };

		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);

		SetPos(70, 21);
		printf("再来一局?Y/N");
		ch = getchar();
		getchar();//用来吸收缓冲区的回车键
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 44);
}

int main()
{
	setlocale(LC_ALL, "");//切换本地化,""内无空格
	srand((unsigned int)time(NULL));
	game();

	return 0;
}

游戏完整代码

  • 代码分为三个文件"Snake.c " ,“Snake.h”,“test.c”
  • test.c
#define _CRT_SECURE_NO_WARNINGS 1 
#include"Snake.h"

void game()
{
	//Snake snake = { 0 };
	int ch=0;
	do
	{
		Snake snake = { 0 };

		GameStart(&snake);
		GameRun(&snake);
		GameEnd(&snake);

		SetPos(70, 21);
		system("pause");

		SetPos(70, 22);
		printf("再来一局?Y/N");
		ch = getchar();
		getchar();//用来吸收缓冲区的回车键
	} while (ch == 'Y' || ch == 'y');
	SetPos(0, 44);
}

int main()
{
	setlocale(LC_ALL, "");//切换本地化,""只有单纯的双引号,里面没有任何东西
	srand((unsigned int)time(NULL));
	game();

	return 0;
}
  • Snake.h
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdbool.h>
#include<locale.h>
#include<stdlib.h>
#include<windows.h>

#define WALL L'□'
#define HEAD L'★'      
#define BODY L'●'
#define FOOD L'☆'

#define POS_X 70
#define POS_Y 10
#define SNAKELEN 3

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

typedef struct SnakeNode//蛇身节点的结构体
{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

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

enum GAME_STATUS//游戏状态
{
	OK = 1,
	END_NORMAL,
	KILL_BY_WALL,
	KILL_BY_SELF
};

typedef struct Snake//蛇相关信息的结构体
{
	pSnakeNode _psnake;//用来维护整条蛇的指针,指向头部
	pSnakeNode _pfood;//用来维护食物节点的指针,指向食物
	int _score;//得分
	int _foodweight;//食物分数
	int _sleeptime;//调用Sleep函数,控制蛇的速度
	int _hightestscore;//历史最高分
	enum DIRECTION _dir;//蛇方向的枚举类型
	enum GAMES_TATUS _status;//游戏状态的枚举类型
}Snake, * pSnake;

void game();

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

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

void SetPos(int x, int y);//坐标定位

void CreateMap();//打印地图

void InitSnake(pSnake ps);//初始蛇的相关信息

pSnakeNode ListBuyNode();//创建节点

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

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

void PrintHelpInfo(pSnake ps);//游戏操作提示信息

void Pause();//暂停

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

int IsFood(pSnake ps, pSnakeNode pnext);//判断是否为食物

void EatFood(pSnake ps, pSnakeNode pnext);//吃到食物

void NoFood(pSnake ps, pSnakeNode pnext);//没吃到食物

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

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

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

//记录功能
void LoadScore(pSnake ps);//导入历史最高分

void SaveScore(pSnake ps);//保存历史最高分
  • Snake.c
#define _CRT_SECURE_NO_WARNINGS 1 
#include"Snake.h"

void SetPos(int x, int y)
{
	COORD pos = { x,y };
	HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(handle, pos);
}

void WelcomeToGame()
{
	SetPos(80, 20);
	printf("欢迎来到贪吃蛇小游戏");
	SetPos(85, 40);
	system("pause");//会暂停程序,直到按任意键继续
	system("cls");//清除当前屏幕信息
	SetPos(65, 20);
	printf("使用 ↑.↓.←.→ 控制方向,按F2键加速,F3键减速");
	SetPos(80, 40);
	system("pause");
	system("cls");
}

void CreateMap()
{
	//上
	for (size_t i = 2; i <= 160; i += 2)
	{
		SetPos(i, 1);
		wprintf(L"%lc", WALL);
	}
	//下
	for (size_t i = 2; i <= 160; i += 2)
	{
		SetPos(i, 44);
		wprintf(L"%lc", WALL);
	}
	//左
	for (size_t i = 2; i <= 43; i++)
	{
		SetPos(2, i);
		wprintf(L"%lc", WALL);

	}
	//右
	for (size_t i = 2; i <= 43; i++)
	{
		SetPos(160, i);
		wprintf(L"%lc", WALL);
	}

}


pSnakeNode ListBuyNode()
{
	pSnakeNode node = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (node == NULL)
	{
		perror("ListBuyNode()::malloc()");
		exit;
	}
	return node;
}

void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;
	for (size_t i = 0; i < SNAKELEN; i++)//建议最少设置3个节点
	{
		cur = ListBuyNode();
		cur->x = POS_X + i * 2;
		cur->y = POS_Y;
		cur->next = NULL;

		if (ps->_psnake == NULL)//连接蛇身
		{
			ps->_psnake = cur;
		}
		else//头插
		{
			cur->next = ps->_psnake;
			ps->_psnake = cur;
		}
	}
	ps->_dir = RIGHT;//默认方向为右
	ps->_foodweight = 10;
	ps->_score = 0;
	ps->_hightestscore = 0;
	ps->_sleeptime = 200;//单位是毫秒
	ps->_status = OK;//游戏状态
	//打印蛇身
	cur = ps->_psnake;
	while (cur)
	{
		if (cur == ps->_psnake)
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", BODY);
		}
		cur = cur->next;
	}
}

void LoadScore(pSnake ps)
{
	int min = 0;
	FILE* pf = fopen("HightestScore.txt", "r");
	if (pf == NULL)//自动创建一个HightestScore.txt文件
	{
		FILE* pf = fopen("HightestScore.txt", "w");
		if (pf == NULL)
		{
			perror("LoadScore()::fopen()");
			exit;
		}
		fprintf(pf, "%d", min);//第一次打开时设置为0
		fclose(pf);
		pf = NULL;
		pf = fopen("HightestScore.txt", "r");
	}
	fscanf(pf,"%d", &ps->_hightestscore);
	fclose(pf);
	pf = NULL;

}

void CreateFood(pSnake ps)
{
	int x;
	int y;
again:
	do
	{
		x = rand() % 155 + 4;//4-158 墙边宽2-160
		y = rand() % 40 + 3;//3-42   墙边长2-43
	} while (x % 2 != 0);//x坐标要为2的倍数,不然打印时可能卡墙里

	pSnakeNode cur = ps->_psnake;
	while (cur)//防止与蛇身冲突
	{
		if (cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}
	pSnakeNode food = ListBuyNode();
	ps->_pfood = food;
	food->x = x;
	food->y = y;
	SetPos(x, y);
	wprintf(L"%lc", FOOD);

}


void GameStart(pSnake ps)
{
	system("mode con cols=192 lines=46");
	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);
	LoadScore(ps);
	CreateFood(ps);

}

void PrintHelpInfo(pSnake ps)//固定信息
{
	SetPos(164, 9);
	printf("历史最高分:%-5d", ps->_hightestscore);
	SetPos(164, 20);
	printf("使用 ↑.↓.←.→ 控制方向");
	SetPos(164, 21);
	printf("按F2键加速,F3键减速");
	SetPos(164, 22);
	printf("按空格暂停,ESC退出游戏");
}

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

int IsFood(pSnake ps, pSnakeNode pnext)
{
	if (ps->_pfood->x == pnext->x && ps->_pfood->y == pnext->y)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

void EatFood(pSnake ps, pSnakeNode pnext)
{
	pnext->next = ps->_psnake;//先将新节点与蛇身连接起来
	ps->_psnake = pnext;

	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		if (cur == ps->_psnake)
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			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 pnext)//不吃食物,节点数量不变
{
	pnext->next = ps->_psnake;//链接新节点
	ps->_psnake = pnext;

	pSnakeNode cur = ps->_psnake;
	while (cur->next->next)//增加了新节点,但要保持数量不变,则需要找到倒数第二个节点释放最后一个节点
	{
		if (cur == ps->_psnake)
		{
			SetPos(cur->x, cur->y);
			wprintf(L"%lc", HEAD);
		}
		else
		{
			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 KillByWall(pSnake ps)
{
	if (ps->_psnake->x == 2 ||
		ps->_psnake->x == 160 ||
		ps->_psnake->y == 1 ||
		ps->_psnake->y == 44)
	{
		ps->_status = KILL_BY_WALL;
	}
}

void KillBySelf(pSnake ps)
{
	pSnakeNode cur = ps->_psnake->next;
	while (cur)
	{
		if (ps->_psnake->x == cur->x && ps->_psnake->y == cur->y)
		{
			ps->_status = KILL_BY_SELF;
			break;
		}
		cur = cur->next;
	}
}

void SnakeMove(pSnake ps)//蛇的移动是靠连接的新的节点中的x,y坐标
{
	pSnakeNode pnext = ListBuyNode();
	switch (ps->_dir)//设置新连接节点的x,y坐标
	{
	case UP:
		pnext->x = ps->_psnake->x;
		pnext->y = ps->_psnake->y - 1;
		break;
	case DOWN:
		pnext->x = ps->_psnake->x;
		pnext->y = ps->_psnake->y + 1;
		break;
	case LEFT:
		pnext->x = ps->_psnake->x - 2;
		pnext->y = ps->_psnake->y;
		break;
	case RIGHT:
		pnext->x = ps->_psnake->x + 2;
		pnext->y = ps->_psnake->y;
		break;
	}//新的节点要么是食物,要么不是

	if (IsFood(ps, pnext))
	{
		EatFood(ps, pnext);
	}
	else
	{
		NoFood(ps, pnext);
	}

	KillByWall(ps);
	KillBySelf(ps);

}

void GameRun(pSnake ps)
{
	PrintHelpInfo(ps);
	do
	{
		SetPos(164, 10);
		printf("当前得分 %-5d", ps->_score);
		SetPos(164, 11);
		printf("食物分数 %-2d", ps->_foodweight);//初始为10分,打印时应占2位
		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_ESCAPE))
		{
			ps->_status = END_NORMAL;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			Pause();
		}
		else if (KEY_PRESS(0x71))//F2
		{
			if (ps->_sleeptime > 80)
			{
				ps->_sleeptime -= 30;
				ps->_foodweight += 2;
			}

		}
		else if (KEY_PRESS(0x72))//F3
		{
			if (ps->_sleeptime < 320)
			{
				ps->_sleeptime += 30;
				ps->_foodweight -= 2;
			}
		}
		Sleep(ps->_sleeptime);
		SnakeMove(ps);

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

void SaveScore(pSnake ps)
{
	FILE* pf = fopen("HightestScore.txt", "w");
	if (pf == NULL)
	{
		perror("Error opening file");
		return;
	}
	if (ps->_score > ps->_hightestscore)
	{
		fprintf(pf, "%d", ps->_score);
	}
	else
	{
		fprintf(pf, "%d", ps->_hightestscore);
	}
	fclose(pf);
	pf = NULL;
}


void GameEnd(pSnake ps)
{
	SetPos(70, 20);
	switch (ps->_status)
	{
	case END_NORMAL:
		printf("退出游戏成功");
		break;
	case KILL_BY_WALL:
		printf("撞墙了");
		break;
	case KILL_BY_SELF:
		printf("撞到自己了");
		break;
	}

	SaveScore(ps);

	pSnakeNode cur = ps->_psnake;
	while (cur)
	{
		pSnakeNode del = cur;
		cur = cur->next;
		free(del);
	}
	//ps->_psnake = NULL;
	ps = NULL;
}

控制台补充

  • 如果大家的VS窗口是这样的,需要改为控制台窗口
    在这里插入图片描述
    游戏循环逻辑问题

  • 这部分不一定每个人都会遇到,如果遇到了,就可以通过上述方案解决,如果没遇到,直接忽略

  • 我们在结束一局游戏时想要重复玩的逻辑是这样的,只需要输入Y/y就能再开一把
    在这里插入图片描述

  • 但有些时候可能会遇到下图这样的情况,明明输入的是Y/y,但是却会结束游戏,调试也让你一头雾水。让你怀疑代码是不是有问题。怀疑这么简单的循环逻辑也会错?答案是否定的,代码没有错!那么问题出在哪里呢?
    在这里插入图片描述

  • 这其实是因为控制台的原因,贪吃蛇游戏进行执行的过程中,光标虽然被隐匿了,但是光标行为也是存在的,只是没有调用scanf函数(getchar同理),那么在下次调用scanf函数之前,这些按键行为都是会被记录的,再调用scanf的时候,这些按键行为就会被执行,在贪吃蛇游戏执行的过程中,所有的按键行为都会被记录

  • 游戏中你遇到的上次的输入会回显到光标后面,就是因为按了上键和右键,这是因为上键和右键可以调出控制台的上次输入,这不是程序的bug,是控制台的行为

  • 我们可以通过下面的代码对输入内容进行回显
    在这里插入图片描述

int main()//请在贪吃蛇项目的游戏代码里测试
{
	char arr[100];
	
	int count = 0;
	while (1)
	{
		if (count != 3)
		{
			int n = 1;
			while (n==1)
			{
				scanf("%s", arr);
				n++;
			}
			while (n==2)
			{
				printf("%s\n", arr);
				n++;
			}

			if (count == 2)
			{
				printf("已完成3次输入,已用完输入次数\n");
			}
			count++;
		}
		if ((count==3)&&(KEY_PRESS(VK_UP)||KEY_PRESS(VK_RIGHT)))//上面输入次数已用完
		{                                                       //只有按下上键或有键
			//scanf("%s", arr);                                 //才能调用scanf/getchar
			getchar();                                          //进行回显
		}
	}

	return 0;
}

  • 通过上面回显测试的代码可以看到确实如我们上面所说,也是导致我们游戏逻辑异常的原因

解决方案

  • 在printf(“再来一局”)之前加上;
system("pause");

游戏改进

  • 在此基础上,我们可以进一步优化
    • 对游戏界面进行优化,让界面看起来更加美观
    • 改进分数与速度的机制,怎加难度
    • 拓展玩法,如蛇可以穿墙,可以吃自己
  • 希望这篇博客对刚学完C语言和链表,又想搞点小项目的你有所帮助,即巩固前面所学知识,又能提高编程兴趣。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值