贪吃蛇(C语言实现,API)

一、游戏背景

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

本贪吃蛇游戏基于C语言开发,采用控制台窗口作为游戏界面,通过键盘方向 键控制蛇的移动,蛇吃到随机生成的食物后身体增长、分数增加 ,若蛇头触碰墙壁或自身身体则游戏结束 ,整体通过循环刷新界面坐标控制按键检测实现经典贪吃蛇的核心玩法,代码简洁易懂,适合 C 语言初学者学习图形控制、逻辑判断和简单游戏开发

二、游戏效果演示



三、贪吃蛇基本功能

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

实现基本的功能:

• 贪吃蛇地图绘制

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

• 蛇撞墙死亡

• 蛇撞⾃⾝死亡

• 计算得分

• 蛇⾝加速、减速

• 暂停游戏

四、需要的知识

C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等

五、Win32 API介绍

实现贪吃蛇会使⽤到的⼀些Win32 API知识

1.Win32 API

Windows 作为多任务操作系统,不仅负责统筹应用程序运行、内存分配与系统资源管理,更是一个庞大的系统服务平台。应用程序可通过调用平台提供的各类系统服务(每项服务对应一个功能函数),实现窗口创建、图形绘制、外设调用等操作。由于这些函数服务于应用程序开发,因此被称为应用程序编程接口,即 API 函数;而 WIN32 API 专指微软 Windows 32 位平台下的应用程序编程接口

2.控制台程序(Console)

平常运⾏起来的**⿊框程序** 其实就是控制台程序

可以使⽤cmd命令来设置控制台窗⼝的**⻓宽**:设置控制台窗⼝的⼤⼩,30⾏,100列

c 复制代码
mode con cols=100 lines=30

参考:mode指令

还可以通过命令设置控制台窗⼝的名字

c 复制代码
title 贪吃蛇

参考:title指令

这些指令能在控制台窗⼝执⾏的命令也可以调⽤C语⾔函数 system来执⾏

c 复制代码
include <stdio.h>
int main()
{
 //设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
    system("mode con cols=100 lines=30");
 //设置cmd窗⼝名称
    system("title 贪吃蛇");
    getchar();
    return 0;
}

3.控制台屏幕上的坐标 COORD

参考:COORD

COORDWindows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系 (0,0)原点位于缓冲区的顶部左侧单元格

COORD类型的声明:

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

给坐标赋值:

c 复制代码
COORD pos = { 10, 15 };

4.GetStdHandle

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

c 复制代码
HANDLE GetStdHandle(DWORD nStdHandle);

如:

c 复制代码
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

5.GetConsoleCursorInfo

参考:GetConsoleCursorInfo

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

c 复制代码
BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
//PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息

5.1.CONSOLE_CURSOR_INFO

c 复制代码
typedef struct _CONSOLE_CURSOR_INFO 
{
	DWORD dwSize;
	BOOL bVisible;
}
  1. dwSize光标填充的字符单元格的百分⽐ 。 此值介于1到100之间 。 光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条
  2. bVisible游标的可⻅性。 如果光标可⻅,则此成员为 TRUE
c 复制代码
CursorInfo.bVisible = false; //隐藏控制台光标

6.SetConsoleCursorInfo

参考:SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性

c 复制代码
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo)

如:

c 复制代码
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;

//获取控制台光标信息
GetConsoleCursorInfo(hOutput, &CursorInfo);

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

//设置控制台光标状态
SetConsoleCursorInfo(hOutput, &CursorInfo);

7.SetConsoleCursorPosition

参考:SetConsoleCursorPosition

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

c 复制代码
BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput, COORD pos)

如:

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

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

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

8. GetAsyncKeyState

参考:GetAsyncKeyState

获取按键情况,GetAsyncKeyState 的函数原型如下:

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

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位short数据中,最⾼位是 1说明按键的状态是按下如果最⾼是 0说明按键的状态是抬起 ;如果最低位被置为1则说明,该按键被按过,否则为0

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

参考:虚拟键码 (Winuser.h) - Win32 apps

六、贪吃蛇游戏设计与分析

1.地图



1.1.setlocale 函数

参考:setlocale

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

setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。

setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数LC_ALL,就会影响所有的类项。

C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和 ""(本地模式)。

在任意程序执⾏开始,都会隐藏式执⾏调⽤:

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

当地区设置为"C"时,设置为C语⾔默认的模式,这时库函数按正常⽅式执⾏。

当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤""作为第2个参数,调⽤setlocale

数就可以切换到本地模式,这种模式下程序会适应本地环境。

⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等

c 复制代码
setlocale(LC_ALL, "");//切换到本地环境

setlocale 的返回值是⼀个字符串指针,表⽰已经设置好的格式。如果调⽤失败,则返回空指针 NULL
setlocale() 可以⽤来查询当前地区,这时第⼆个参数设为 NULL 就可以了。

c 复制代码
#include <locale.h>
int main()
{
	char* loc;
	loc = setlocale(LC_ALL, NULL);
	printf("默认的本地信息:%s\n", loc);
	loc = setlocale(LC_ALL, "");
	printf("设置后的本地信息: %s\n", loc);
	return 0;
}

1.2.宽字符打印

在游戏地图上,我们打印墙体使⽤宽字符 :□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★普通的字符是占1个字节的 ,这类宽字符是占⽤2个字节

宽字符的字⾯量必须加上前缀 L ,否则 C 语⾔会把字⾯量当作窄字符类型处理。前缀 L在单引号前⾯,表⽰宽字符,宽字符的打印使⽤ wprintf ,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls

2.蛇⾝和⻝物

初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处

注意

蛇的每个节点的x坐标必须是2个倍数 ,否则可能会出现蛇的**⼀个节点有⼀半⼉出现在墙体中** ,另外⼀般在墙外的现象,坐标不好对⻬

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

3.结构设计

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

c 复制代码
// 贪吃蛇节点结构体
// 作用:表示贪吃蛇身体的每一个小方块
typedef struct SnakeNode
{
    // 节点的横坐标(在控制台/游戏界面的 X 位置)
    int x;
    // 节点的纵坐标(在控制台/游戏界面的 Y 位置)
    int y;
    // 指针:指向下一个身体节点,形成链表结构
    struct SnakeNode* next;
}
// 结构体别名:直接用 SnakeNode 表示节点
SnakeNode,
// 指针别名:pSnakeNode 等价于 struct SnakeNode*
* pSnakeNode;

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

c 复制代码
// 贪吃蛇游戏总控结构体
// 作用:管理整个游戏的所有数据(蛇、食物、分数、状态、方向等)
typedef struct Snake
{
    // 指向蛇头节点的指针,用来维护整条蛇的链表结构
    pSnakeNode _pSnake;

    // 指向食物节点的指针,管理游戏中食物的位置
    pSnakeNode _pFood;

    // 蛇头当前移动的方向(上/下/左/右),默认向右
    enum DIRECTION _Dir;

    // 游戏当前状态(正常运行/游戏结束/暂停等)
    enum GAME_STATUS _Status;

    // 游戏当前获得的总分数
    int _Score;

    // 每个食物的分值,默认每个食物 10 分
    int _foodWeight;

    // 蛇每移动一步的休眠时间(控制游戏速度)
    int _SleepTime;
}
// 结构体别名:直接用 Snake 表示整个游戏对象
Snake,
// 指针别名:pSnake 等价于 struct Snake*
* pSnake;

蛇的⽅向,可以⼀⼀列举,使⽤枚举

c 复制代码
// 方向枚举
// 作用:定义蛇头的四个移动方向
enum DIRECTION
{
    UP,    // 向上
    DOWN,  // 向下
    LEFT,  // 向左
    RIGHT  // 向右(默认方向)
};

游戏状态,可以⼀⼀列举,使⽤枚举

c 复制代码
//游戏状态
enum GAME_STATUS
{
    OK,//正常运⾏
    KILL_BY_WALL,//撞墙
    KILL_BY_SELF,//咬到⾃⼰
    END_NOMAL//正常结束
};

4.游戏流程设计

4.1.游戏开始 - GameStart

  1. 设置游戏窗口的大小
  2. 设置窗口的名字
  3. 隐藏屏幕光标
  4. 打印欢迎界面 - WelcomeToGame
  5. 创建地图 - CreateMap
  6. 初始化蛇身 - nitSnake
  7. 创建食物 -CreateFood

4.2.游戏运行 - GameRun

  1. 右侧打印帮助信息 - PrintHelpInfo
  2. 打印当前已获得分数和每个食物的分数
  3. 获取按键情况 -KEY_PRESS
  4. 根据按键情况移动蛇 -SnakeMove

4.3.SnakeMove(蛇移动子流程)

  1. 根据蛇头的坐标和方向,计算下一个节点的坐标
  2. 判断下一个节点是否是食物 - NextIsFood
  3. 是食物就吃掉 - EatFood
  4. 不是食物,吃掉食物,尾巴删除一节 - NoFood
  5. 判断是否撞墙 - KillByWall
  6. 判断是否装上自己 - KillBySelf

4.4.游戏结束 - GameEnd

  1. 告知游戏结束的原因
  2. 释放蛇身节点

七、有戏逻辑实现分析

1.游戏主逻辑

程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑。

主逻辑分为3个过程:

  1. 游戏开始GameStart完成游戏的初始化
  2. 游戏运⾏GameRun完成游戏运⾏逻辑的实现
  3. 游戏结束GameEnd完成游戏结束的说明,实现资源释放
c 复制代码
#include "Snake.h"

// 游戏核心流程函数
void game()
{
	// 接收用户是否再来一局的输入字符
	char ch = 0;

	// 循环:支持游戏重开
	do
	{
		// 清屏,保证每局游戏界面干净
		system("cls");

		// 定义贪吃蛇游戏结构体变量,初始化为0
		Snake snake = { 0 };

		// 游戏初始化:创建蛇、食物、设置初始状态
		GameStart(&snake);

		// 游戏运行:移动、按键、碰撞、吃食物等核心逻辑
		GameRun(&snake);

		// 游戏结束:释放内存、销毁蛇、清理界面
		GameEnd(&snake);

		// 将光标定位到界面下方,提示用户是否重开
		SetPos(76, 29);
		printf("是否再来一局(Y/N):");

		// 获取用户输入
		ch = getchar();

		// 清空输入缓冲区,防止残留字符影响下一次输入
		while (getchar() != '\n');
	} 
	// 用户输入Y/y,重新开始一局
	while (ch == 'Y' || ch == 'y');

	// 游戏完全退出,将光标归位
	SetPos(0, 53);
}

// 主函数:程序入口
int main()
{
	// 设置本地语言环境,解决中文乱码问题
	setlocale(LC_ALL, "");

	// 设置随机数种子,保证食物随机生成
	srand((unsigned)time(NULL));

	// 启动游戏逻辑
	game();

	// 程序正常结束
	return 0;
}

2. 游戏开始GameStart

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

  1. 控制台窗⼝⼤⼩的设置
  2. 控制台窗⼝名字的设置
  3. ⿏标光标的隐藏
  4. 打印欢迎界⾯
  5. 创建地图
  6. 初始化蛇
  7. 创建第⼀个⻝物
c 复制代码
//游戏的初始化
//功能:启动游戏时的所有准备工作(窗口、光标、地图、蛇、食物)
void GameStart(pSnake ps)
{
	//设置控制台窗口大小:宽180列,高55行
	system("mode con cols=180 lines=55");

	//设置控制台窗口标题名称:贪吃蛇
	system("title 贪吃蛇");

	//获取标准输出句柄(用于操作控制台光标)
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	//定义光标信息结构体
	CONSOLE_CURSOR_INFO CurserInfo;
	//获取当前控制台光标信息
	GetConsoleCursorInfo(houtput, &CurserInfo);
	//设置光标不可见(游戏界面更美观)
	CurserInfo.bVisible = false;
	//将设置应用到控制台
	SetConsoleCursorInfo(houtput, &CurserInfo);

    //打印游戏欢迎界面与操作说明
	WelcomeToGame();

	//绘制游戏地图边框
	CreateMap();

	//初始化蛇的身体、方向、状态等信息
	InitSnake(ps);

	//随机创建第一个食物
	CreateFood(ps);
}

2.1.打印欢迎界⾯

WelcomeToGame函数实现逻辑:

该函数用于展示游戏启动前的欢迎界面与操作说明,通过光标定位函数在控制台指定位置打印欢迎语,暂停等待玩家按键确认后清屏;接着再次定位光标打印详细操作说明,包括方向键控制、加速减速规则等内容,再次暂停等待玩家按键后清屏,完成游戏前的引导流程,为玩家提供清晰的操作指引

c 复制代码
//打印界面和功能介绍
//功能:展示游戏欢迎语 + 操作说明,按任意键继续
void WelcomeToGame()
{
	//设置光标位置:x=82,y=27
	SetPos(82,27);
	//打印游戏欢迎语(宽字符,支持中文)
	wprintf(L"欢迎来到贪吃蛇小游戏\n");

	//设置光标位置:x=82,y=30
	SetPos(82, 30);
	//暂停,提示按任意键继续
	system("pause");
	//清屏
	system("cls");

	//设置光标位置:x=50,y=27
	SetPos(50, 27);
	//打印游戏操作说明:方向键控制、F3加速、F4减速
	wprintf(L"用 ↑ ↓ ← → 来控制蛇的移动,按 F3 加速,按 F4 减速,加速能得到更高的分数\n");

	//设置光标位置:x=80,y=30
	SetPos(80, 30);
	//暂停,按任意键开始游戏
	system("pause");
	//清屏,进入游戏界面
	system("cls");
}

2.2.创建地图

创建地图就是将墙打印出来,因为是宽字符打印 ,所有使⽤wprintf函数,打印格式串前使⽤L

c 复制代码
// 墙:定义地图边框的显示符号
// L'□' 表示宽字符方块,用于绘制游戏地图的围墙
#define WALL L'□'

创建地图函数CreateMap

该函数用于绘制贪吃蛇游戏的闭合地图围墙,通过循环与光标定位分四部分完成:首先从左上角开始横向打印上边界围墙,接着定位到控制台底部横向打印下边界围墙,再分别沿最左侧与最右侧纵向打印左右边界围墙,最终形成一个封闭的矩形游戏区域,限制蛇的移动范围

c 复制代码
//绘制地图
//功能:使用 WALL 符号绘制游戏的上下左右围墙
void CreateMap()
{
	// 绘制【上围墙】,从坐标(0,0)开始,横向打印70个墙方块
	for (int i = 0; i < 70; i++)
	{
		wprintf(L"%lc", WALL);
	}

    // 绘制【下围墙】,先将光标定位到 (0, 50),再横向打印70个墙方块
	SetPos(0, 50);
	for (int i = 0; i < 70; i++)
	{
		wprintf(L"%lc", WALL);
	}

	// 绘制【左围墙】,纵向打印,从y=1到y=49,x坐标固定为0
	for (int i = 1; i < 50; i++)
	{
		SetPos(0, i);
		wprintf(L"%lc", WALL);
	}
	
	// 绘制【右围墙】,纵向打印,从y=1到y=49,x坐标固定为138
	for (int i = 1; i < 50; i++)
	{
		SetPos(138, i);
		wprintf(L"%lc", WALL);
	}
}

2.3.初始化蛇⾝

创建5个节点,然后将每个节点存放在链表中进⾏管理

创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。

再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,每个⻝物的分数

  1. 游戏状态是:OK
  2. 蛇的移动速度:200毫秒
  3. 蛇的默认⽅向:RIGHT
  4. 初始成绩:0
  5. 每个⻝物的分数:10
  6. 蛇的初始位置:(70,30)

蛇⾝打印的宽字符:

c 复制代码
// 蛇身的符号
// L'●' 表示实心圆,作为贪吃蛇身体节点的显示图案
#define BODY  L'●'

蛇的初始位置:(70,30)

c 复制代码
//蛇的初始位置
#define POS_X 70
#define POS_Y 30

初始化蛇⾝函数:InitSnake

c 复制代码
//初始化蛇
//功能:创建蛇的初始身体(5个节点),设置位置、打印蛇身、初始化游戏属性
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() failed");
			exit(-1);
		}
		cur->next = NULL;
		// 设置节点坐标:x 依次递增,y 保持不变(水平向右)
		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->_direction = RIGHT;  // 默认向右移动
	ps->_score = 0;          // 初始分数 0
	ps->_food_weight = 10;   // 每个食物 10 分
	ps->_sleep_time = 200;   // 移动间隔 200ms(控制速度)
	ps->_status = OK;        // 游戏状态正常
}

2.4.创建⼀个⻝物

  1. 先随机⽣成⻝物的坐标
  2. x坐标必须是2的倍数
  3. ⻝物的坐标得在墙体内部
  4. ⻝物的坐标不能和蛇⾝每个节点的坐标重复
  5. 创建⻝物节点,打印⻝物

⻝物打印的宽字符:

c 复制代码
//食物的符号
#define FOOD  L'★'

创建⻝物的函数:CreateFood

c 复制代码
// 功能:在地图随机位置创建食物,且不与蛇身重叠
void CreateFood(pSnake ps)
{
	// 定义食物坐标变量
	int x = 0;
	int y = 0;
	
// 标签:坐标冲突时跳回此处重新生成
again:
	// 随机生成食物坐标,并保证 x 坐标为偶数(与蛇身对齐)
	do
	{
		// 随机生成 x 坐标:范围 2 ~ 137
		x = (rand() % 135) + 2;
		// 随机生成 y 坐标:范围 1 ~ 49
		y = (rand() % 49) + 1;
	} while (x % 2 != 0);

	// 遍历蛇身,检测食物坐标是否与蛇身冲突
	pSnakeNode cur = ps->_pSnake;
	while (cur)
	{
		// x、y 坐标与蛇身节点完全相同,说明冲突
		if (x == cur->x && y == cur->y)
		{
			// 跳回重新生成坐标
			goto again;
		}
		// 遍历下一个蛇身节点
		cur = cur->next;
	}
	 
	// 动态分配食物节点内存
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (pFood == NULL)
	{
		// 内存分配失败,打印错误信息
		perror("CreateFood()::malloc() failed");
		// 退出程序
		exit(-2);
	}
	// 给食物节点赋值 x 坐标
	pFood->x = x;
	// 给食物节点赋值 y 坐标
	pFood->y = y;
	// 初始化食物节点 next 指针为空
	pFood->next = NULL;

	// 光标定位到食物坐标
	SetPos(x, y);
	// 打印食物图案
	wprintf(L"%lc", FOOD);

	// 将创建好的食物节点保存到游戏结构体
	ps->_pFood = pFood;
}

3. 游戏运⾏GameRun

该函数是贪吃蛇游戏的核心运行循环,首先打印右侧操作帮助信息,随后进入循环:实时刷新显示总分数与当前食物分数;通过宏检测玩家按键,实现上下左右方向控制(禁止反向)、空格暂停 / 继续、F3 加速、F4 减速、ESC 退出游戏等功能;每次循环调用蛇移动函数让蛇前进一格,并根据设置的休眠时间控制移动速度;只要游戏状态保持正常,循环就持续执行,直到撞墙、自撞或主动退出才终止循环,结束游戏运行

需要的虚拟按键:

  1. 上:VK_UP
  2. 下: VK_DOWN
  3. 左: VK_LEFT
  4. 右: VK_RIGHT
  5. 空格: VK_SPACE
  6. ESC: VK_ESCAPE
  7. F3: VK_F3
  8. F4: VK_F4
c 复制代码
在这里插入代码片// 功能:游戏运行核心逻辑
void GameRun(pSnake ps)
{
	// 打印游戏右侧的操作帮助信息
	PrintHelpInfo();
	// 游戏主循环:只要状态正常就持续运行
	do
	{
		// 定位光标并实时打印【总分数】
		SetPos(145, 24);
		printf("总分数:%d", ps->_score);
		// 定位光标并实时打印【当前食物的分数】
		SetPos(145, 26);
		printf("当前食物的分数:%2d", ps->_food_weight);

		// 检测 上 键:不能直接向下掉头
		if (KEY_PRESS(VK_UP) && ps->_direction != DOWN)
		{
			ps->_direction = UP;
		}
		// 检测 下 键:不能直接向上掉头
		else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
		{
			ps->_direction = DOWN;
		}
		// 检测 左 键:不能直接向右掉头
		else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
		{
			ps->_direction = LEFT;
		}
		// 检测 右 键:不能直接向左掉头
		else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
		{
			ps->_direction = RIGHT;
		}
		// 检测 空格 键:暂停/继续游戏
		else if (KEY_PRESS(VK_SPACE))
		{
			//调用暂停函数
			Pause();
		}
		// 检测 F3 键:加速(速度上限60ms)
		else if (KEY_PRESS(VK_F3))
		{
			// 加速
			if (ps->_sleep_time >= 60)
			{
				ps->_sleep_time -= 30;
				ps->_food_weight += 2;
			}
		}
		// 检测 F4 键:减速(分数下限2)
		else if (KEY_PRESS(VK_F4))
		{
			// 减速
			if (ps->_food_weight >= 2)
			{
				ps->_sleep_time += 30;
				ps->_food_weight -= 2;
			}
		}
		// 检测 ESC 键:正常退出游戏
		else if (KEY_PRESS(VK_ESCAPE))
		{
			//正常退出游戏
			ps->_status = END_NORMAL;
		}

        // 执行蛇走一步的完整逻辑(移动/吃食物/碰撞检测)
		SnakeMove(ps);
		// 按设置的时间休眠,控制蛇的移动速度
		Sleep(ps->_sleep_time);
	// 游戏状态为 OK 时,持续循环
	} while (ps->_status == OK);
}

3.1.KEY_PRESS

检测按键状态,封装了⼀个宏

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

3.2.PrintHelpInfo

该函数用于在游戏界面右侧固定位置打印操作帮助信息,通过光标定位函数将提示文字依次显示在指定坐标处,内容包含游戏规则、方向控制、加速减速、退出及暂停方式,让玩家在游戏过程中随时查看操作说明,提升游戏体验

c 复制代码
// 功能:在游戏界面右侧打印操作帮助信息
void PrintHelpInfo()
{
	// 定位光标到 (145, 6),打印游戏规则:不能穿墙、不能咬到自己
	SetPos(145, 6);
	wprintf(L"%ls",L"不能穿墙,不能咬到自己");	
	
	// 定位光标到 (145, 8),打印:方向键控制移动
	SetPos(145, 8);
	wprintf(L"%ls", L"用 ↑ ↓ ← → 来控制蛇的移动");
	
	// 定位光标到 (145, 10),打印:F3加速、F4减速
	SetPos(145, 10);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	
	// 定位光标到 (145, 12),打印:加速分数更高
	SetPos(145, 12);
	wprintf(L"%ls", L"加速能得到更高的分数");
	
	// 定位光标到 (145, 14),打印:按ESC退出游戏
	SetPos(145, 14);
	wprintf(L"%ls", L"按 Esc 退出游戏");
	
	// 定位光标到 (145, 16),打印:空格暂停/开始
	SetPos(145, 16);
	wprintf(L"%ls", L"按 空格 开始/暂停游戏");
}

3.3.蛇⾝移动SnakeMove

先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标,确定了下⼀个位置后,看下⼀个位置是否是⻝物NextIsFood,是⻝物就做吃⻝物处理EatFood,如果不是⻝物则做前进⼀步的处理NoFood

蛇⾝移动后,判断此次移动是否会造成撞墙KillByWall或者撞上⾃⼰蛇⾝KillBySelf,从⽽影响游戏的状态

c 复制代码
// 功能:蛇走一步的完整过程
void SnakeMove(pSnake ps)
{
	// 创建一个新节点,表示蛇即将到达的下一个位置
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	// 判断节点内存分配是否成功
	if (pNextNode == NULL)
	{
		// 分配失败,打印系统错误信息
		perror("SnakeMove()::malloc() failed");
		// 退出程序
		exit(-3);
	}

	// 根据蛇当前的移动方向,设置下一个节点的坐标
	switch(ps->_direction)
	{
		// 向上:x不变,y减1
		case UP:
			pNextNode->x = ps->_pSnake->x;
			pNextNode->y = ps->_pSnake->y - 1;
			break;

		// 向下:x不变,y加1
		case DOWN:
			pNextNode->x = ps->_pSnake->x;
			pNextNode->y = ps->_pSnake->y + 1;
			break;

		// 向左:x减2,y不变(保证与蛇身对齐)
		case LEFT:
			pNextNode->x = ps->_pSnake->x - 2;
			pNextNode->y = ps->_pSnake->y;
			break;

		// 向右:x加2,y不变(保证与蛇身对齐)
		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);
}
3.3.1.NextIsFood

该函数用于判断蛇的下一个移动位置是否为食物,通过对比下一个节点坐标与食物节点的 x、y 坐标是否完全相等,返回布尔值结果,为蛇移动时执行吃食物逻辑提供判断依据

c 复制代码
// 功能:检测蛇的下一个位置是否是食物
// 参数:pn - 蛇下一个位置的节点,ps - 游戏结构体指针
// 返回值:是食物返回true,不是返回false
bool NextIsFood(pSnakeNode pn, pSnake ps)
{
	// 对比下一个节点坐标与食物坐标,完全相等则表示是食物
	return ps->_pFood->x == pn->x && ps->_pFood->y == pn->y;
}
3.3.2.EatFood

该函数用于处理蛇吃到食物后的逻辑,采用头插法将食物节点直接变为新蛇头,释放临时节点避免内存泄漏;遍历整条蛇身重新打印,更新总分数;最后调用创建食物函数,在地图上随机生成新的食物,完成吃食物、长身体、加分、刷新食物的完整流程

c 复制代码
// 功能:蛇吃到食物,身体增长、加分、生成新食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	// 头插法:将食物节点作为新蛇头,插入链表头部
	ps->_pFood->next = ps->_pSnake;
	ps->_pSnake = ps->_pFood;

	// 释放临时创建的下一个位置节点,避免内存泄漏
	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);
}
3.3.3.NoFood

该函数用于处理蛇未吃到食物的正常移动逻辑,将新节点作为蛇头插入链表头部;遍历链表到倒数第二个节点并打印蛇身,将最后一个节点位置打印为空格实现擦除效果,释放最后一个节点内存并断开链接,模拟蛇前进、尾部消失的移动效果

c 复制代码
// 功能:蛇下一步位置不是食物,正常向前移动
void NoFood(pSnakeNode pn, pSnake ps)
{
	// 新节点作为蛇头,插入链表头部
	pn->next = ps->_pSnake;
	ps->_pSnake = pn;

	// 从头遍历,直到倒数第二个节点
	pSnakeNode cur = ps->_pSnake;
	while (cur->next->next != NULL)
	{
		// 定位并打印当前节点的蛇身
		SetPos(cur->x, cur->y);
		wprintf(L"%lc", BODY);
		// 指针后移
		cur = cur->next;
	}
3.3.4.KillByWall

该函数用于检测蛇头是否撞到游戏地图的围墙,判断蛇头坐标是否触碰上下左右任意一面围墙,若撞墙则将游戏状态设置为撞墙死亡,触发游戏结束

c 复制代码
// 功能:检测蛇头是否撞到围墙,撞墙则游戏结束
void KillByWall(pSnake ps)
{
	// 判断蛇头坐标是否触碰 左/右/上/下 任意一面围墙
	if (ps->_pSnake->x == 0 || ps->_pSnake->x == 138 || ps->_pSnake->y == 0 || ps->_pSnake->y == 50)
	{
		// 设置游戏状态为:撞墙死亡
		ps->_status = KILL_BY_WALL;
	}
}
3.3.5.KillBySelf

该函数用于检测蛇头是否撞到自身身体,从蛇头的下一个节点开始遍历整条蛇身,逐一对比身体节点坐标与蛇头坐标,若坐标重合则判定蛇撞到自己,将游戏状态设置为自撞死亡,触发游戏结束

c 复制代码
// 功能:检测蛇头是否撞到自己的身体,自撞则游戏结束
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;
	}
}

4.游戏结束

该函数用于游戏结束后的善后处理,先清空控制台屏幕,根据游戏结束状态(正常退出、撞到自己、撞墙)打印对应的结束提示语;随后遍历蛇身链表,逐个释放所有节点内存,避免内存泄漏,完成游戏全部清理工作

c 复制代码
// 功能:游戏结束,打印结果并释放内存
void GameEnd(pSnake ps)
{
	// 清空控制台屏幕
	system("cls");
	// 根据游戏结束状态,打印不同的提示信息
	switch (ps->_status)
	{
		// 正常主动退出
		case END_NORMAL:
			SetPos(78, 27);
			printf("主动结束游戏\n");
			break;
		// 撞到自己身体
		case KILL_BY_SELF:
			SetPos(79, 27);
			printf("撞到自己,游戏结束\n");
			break;
		// 撞到围墙
		case KILL_BY_WALL:
			SetPos(80, 27);
			printf("撞到墙,游戏结束\n");
			break;
	}

八、所有代码

test.c

c 复制代码
// 包含自定义的贪吃蛇头文件,提供游戏相关的函数和结构体声明
#include "Snake.h"

// 游戏主逻辑函数,封装整个贪吃蛇游戏的流程控制
void game()
{
	// 定义字符变量ch,用于接收用户是否再来一局的输入(Y/N)
	char ch = 0;

	// do-while循环:先执行一次游戏,再根据用户输入判断是否重开
	do
	{
		// 调用系统命令cls,清空控制台屏幕,保证每局游戏界面干净
		system("cls");

		// 定义贪吃蛇结构体变量snake,初始化为0(清空所有成员变量)
		Snake snake = { 0 };

		// 调用游戏初始化函数,传入snake的地址,完成蛇身、地图、食物等初始设置
		GameStart(&snake);

		// 调用游戏运行函数,传入snake的地址,处理游戏核心逻辑:移动、吃食物、撞墙、撞身体等
		GameRun(&snake);

		// 调用游戏结束函数,传入snake的地址,做善后处理:释放内存、重置数据等
		GameEnd(&snake);

		// 调用设置光标位置函数,将控制台光标移动到(76,29)坐标处
		SetPos(76, 29);

		// 在光标位置输出提示语,询问玩家是否再来一局
		printf("是否再来一局(Y/N):");

		// 获取用户输入的一个字符,赋值给ch
		ch = getchar();

		// 循环读取缓冲区剩余字符,直到读到换行符\n,清空输入缓冲区,避免影响下一次输入
		while (getchar() != '\n');

	// 循环条件:如果用户输入Y或y,就重新开始一局游戏
	} while (ch == 'Y' || ch == 'y');

	// 游戏完全退出前,将光标移动到(0,53)坐标处
	SetPos(0, 53);
}

// 程序入口主函数
int main()
{
	// 设置本地语言环境,适配控制台显示中文、特殊字符等,解决乱码问题
	setlocale(LC_ALL, "");

	// 设置随机数生成器的种子,以当前系统时间为种子,保证食物随机生成位置不重复
	srand((unsigned)time(NULL));

	// 调用game函数,启动贪吃蛇游戏
	game();

	// 主函数正常结束,返回0给操作系统
	return 0;
}

Snake.h

c 复制代码
// 防止头文件被重复包含(多次引用头文件时,只编译一次)
#pragma once

// 标准输入输出头文件,提供 printf、scanf、getchar 等输入输出函数
#include <stdio.h>
// 本地化设置头文件,用于设置控制台语言环境,解决中文乱码问题
#include <locale.h>
// Windows系统API头文件,提供控制台操作、按键检测、光标定位等功能
#include <windows.h>
// C语言标准布尔类型头文件,提供 bool、true、false
#include <stdbool.h>
// 标准库头文件,提供内存分配(malloc/free)、随机数(rand)等功能
#include <stdlib.h>
// 时间头文件,提供 time 函数,用于生成随机数种子
#include <time.h>
// 控制台输入头文件,提供 _getch 等无回显按键读取函数
#include <conio.h>

// 宏定义:墙壁的符号(宽字符,适配中文控制台) L表示宽字符
#define WALL L'□'
// 宏定义:蛇身的符号
#define BODY  L'●'
// 宏定义:食物的符号
#define FOOD  L'★'

// 宏定义:蛇出生的初始X坐标
#define POS_X 70
// 宏定义:蛇出生的初始Y坐标
#define POS_Y 30

// 宏定义:检测键盘按键是否按下
// GetAsyncKeyState:WindowsAPI,检测虚拟按键状态 &1 表示按键按下
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1) ? 1 : 0)

// 枚举类型:蛇的移动方向
enum DIRECTION
{
	UP,		// 向上 0
	DOWN,	// 向下 1
	LEFT,	// 向左 2
	RIGHT	// 向右 3
};

// 枚举类型:游戏运行状态
enum GAME_STATUS
{
	OK,				// 游戏正常运行
	KILL_BY_WALL,	// 游戏结束:撞墙
	KILL_BY_SELF,	// 游戏结束:撞到自己身体
	END_NORMAL		// 游戏正常退出(玩家主动退出)
};

// 结构体类型定义:蛇身的每一个节点
typedef struct SnakeNode
{
	int x;				// 节点的X坐标(横向)
	int y;				// 节点的Y坐标(纵向)
	struct SnakeNode* next;	// 指针:指向下一个蛇身节点,形成链表结构
}SnakeNode, * pSnakeNode;	// 重命名:SnakeNode=节点结构体,pSnakeNode=节点指针

// 结构体类型定义:贪吃蛇游戏整体管理结构
typedef struct Snake
{
	pSnakeNode _pSnake;		// 指针:指向蛇头(整条蛇的链表头节点)
	pSnakeNode _pFood;		// 指针:指向当前食物的节点
	enum DIRECTION _direction;	// 当前蛇的移动方向
	enum GAME_STATUS _status;	// 当前游戏状态(正常/撞墙/撞自己/退出)
	int _food_weight;		// 单个食物的分数
	int _score;				// 玩家当前总得分
	int _sleep_time; 		// 蛇移动间隔时间(值越小移动越快,越大越慢)
}Snake,* pSnake;	// 重命名:Snake=游戏结构体,pSnake=游戏结构体指针

// 函数声明:游戏初始化(创建蛇、食物、地图、初始状态)
void GameStart(pSnake ps);

// 函数声明:打印游戏欢迎界面
void WelcomeToGame();

// 函数声明:设置控制台光标坐标(x横向,y纵向)
void SetPos(short x, short y);

// 函数声明:绘制游戏地图(围墙)
void CreateMap();

// 函数声明:初始化蛇的身体(创建初始蛇头+蛇身)
void InitSnake(pSnake ps);

// 函数声明:随机创建食物(不能出现在墙上/蛇身上)
void CreateFood(pSnake ps);

// 函数声明:游戏主运行逻辑(按键、移动、碰撞检测)
void GameRun(pSnake ps);

// 函数声明:蛇向前移动一步的核心逻辑
void SnakeMove(pSnake ps);

// 函数声明:检测蛇的下一个位置是否是食物,返回true/false
bool 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

c 复制代码
// 包含自定义贪吃蛇头文件,导入结构体、枚举、宏、函数声明
#include "Snake.h"

// 功能:设置控制台光标到指定(x,y)坐标位置
// 参数x:横向列坐标,参数y:纵向行坐标
void SetPos(short x, short y)
{
	// 定义控制台输出设备句柄变量,初始化为空指针
	HANDLE houtput = NULL;
	// 获取控制台标准输出窗口的句柄,赋值给houtput
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	// 定义COORD坐标结构体变量pos,存入x、y坐标
	COORD pos = { x,y };
	// 调用Windows控制台API,移动光标到pos坐标位置
	SetConsoleCursorPosition(houtput, pos);
}

// 功能:打印游戏欢迎界面、操作介绍、分步引导
void WelcomeToGame()
{
	// 将光标移动到坐标(82,27)位置
	SetPos(82,27);
	// 宽字符打印欢迎贪吃蛇游戏字样
	wprintf(L"欢迎来到贪吃蛇小游戏\n");
	// 光标移动到(82,30)
	SetPos(82, 30);
	// 控制台暂停,等待用户按任意键继续
	system("pause");
	// 清空整个控制台屏幕
	system("cls");
	// 光标移动到(50,27)
	SetPos(50, 27);
	// 打印游戏按键操作说明
	wprintf(L"用 ↑ ↓ ← → 来控制蛇的移动,按 F3 加速,按 F4 减速,加速能得到更高的分数\n");
	// 光标移动到(80,30)
	SetPos(80, 30);
	// 再次暂停等待按键
	system("pause");
	// 再次清屏,进入正式游戏界面
	system("cls");
}

// 功能:绘制游戏四周围墙地图
void CreateMap()
{
	// 循环打印上方围墙,一共70个围墙符号
	for (int i = 0; i < 70; i++)
	{
		// 宽字符输出围墙符号□
		wprintf(L"%lc", WALL);
	}

    // 将光标移动到下方围墙起始坐标(0,50)
	SetPos(0, 50);
	// 循环打印下方围墙,70个围墙符号
	for (int i = 0; i < 70; i++)
	{
		// 输出围墙符号
		wprintf(L"%lc", WALL);
	}

	// 循环绘制左侧围墙,纵向从第1行到第49行
	for (int i = 1; i < 50; i++)
	{
		// 光标移动到左侧围墙对应坐标(0,i)
		SetPos(0, i);
		// 输出围墙符号
		wprintf(L"%lc", WALL);
	}
	
	// 循环绘制右侧围墙,纵向从第1行到第49行
	for (int i = 1; i < 50; i++)
	{
		// 光标移动到右侧围墙对应坐标(138,i)
		SetPos(138, i);
		// 输出围墙符号
		wprintf(L"%lc", WALL);
	}
}

// 功能:初始化贪吃蛇,创建初始5节蛇身链表
// 参数ps:贪吃蛇整体结构体指针
void InitSnake(pSnake ps)
{
	// 定义临时节点指针cur,用于创建蛇身节点,初始为空
	pSnakeNode cur = NULL;
	// 循环5次,创建初始5节蛇身体
	for (int i = 0; i < 5; i++)
	{
		// 动态内存分配,申请一个蛇节点大小空间,强转为节点指针
		cur = (pSnakeNode)malloc(sizeof(SnakeNode));
		// 判断内存是否申请失败
		if (cur == NULL)
		{
			// 打印内存分配失败错误信息
			perror("nitSnake()::malloc() failed");
			// 异常退出程序,返回错误码-1
			exit(-1);
		}
		// 新节点后继指针置空
		cur->next = NULL;
		// 设置节点x坐标,初始位置向右依次偏移2格
		cur->x = POS_X + 2 * i;
		// 设置节点y坐标,保持初始行不变
		cur->y = POS_Y;

		// 判断当前蛇头是否为空(链表为空)
		if (ps->_pSnake == NULL)
		{
			// 链表为空,当前新节点直接作为蛇头
			ps->_pSnake = cur;
		}
		// 链表不为空,使用头插法插入新节点
		else
		{
			// 新节点指向原来的蛇头
			cur->next = ps->_pSnake;
			// 更新蛇头为当前新节点
			ps->_pSnake = cur;
		}
	}

	// 将cur重新指向蛇头,准备遍历打印蛇身
	cur = ps->_pSnake;
	// 遍历整条蛇链表
	while (cur)
	{
		// 光标移动到当前蛇节点坐标
		SetPos(cur->x, cur->y);
		// 打印蛇身符号●
		wprintf(L"%lc", BODY);
		// cur向后移动,遍历下一个节点
		cur = cur->next;
	}

	// 设置蛇默认移动方向为向右
	ps->_direction = RIGHT;
	// 初始化总分数为0
	ps->_score = 0;
	// 设置单个食物基础分值为10
	ps->_food_weight = 10;
	// 设置蛇初始移动间隔时间200毫秒
	ps->_sleep_time = 200;
	// 设置游戏初始状态为正常运行
	ps->_status = OK;
}

// 功能:随机生成食物,食物不能在蛇身上、不能在墙上
void CreateFood(pSnake ps)
{
	// 定义食物x坐标变量,初始0
	int x = 0;
	// 定义食物y坐标变量,初始0
	int y = 0;
	
// 定义跳转标签,坐标冲突时回到此处重新生成食物
again:
	// 循环随机生成坐标,直到x为偶数(和蛇对齐)
	do
	{
		// 随机生成x坐标范围2~136
		x = (rand() % 135) + 2;
		// 随机生成y坐标范围1~49
		y = (rand() % 49) + 1;
	} while (x % 2 != 0);

	// cur指向蛇头,开始遍历判断食物是否在蛇身上
	pSnakeNode cur = ps->_pSnake;
	// 遍历整条蛇链表
	while (cur)
	{
		// 判断食物坐标和当前蛇节点坐标重合
		if (x == cur->x && y == cur->y)
		{
			// 坐标冲突,跳转到again重新生成
			goto again;
		}
		// cur向后遍历下一个蛇节点
		cur = cur->next;
	}
	 
	// 动态分配食物节点内存
	pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
	// 判断内存分配是否失败
	if (pFood == NULL)
	{
		// 打印错误信息
		perror("CreateFood()::malloc() failed");
		// 异常退出,错误码-2
		exit(-2);
	}
	// 给食物节点赋值x坐标
	pFood->x = x;
	// 给食物节点赋值y坐标
	pFood->y = y;
	// 食物节点后继指针置空
	pFood->next = NULL;

	// 光标移动到食物坐标
	SetPos(x, y);
	// 打印食物符号★
	wprintf(L"%lc", FOOD);

	// 游戏结构体保存食物节点指针
	ps->_pFood = pFood;
}

// 功能:游戏全部初始化总函数(窗口、光标、欢迎页、地图、蛇、食物)
void GameStart(pSnake ps)
{
	// 设置控制台窗口大小:180列宽,55行高
	system("mode con cols=180 lines=55");

	// 设置控制台窗口标题为贪吃蛇
	system("title 贪吃蛇");

	// 获取控制台输出句柄
	HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	// 定义光标信息结构体变量
	CONSOLE_CURSOR_INFO CurserInfo;
	// 获取当前控制台光标属性信息
	GetConsoleCursorInfo(houtput, &CurserInfo);
	// 设置光标不可见
	CurserInfo.bVisible = false;
	// 应用新的光标设置
	SetConsoleCursorInfo(houtput, &CurserInfo);

    // 调用欢迎界面打印函数
	WelcomeToGame();
	// 调用绘制地图围墙函数
	CreateMap();
	// 调用初始化蛇身函数
	InitSnake(ps);
	// 调用创建第一个食物函数
	CreateFood(ps);
}

// 功能:在界面右侧打印游戏帮助说明文字
void PrintHelpInfo()
{
	// 光标定位,打印游戏规则
	SetPos(145, 6);
	wprintf(L"%ls",L"不能穿墙,不能咬到自己");
	// 光标定位,打印方向按键说明
	SetPos(145, 8);
	wprintf(L"%ls", L"用 ↑ ↓ ← → 来控制蛇的移动");
	// 光标定位,打印F3F4加速减速说明
	SetPos(145, 10);
	wprintf(L"%ls", L"按 F3 加速,按 F4 减速");
	// 光标定位,打印加速加分规则
	SetPos(145, 12);
	wprintf(L"%ls", L"加速能得到更高的分数");
	// 光标定位,打印ESC退出说明
	SetPos(145, 14);
	wprintf(L"%ls", L"按 Esc 退出游戏");
	// 光标定位,打印空格暂停说明
	SetPos(145, 16);
	wprintf(L"%ls", L"按 空格 开始/暂停游戏");
}

// 功能:游戏暂停函数,按空格继续
void Pause()
{
	// 死循环等待按键
	while (1)
	{
		// 休眠200毫秒,减少CPU占用
		Sleep(200);
		// 判断空格键是否按下
		if (KEY_PRESS(VK_SPACE))
		{
			// 按下空格,跳出循环,结束暂停
			break;
		}
	}
}

// 功能:判断蛇下一步位置是不是食物
// 返回true:是食物,false:不是食物
bool NextIsFood(pSnakeNode pn, pSnake ps)
{
	// 判断下一步节点坐标==食物坐标
	return ps->_pFood->x == pn->x && ps->_pFood->y == pn->y;
}


// 功能:蛇吃到食物时执行:蛇变长、加分、刷新食物
void EatFood(pSnakeNode pn, pSnake ps)
{
	// 食物节点作为新蛇头,头插法
	ps->_pFood->next = ps->_pSnake;
	// 更新蛇头为食物节点
	ps->_pSnake = ps->_pFood;

	// 释放临时申请的下一步节点内存
	free(pn);
	// 指针置空,防止野指针
	pn = NULL;

	// cur指向蛇头,准备重新打印整条蛇
	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);
}

// 功能:蛇下一步没有食物,正常移动,不变长
void NoFood(pSnakeNode pn, pSnake ps)
{
	// 新节点作为蛇头,头插
	pn->next = ps->_pSnake;
	// 更新蛇头
	ps->_pSnake = pn;
	// cur指向蛇头
	pSnakeNode cur = ps->_pSnake;

	// 遍历到倒数第二个节点
	while (cur->next->next != NULL)
	{
		// 光标定位打印蛇身
		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 == 0 || ps->_pSnake->x == 138 || ps->_pSnake->y == 0 || ps->_pSnake->y == 50)
	{
		// 设置游戏状态为撞墙死亡
		ps->_status = KILL_BY_WALL;
	}
}

// 功能:检测蛇头是否撞到自己身体
void KillBySelf(pSnake ps)
{
	// cur从蛇头下一个节点开始遍历(身体部分)
	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;
	}
}

// 功能:蛇每移动一步的完整逻辑
void SnakeMove(pSnake ps)
{
	// 动态申请一个新节点,代表蛇即将走到的下一个位置
	pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
	// 判断内存申请是否失败
	if (pNextNode == NULL)
	{
		// 打印错误信息
		perror("SnakeMove()::malloc() failed");
		// 异常退出,错误码-3
		exit(-3);
	}

	// 根据当前蛇方向,计算下一步坐标
	switch(ps->_direction)
	{
	case UP:
		// 向上移动:x不变,y减1
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y - 1;
		break;

	case DOWN:
		// 向下移动:x不变,y加1
		pNextNode->x = ps->_pSnake->x;
		pNextNode->y = ps->_pSnake->y + 1;
		break;

	case LEFT:
		// 向左移动:x减2,y不变
		pNextNode->x = ps->_pSnake->x - 2;
		pNextNode->y = ps->_pSnake->y;
		break;

	case RIGHT:
		// 向右移动:x加2,y不变
		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);
}

// 功能:游戏主循环运行函数(按键、移动、计分、速度控制)
void GameRun(pSnake ps)
{
	// 打印右侧帮助信息
	PrintHelpInfo();
	// 游戏正常运行时持续循环
	do
	{
		// 光标定位,打印当前总分数
		SetPos(145, 24);
		printf("总分数:%d", ps->_score);
		// 光标定位,打印当前单个食物分值
		SetPos(145, 26);
		printf("当前食物的分数:%2d", ps->_food_weight);

		// 按下上键,且当前不是向下,禁止180°掉头
		if (KEY_PRESS(VK_UP) && ps->_direction != DOWN)
		{
			// 修改方向为向上
			ps->_direction = UP;
		}
		// 按下下键,且当前不是向上
		else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP)
		{
			// 修改方向为向下
			ps->_direction = DOWN;
		}
		// 按下左键,且当前不是向右
		else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT)
		{
			// 修改方向为向左
			ps->_direction = LEFT;
		}
		// 按下右键,且当前不是向左
		else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT)
		{
			// 修改方向为向右
			ps->_direction = RIGHT;
		}
		// 按下空格键
		else if (KEY_PRESS(VK_SPACE))
		{
			// 调用暂停函数
			Pause();
		}
		// 按下F3加速键
		else if (KEY_PRESS(VK_F3))
		{
			// 速度大于等于60才可以加速
			if (ps->_sleep_time >= 60)
			{
				// 移动间隔减少30ms,速度变快
				ps->_sleep_time -= 30;
				// 单个食物分数+2
				ps->_food_weight += 2;
			}
		}
		// 按下F4减速键
		else if (KEY_PRESS(VK_F4))
		{
			// 食物分数大于等于2才可以减速
			if (ps->_food_weight >= 2)
			{
				// 移动间隔增加30ms,速度变慢
				ps->_sleep_time += 30;
				// 单个食物分数-2
				ps->_food_weight -= 2;
			}
		}
		// 按下ESC退出键
		else if (KEY_PRESS(VK_ESCAPE))
		{
			// 设置状态为主动正常退出
			ps->_status = END_NORMAL;
		}

        // 调用蛇移动一步函数
		SnakeMove(ps);
		// 按照设置时间休眠,控制移动速度
		Sleep(ps->_sleep_time);
	} while (ps->_status == OK);
}

// 功能:游戏结束,打印结束原因,释放蛇身全部内存
void GameEnd(pSnake ps)
{
	// 清空屏幕
	system("cls");
	// 根据游戏结束状态打印对应提示
	switch (ps->_status)
	{
	case END_NORMAL:
		// 光标定位,打印主动退出
		SetPos(78, 27);
		printf("主动结束游戏\n");
		break;
	case KILL_BY_SELF:
		// 光标定位,打印撞到自己
		SetPos(79, 27);
		printf("撞到自己,游戏结束\n");
		break;
	case KILL_BY_WALL:
		// 光标定位,打印撞到墙
		SetPos(80, 27);
		printf("撞到墙,游戏结束\n");
		break;
	}

	// cur指向蛇头,准备释放链表
	pSnakeNode cur = ps->_pSnake;
	// 遍历整条蛇链表释放内存
	while (cur)
	{
		// del保存当前要删除节点
		pSnakeNode del = cur;
		// cur向后移动
		cur = cur->next;
		// 释放当前节点内存
		free(del);
	}
}
相关推荐
Makoto_Kimur2 小时前
java开发面试-AI Coding速成
java·开发语言
laowangpython2 小时前
Gurobi求解器Matlab安装配置教程
开发语言·其他·matlab
wengqidaifeng2 小时前
python启航:1.基础语法知识
开发语言·python
观北海2 小时前
Windows 平台 Python 极简 ORB-SLAM3 Demo,从零实现实时视觉定位
开发语言·python·动态规划
Ulyanov4 小时前
《PySide6 GUI开发指南:QML核心与实践》 第二篇:QML语法精要——构建声明式UI的基础
java·开发语言·javascript·python·ui·gui·雷达电子对抗系统仿真
码界筑梦坊4 小时前
357-基于Java的大型商场应急预案管理系统
java·开发语言·毕业设计·知识分享
anzhxu4 小时前
Go基础之环境搭建
开发语言·后端·golang
yu85939584 小时前
基于MATLAB的随机振动仿真与分析完整实现
开发语言·matlab
赵钰老师4 小时前
【结构方程模型SEM】最新基于R语言结构方程模型分析
开发语言·数据分析·r语言