C语言初学者必看:控制台贪吃蛇游戏的实现教程(超详细)

引言

众所周知,贪吃蛇、俄罗斯方块、扫雷等都是非常经典的游戏,在今天这篇文章中将由浅入深的介绍:如何使用C语言在控制台实现这样一个经典的贪吃蛇小游戏。

在实现之这个程序之前,先了解一下必要知识:

1. Win32 API

Windows系统除了控制应用程序执行、分配内存、管理资源以外,也是一个很大的服务中心,调用这个服务中心的各种服务(也就是函数),可以帮应用程序达到开启窗口、描绘图形、使用设备等目的。

1.1 控制台程序

在控制台窗口中可以使用命令来设置控制台窗口的长宽,比如:

C 复制代码
mode con cols=100 lines=50

也可以设置控制台窗口的名字:

C 复制代码
title 贪吃蛇

这些命令也可以在C语言中调用函数system执行:

C 复制代码
#include<stdio.h>
#include<Windows.h>

int main()
{
	// 设置控制台大小:长100列,宽50列
	system("mode con cols=100 lines=50");
	// 设置控制台名称
	system("title 贪吃蛇");

	return 0;
}

1.2 控制台坐标COORD

COORD是Windows API中定义的结构体,表示一个字符在屏幕上的坐标

C 复制代码
typedef struct _COORD 
{ 
    SHORT X; 
    SHORT Y; 
} COORD, *PCOORD;

给坐标赋值:

C 复制代码
COORD pos = { 50, 25 };

1.3 GetStdHandle

GetStdHandle是一个Windows API函数,它用于从一个标准设备(标准输入、标准输出、或标准错误)中获得一个句柄,从而用来操作设备。 GetStdHandle 函数 - Windows Console | Microsoft Learn

在官方文档中可以看到参数与返回类型:

可以看到这个函数的参数有三个,返回类型为HANDLE,所以可以定义一个HANDLE变量,用于接收GetStdHandle返回的句柄。

C 复制代码
HANDLE hOutPut = NULL;
hOutPut = GetStdHandle(STD_OUTPUT_HANDLE)

1.4 CONSOLE_CURSOR_INFO

这是一个结构体,包含了控制台光标的信息。

C 复制代码
typedef struct _CONSOLE_CURSOR_INFO {
    DWORD dwSize;    // 光标大小,表示光标填充字符格的百分比
    BOOL bVisible;   // 光标的可见性,TRUE 表示光标可见,FALSE 表示光标不可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

1.5 GetConsoleCursorInfo

GetConsoleCursorInfo 是 Windows API 中的一个函数,用于检索指定控制台屏幕缓冲区的光标大小和可见性信息

C 复制代码
BOOL WINAPI GetConsoleCursorInfo(
    _In_  HANDLE hConsoleOutput,
    _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
  • hConsoleOutput :控制台屏幕缓冲区的句柄,也就是在1.3中hOutPut获取到的句柄。
  • lpConsoleCursorInfo :指向 CONSOLE_CURSOR_INFO 结构的指针,用于接收光标信息。

所以获取控制台光标信息可以这样做:

C 复制代码
	// 定义一个 CONSOLE_CURSOR_INFO 结构体变量
	CONSOLE_CURSOR_INFO curSorInfo;
	// 获取当前控制台光标信息存储在 curSorInfo 中
	GetConsoleCursorInfo(hOutPut, &curSorInfo);

1.6 SetConsoleCursorInfo

顾名思义,这是一个设置控制台光标的函数。 SetConsoleCursorInfo 函数 - Windows Console | Microsoft Learn

这里的两个参数分别是:

  • hConsoleOutput:控制台屏幕缓冲区的句柄。
  • lpConsoleCursorInfo :指向CONSOLE_CURSOR_INFO 结构的指针。

接下来就是隐藏控制台光标:

C 复制代码
	// 获取标准输出(控制台)的句柄
	HANDLE hOutPut = NULL;
	hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);

	// 定义一个 CONSOLE_CURSOR_INFO 结构体变量
	CONSOLE_CURSOR_INFO curSorInfo;
	// 获取当前控制台光标信息存储在 curSorInfo 中
	GetConsoleCursorInfo(hOutPut, &curSorInfo);
	// 将 bVisible 更改为 false,代表光标不可见
	curSorInfo.bVisible = false;
	// 最后设置控制台光标
	SetConsoleCursorInfo(hOutPut, &curSorInfo);

1.7 SetConsoleCursorPosition

SetConsoleCursorPosition 是 Windows API 中的一个函数,用于设置控制台光标的位置。可以通过调用这个函数将光标设置到指定位置。SetConsoleCursorPosition 函数 - Windows Console | Microsoft Learn

可以看到有两个参数:

  • hConsoleOutput:控制台屏幕缓冲区的句柄。
  • dwCursorPosition :一个 COORD 结构体(在1.2中讲述),指定光标的新位置。

使用方法:

C 复制代码
	COORD pos = { 20,15 };
	// 获取标准输出(控制台)的句柄
	HANDLE hOutPut = NULL;
	hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
	// 设置控制台光标位置为 pos
	SetConsoleCursorPosition(hOutPut, pos);

1.8 GetAsyncKeyState

GetAsyncKeyState 是 Windows API 中的一个函数,用于检测某个键的状态,即该键是否被按下或释放。

函数原型:

C 复制代码
SHORT GetAsyncKeyState(int vKey);

这里的参数vKey代表着虚拟键码,以下是一些常见的虚拟键码及其对应的按键:

虚拟键码 描述 对应按键
VK_LBUTTON 左鼠标按钮 鼠标左键
VK_RBUTTON 右鼠标按钮 鼠标右键
VK_CANCEL 取消键 Ctrl + Break
VK_BACK 退格键 Backspace
VK_TAB 制表键 Tab
VK_CLEAR 清除键 Num Pad上的Clear键
VK_RETURN 回车键 Enter
VK_SHIFT Shift键 Shift
VK_CONTROL Ctrl键 Ctrl
VK_MENU Alt键 Alt
VK_PAUSE 暂停键 Pause
VK_CAPITAL 大写锁定键 Caps Lock
VK_ESCAPE 退出键 Esc
VK_SPACE 空格键 Space
VK_PRIOR Page Up键 Page Up
VK_NEXT Page Down键 Page Down
VK_END End键 End
VK_HOME Home键 Home
VK_LEFT 左方向键 左箭头
VK_UP 上方向键 上箭头
VK_RIGHT 右方向键 右箭头
VK_DOWN 下方向键 下箭头
VK_SELECT 选择键 Num Pad上的Select键
VK_PRINT 打印键 Print Screen
VK_EXECUTE 执行键 Num Pad上的Enter键
VK_SNAPSHOT 系统快照键 Print Screen
VK_INSERT 插入键 Insert
VK_DELETE 删除键 Delete
VK_HELP 帮助键 F1
VK_F1 功能键 F1 F1
VK_F2 功能键 F2 F2
VK_F3 功能键 F3 F3
VK_F4 功能键 F4 F4
VK_F5 功能键 F5 F5
VK_F6 功能键 F6 F6
VK_F7 功能键 F7 F7
VK_F8 功能键 F8 F8
VK_F9 功能键 F9 F9
VK_F10 功能键 F10 F10
VK_F11 功能键 F11 F11
VK_F12 功能键 F12 F12

参考文档:虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn

GetAsyncKeyState 返回一个 SHORT 类型的值,该值的低位和高位分别表示键的状态:

  • 低位(第 1 位)

    • 0:键未被按下。
    • 1:键已被按下。

所以要判断一个键是否被按过,可以检测GetAsyncKeyState的返回值最低位是否为1,即& 0x1

C 复制代码
// 这里以左方向键为例
(GetAsyncKeyState(VK_LEFT) & 0x1) ? 1 : 0;

2. 具体分析

2.1 实现效果

2.2 宽字符

在上图可以看到打印出来的墙体、蛇身等元素,占的宽比要比正常字符宽,这是因为全部使用的宽字符进行打印。

普通字符一般占用1个字节,而这类宽字符占用2个字节。

C语言的宽字符历程始于ASCII编码的局限,随着Unicode的出现,C90标准引入宽字符概念和wchar_t类型,C94补充完善了相关头文件及函数,C99改进限制并扩展格式化选项,其初衷是满足多语言表示需求,如今在多语言系统中广泛应用,尤其处理Unicode字符时表现出色。

2.3 <locale.h>

在程序中可以包含<locale.h>头文件,其中提供了允许开发者针对特定地区调整程序行为的函数。

<locale.h>提供的函数控制C标准库中对于不同的地区产生不一样行为的部分,通过修改地区可以改变行为来适应不同地区,并且C语言支持针对不同类项进行修改,每个宏都指定了其中一个类项,

详细说明参考:%> | Microsoft Learn

2.4 setlocale函数

C 复制代码
char* setlocale (int category, const char* locale);

setlocale 函数可以用于修改当前地区,修改一个类项或多个类项。

这个函数第一个参数可以是上文类项中的一个并且每次只影响一个类项,如果第一个参数是LC_ALL,就会影响所有类项。

C标准给第二个参数定义了2种取值:C"",并且在任意程序执行开始都会隐式调用。

C 复制代码
setlocale(LC_ALL, "C");

当程序运行起来后需要改变地区就需要显式调用,使用""作为第二个参数,就可以切换到本地模式,这种模式下程序会适应本地环境,支持宽字符的输出。

C 复制代码
// 适配本地环境
setlocale(LC_ALL, "");

2.5 宽字符的打印

C 复制代码
#include<locale.h>

int main()
{
	// 适配本地环境
	setlocale(LC_ALL, "");
	wchar_t ch1 = L'贪';
	wchar_t ch2 = L'吃';
	wchar_t ch3 = L'蛇';
	char a = 'a';
	char b = 'b';

	printf("%c%c\n", a, b);
	wprintf(L"%c\n", ch1);
	wprintf(L"%c\n", ch2);
	wprintf(L"%c\n", ch3);

	return 0;
}

输出结果:

正常的光标占用的一个宽度便为1个普通字符的宽度,所以可以看到1个宽字符的宽度 == 2个普通字符的宽度,所以要打印宽字符就需要处理好控制台坐标的计算。

2.6 地图分析

要在控制台指定位置打印信息,首先要知道该位置的坐标,控制台中的坐标如图,横向为X轴,从左向右增长,纵向为Y轴,从上到下增长。

假设要实现一个正方形的墙体:例如27行58列,并且使用正方体□这种宽字符打印:

2.7 蛇体与食物

初始化状态假设蛇的身体长度为5,而蛇身的每个节点使用●来打印,比如(24,5)这个位置为蛇,连续5个节点。

关于食物,我们在墙体范围内随机生成一个坐标,并且不能与蛇身以及墙体重合,使用★打印。

并且需要注意,不管是墙体或是蛇体,亦或是食物的X坐标都需要是2的倍数,否则可能会出现一半在墙体中或一半在墙外的情况。

2.8 流程设计

3. 程序设计

3.1 蛇身体、游戏状态等数据结构

在进行游戏过程中,蛇每吃一个食物,身体就会变长一节,所以使用链表存储蛇身体的信息,每一节蛇身就是链表的每个节点,而每个节点只需要记录好蛇身在控制台上的坐标就可以,所以定义一个结构体如下:

C 复制代码
// 蛇体节点
typedef struct SnakeNode

{
	int x;
	int y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

而还需要管理游戏进行的状态、蛇身的方向、当前分数等等,所以再封装一个结构体:

C 复制代码
// 贪吃蛇
typedef struct Snake
{
    pSnakeNode _pSnake;     // 维护整条蛇的指针
    pSnakeNode _pFood;      // 维护食物的指针
    enum DIRECTION _Dir;    // 蛇头的方向默认是向右
    enum GAME_STATUS _Status;   // 游戏状态
    int _Socre;     // 当前获得分数
    int _Food_Weight;       // 默认每个食物10分
    int _SleepTime;     // 每走一步休眠时间
}Snake, * pSnake;

在上面的结构体中,有一个蛇的方向,这里使用枚举来定义:

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

还有游戏运行中的几种状态:正常运行、撞墙结束、咬到自己、正常退出结束,也使用枚举:

C 复制代码
// 游戏状态
enum GAME_STATUS
{
    OK,             // 正常运行
    KILL_BY_WALL,   // 撞墙
    KILL_BY_SELF,   // 咬到自己
    END_NOMAL       // 正常结束
};

3.2 SetPos()

在运行过程中,经常需要定位光标来控制蛇的行走等行为,所以预先封装一个定位的函数:

C 复制代码
// 定位光标
void SetPos(short x, short y)
{
	// 获得标准输出设备句柄
	HANDLE hOutPut = NULL;
	hOutPut = GetStdHandle(STD_OUTPUT_HANDLE);
	// 定位光标的位置
	COORD pos = { x,y };
	SetConsoleCursorPosition(hOutPut, pos);
}

3.3 main()

在main函数中,首先使用setlocale来适配本地环境,并且在运行过程当中食物的位置也是随机生成,所以也需要初始化一下srand函数。

C 复制代码
int main()
{
	// 适配本地环境
	setlocale(LC_ALL, "");
	srand((unsigned int)time(NULL));
	test();
	return 0;
}

3.4 test()

这里使用一个do while循环。

C 复制代码
void test()
{
	char ch = 0;
	do
	{
		system("cls");
		// 创建贪吃蛇
		Snake snake = { 0 };
		// 初始化
		GameStart(&snake);
		// 运行
 		GameRun(&snake);
		// 结束回收空间
		GameEnd(&snake);

		SetPos(20, 15);
		printf("要再来一局么?(Y/N):");
		scanf(" %c", &ch);
	} while (ch == 'Y' || ch == 'y');

	SetPos(0, 27);
}

3.5 GameStart()

在这个函数里需要:

  • 设置控制台大小、名称、隐藏光标。
  • 打印欢迎界面、功能介绍页面。
  • 绘制地图
  • 创建蛇以及基本属性
  • 创建食物

3.5.1 WelcomeToGame()

这个函数里就是简单的光标定位到需要的位置,然后打印。

C 复制代码
// 打印欢迎界面
void WelcomeToGame()
{
	SetPos(40, 14);
	wprintf(L"欢迎来到贪吃蛇游戏\n");
	SetPos(41, 20);
	system("pause");

	system("cls");

	SetPos(20, 10);
	wprintf(L"使用 ↑ ↓ ← → 来控制蛇的移动,按F3加速,按F4减速\n");
	SetPos(20, 11);
	wprintf(L"加速可以得到更高的分数\n");
	SetPos(20, 13);
	wprintf(L"进入游戏后按空格键开始!!!\n");
	SetPos(41, 21);
	system("pause");
        
	system("cls");
}

3.5.2 DrawMap()

这里打印墙体需要用到一个正方形的宽字符,所以在头文件中定义一个宏。

C 复制代码
#define WALL L'□'

使用循环上下左右打印就可以。

C 复制代码
// 绘制地图
void DrawMap()
{
	// 上
	for (int i = 0; i < 29; i++)
	{
		wprintf(L"%lc", WALL);
	}

	// 下
	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);
	}
}

这里需要注意,X轴的打印是在一行中,所以只需开始定位一次光标就可以,但Y轴的打印是每一个都需要换行,所以要在循环里定位。

3.5.3 InitSnake()

在这里创建蛇以及基本属性。

每开辟一个节点都需要为蛇身体赋值坐标,并且蛇的身体使用来打印,所以再定义3个宏。

C 复制代码
#define POS_X 24
#define POS_Y 5
#define BODY L'●'

初始化蛇身体为5个节点,所以动态开辟5个节点,每申请一个节点就为节点赋值坐标,并且进行头插,全部申请完后打印蛇的身体。

最后设置相关状态:初始蛇头朝向、初始分数、每个食物的分数、蛇移动的速度以及默认游戏状态。

C 复制代码
// 初始化蛇
void InitSnake(pSnake ps)
{
	pSnakeNode cur = NULL;

	for (int i = 0; i < 5; i++)
	{
		// 开辟蛇身体节点
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		if (cur == NULL)
		{
			perror("InitSnake()::malloc() fail!");
			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;
	while (cur)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		cur = cur->next;
	}

	// 设置贪吃蛇的属性
	ps->_Dir = RIGHT;	// 默认向右
	ps->_Socre = 0;
	ps->_Food_Weight = 10;
	ps->_SleepTime = 200;	// 单位毫秒
	ps->_Status = OK;
}

3.5.4 CreateFood()

食物这里使用这个字符来打印,所以再定义1个宏:

C 复制代码
#define FOOD L'★'

这个函数用来创建食物:

  1. 先随机生成X,Y的坐标,并且检查是否否是2的倍数,如不是则重新生成
  2. 检查食物坐标不能与蛇身体的坐标冲突,如冲突使用goto语句跳回,重新生成。
  3. 因为食物结构与蛇身体结构相同,故使用同一结构。
  4. 最后打印食物。
C 复制代码
// 创建食物
void CreateFood(pSnake ps)
{
	int x = 0;
	int y = 0;
	// 生成x范围2~54,y是1~25,并且x为2的倍数
again:
	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	// x和y不能和蛇的身体冲突
	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() fail!");
		return;
	}
	pFood->x = x;
	pFood->y = y;
	pFood->next = NULL;

	// 打印食物
	SetPos(x, y);
	wprintf(L"%lc", FOOD);
	ps->_pFood = pFood;

}

3.6 GameRun()

接下来是游戏运行中的核心逻辑。

这里需要检查键盘的按键状态,方法在1.8中已讲述,为了方便,我们也将其定义一个宏。

C 复制代码
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

接下来就按照玩家的虚拟键值来处理对应情况(更新相应状态)。

C 复制代码
// 运行
void GameRun(pSnake ps)
{
	// 打印帮助信息
	PrintHelpInfo();

	do
	{
		// 打印分数
		SetPos(64, 10);
		printf("当前总得分为:%d", ps->_Socre);
		SetPos(64, 11);
		printf("当前食物分数为:%2d", 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_ESCAPE))
		{
			// 退出
			ps->_Status = END_NOMAL;
		}
		else if (KEY_PRESS(VK_SPACE))
		{
			// 暂停
			Pause();
		}
		else if (KEY_PRESS(VK_F3))
		{
			// 加速时,将休眠速度-30,最低不低于80,并且每次加速食物分数+2
			if (ps->_SleepTime > 80)
			{
				ps->_SleepTime -= 30;
				ps->_Food_Weight += 2;
			}
		}
		else if (KEY_PRESS(VK_F4))
		{
			// 减速将休眠速度+30,食物-2分,以食物控制,最低不低于2分
			if (ps->_Food_Weight > 2)
			{
				ps->_SleepTime += 30;
				ps->_Food_Weight -= 2;
			}
		}

		// 贪吃蛇走一步
		SnakeMove(ps);
		Sleep(ps->_SleepTime);
	} while (ps->_Status == OK);
}

3.6.1 PrintHelpInfo()

在2.1中可以看到:进入游戏页面后需要在右部分打印帮助信息。

这里比较简单,仍是定位 -> 打印即可。

C 复制代码
// 打印帮助信息
void PrintHelpInfo()
{
	SetPos(64, 15);
	wprintf(L"%ls", L"不可以穿墙,不可以咬到自己");
	SetPos(64, 16);
	wprintf(L"%ls", L"使用 ↑ ↓ ← → 控制蛇的移动");
	SetPos(64, 17);
	wprintf(L"%ls", L"按F3加速,按F4减速");
	SetPos(64, 18);
	wprintf(L"%ls", L"ESC退出游戏,Space暂停游戏");
	SetPos(64, 20);
	wprintf(L"%ls", L"制作者:@444A4E");
}

3.6.2 Pause()

当玩家按空格键暂停的时候,就调用这个函数,实现起来也很容易,只需要循环睡眠即可,再次空格就跳出循环。

C 复制代码
// 暂停
void Pause()
{
    while (1)
    {
		Sleep(200);
		if (KEY_PRESS(VK_SPACE))
		{
			break;
		}
    }
}

3.6.3 SnakeMove()

  • 创建一个节点,表示蛇下一个要走的位置。
  • 使用switch分支检查当前蛇的方向,走对应的case语句,更改新创建的节点位置。
  • 判断下一个节点是否是食物,如果是食物的处理与不是食物的处理
  • 判断是否撞墙或撞到自己
C 复制代码
// 蛇的移动
void SnakeMove(pSnake ps)
{
	// 创建一个节点,表示蛇下一个即将到达的位置
	pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (next == NULL)
	{
		perror("SnakeMove()::malloc() fail!");
		return;
	}

	switch (ps->_Dir)
	{
	case UP:
		next->x = ps->_pSnake->x;
		next->y = ps->_pSnake->y - 1;
		break;
	case DOWN:
		next->x = ps->_pSnake->x;
		next->y = ps->_pSnake->y + 1;
		break;
	case LEFT:
		next->x = ps->_pSnake->x - 2;
		next->y = ps->_pSnake->y;
		break;
	case RIGHT:
		next->x = ps->_pSnake->x + 2;
		next->y = ps->_pSnake->y;
		break;
	}

	// 判断下一个坐标是否是食物
	if (NextIsFood(next, ps))
	{
		EatFood(next, ps);
	}
	else
	{
		NoFood(next, ps);
	}

	// 是否撞墙
	KillByWAll(ps);

	// 是否撞到自己
	KillBySelf(ps);
}
3.6.3.1 NextIsFood()

检查蛇走的下一个位置是否与食物的位置相等然后返回。

C 复制代码
// 判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pnext, pSnake ps)
{
	return (pnext->x == ps->_pFood->x && pnext->y == ps->_pFood->y);
}
3.6.3.2 EatFood()
  1. 如果下一步是食物,则将节点头插,改变头节点。
  2. 打印蛇身体。
  3. 加分,并且重新创建新的食物。
C 复制代码
// 吃食物
void EatFood(pSnakeNode pnext, pSnake ps)
{
	// 头插
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	// 释放next节点
	free(pnext);
	pnext = NULL;

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

	// 加分
	ps->_Socre += ps->_Food_Weight;

	// 重新创建食物
	CreateFood(ps);
}
3.6.3.3 NoFood()

如果不是食物的话:

  1. 头插一个节点
  2. 循环打印蛇身体
  3. 将最后一个节点打印为空白
  4. 释放掉最后一个节点
C 复制代码
// 不是食物处理
void NoFood(pSnakeNode pnext, pSnake ps)
{
	// 头插
	pnext->next = ps->_pSnake;
	ps->_pSnake = pnext;

	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next)
	{
		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;
}
3.6.3.4 KillByWAll()

通过判断蛇的坐标是否与四边墙的坐标相同确定是否撞墙,如果撞墙则更新状态为KILL_BY_WALL

C 复制代码
// 撞墙处理
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;
	}
}
3.6.3.5 KillBySelf()

从第二个节点开始遍历,判断蛇头坐标是否与之后的节点相同,如果相同则更新状态为KILL_BY_SELF

C 复制代码
// 撞到自己处理
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;
	}
}

3.7 GameEnd()

GameRun()函数运行结束后,判断当前状态确定是以什么状态结束游戏,并打印对应提示信息,释放链表。

C 复制代码
// 游戏结束处理
void GameEnd(pSnake ps)
{
	SetPos(20, 12);
	switch (ps->_Status)
	{
	case END_NOMAL:
		printf("您已退出游戏\n");
		break;
	case KILL_BY_SELF:
		printf("您撞到了自己,游戏结束\n");
		break;
	case KILL_BY_WALL:
		printf("您撞到墙了,游戏结束\n");
		break;
	}

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

	ps->_pSnake = NULL;
	ps->_pFood = NULL;
}

4. 源码分享

[github](444A4E/Snake: 控制台实现贪吃蛇游戏)

相关推荐
Bryan_Long7 小时前
如何编译运行一个 C/C++ 文件
c++·c
零零时2 天前
【算法学习之路】5.贪心算法
开发语言·数据结构·c++·学习·算法·贪心算法·c
局外人_Jia3 天前
【简单的C++围棋游戏开发示例】
开发语言·c++·c·visual studio
易保山3 天前
MIT6.S081 - Lab2(系统调用)实验笔记
linux·操作系统·c
newki7 天前
学习笔记,从C到C++入门,总结一篇
android·c++·c
Ronin-Lotus12 天前
嵌入式硬件篇---常用的汇编语言指令
单片机·嵌入式硬件·职场和发展·c·汇编语言
大招至胜12 天前
Mac下VSCode调试skynet的lua环境配置
lua·c
小小小白的编程日记12 天前
List的模拟实现(2)
c
Dongwoo Jeong14 天前
缓存基础解释与缓存友好型编程基础
后端·c·cache·cache friendly