C语言实战项目:贪吃蛇(1)

前言:

通过持续数月的C语言系统学习,我们已经掌握了包括指针操作、结构体使用、文件IO等核心编程能力。为了检验学习成果并提升实战经验,在本篇技术博客中,我将带领大家开发一个具有里程碑意义的经典游戏项目 -- 贪吃蛇。

温馨提示:本篇博客为贪吃蛇游戏的前言准备。

一、贪吃蛇游戏效果演示

游戏效果演示:

二、贪吃蛇游戏设计

2.1 贪吃蛇游戏的最终目标

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

贪吃蛇游戏实现基本的功能:

• 贪吃蛇地图绘制

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

• 蛇撞墙死亡

• 蛇撞⾃⾝死亡

• 计算得分

• 蛇⾝加速、减速

• 暂停游戏

• 退出游戏

2.2贪吃蛇游戏的思维导图

贪吃蛇游戏的思维导图如下图所示:

2.3贪吃蛇游戏的核心逻辑

核心逻辑:循环内依次执行输入处理→蛇移动→碰撞检测→状态显示→休眠:

2.3.1核心数据结构

采用链表存储蛇身:每个SnakeNode节点包含坐标(x,y)和指向下一节点的指针,通过 "头增尾删" 实现蛇的移动(吃食物时只增不删,长度增长)。

2.3.2游戏流程循环

1. 初始化阶段

①控制台设置:调整窗口大小、标题,隐藏光标(提升视觉流畅度)。

②地图与蛇初始化:绘制边界(如上下左右的墙),生成初始蛇身(默认设置为 5 个节点,初始方向向右)。

③食物生成:随机生成坐标,确保不与蛇身重叠。

2. 运行循环(持续重复)

1.输入处理:

①监听键盘事件(方向键改方向、空格暂停 / 继续、F3 加速、F4 减速、ESC 退出)

②限制 "反向无效"(如当前向上时,按向下键不改变方向,避免瞬间自撞)。

2.蛇移动:

①按当前方向,在头部生成新节点(模拟 "前进")。

②若吃到食物(新头节点坐标与食物坐标重合):不删除尾部节点,蛇长度 + 1,重新生成食物并加分。

③若没吃到食物:删除尾部节点(保持长度不变),并清除尾部节点的屏幕显示。

3.碰撞:

①撞墙:新头节点坐标超出地图边界。

②自撞:新头节点坐标与自身其他节点(非头、非尾)坐标重合。

③若碰撞,设置 "游戏结束" 状态,退出循环。

④状态显示:在屏幕右侧显示分数、速度等级、游戏状态(正常 / 暂停)。

⑤休眠控制:通过Sleep(速度)控制移动频率(速度越快,休眠时间越短,蛇移动越敏捷)

3. 结束与重玩

①游戏结束:释放蛇身链表的内存,显示 "Game Over"。

②重玩询问:提示 "是否重玩(Y/N)",根据输入决定是否重启 "初始化→运行循环"。

2.3.3关键机制细节

①移动的本质:链表的 "头插(前进)+ 尾删(保持长度)",视觉上呈现蛇的 "移动" 效果。

②食物系统:随机生成 + 避蛇身检测,保证食物可被吃到;吃食物后长度增长、分数增加,形成 "成长激励"。

③碰撞判定:通过坐标比对,快速判断 "撞墙" 或 "自撞",一旦触发则终止游戏循环。

④速度与策略:F3/F4 调整Sleep时长实现 "加速 / 减速",同时关联分数变化(加速加分、减速减分),让玩家在 "风险(速度快易撞)" 和 "收益(加分多)" 间做选择。

三、贪吃蛇游戏设计的技术栈

1. 编程语言

C 语言:游戏核心逻辑(如蛇的移动、碰撞检测、食物生成等)、数据结构定义、函数实现均使用 C 语言完成,包括结构体、枚举、指针、链表操作等 C 语言核心特性。

2. Windows API

游戏通过 Windows 系统提供的 API 实现控制台交互,主要涉及:

①控制台窗口控制:设置窗口大小,设置窗口标题。

②光标操作:隐藏和显示光标,定位光标位置(用于绘制蛇、食物、墙壁等元素)。

③键盘输入检测:实时获取键盘按键状态(如方向键、F3/F4、空格、ESC 等),实现对蛇的控制和游戏状态切换。

3. 数据结构

链表:

①蛇的身体通过链表连接,使用头插法添加新节点(蛇头移动)。

②通过遍历链表实现蛇身绘制、碰撞检测(撞自己)和内存释放。

结构体与枚举:

①存储蛇节点坐标,存储食物坐标和分数,整合蛇的核心信息(头节点、食物指针、方向、状态等)。

②用枚举定义蛇的移动方向(上下左右),用枚举定义游戏状态(正常运行、撞墙、撞自己、暂停等),使状态管理更清晰。

4. 控制台图形绘制

通过宽字符和光标定位在控制台绘制游戏元素:

①墙壁、蛇身、食物。

②游戏信息(分数、速度等级、操作提示)的文本绘制。

5. 游戏逻辑与状态管理

核心逻辑:

①蛇的移动:通过计算下一个节点坐标,结合方向枚举实现移动,并根据是否吃到食物决定是否增长蛇身或保持长度。

②碰撞检测:检测蛇头是否撞墙,检测蛇头是否撞到自身。

③分数与速度控制:吃食物增加分数,F3/F4 键调整速度(通过_speed控制休眠时间Sleep),并关联分数变化。

④状态检测:通过枚举管理游戏状态(正常运行、暂停、结束等),在循环中根据状态决定流程(继续运行、退出、重启等)

6. 内存管理

①动态内存分配:使用malloc为蛇节点分配内存,避免栈内存溢出。

②内存释放:通过遍历链表释放所有蛇节点内存,防止内存泄漏。

7. 标准库与工具

①C 标准库:stdio.h(输入输出)、stdlib.h(内存分配、随机数)、time.hsrand初始化随机数种子,确保食物位置随机)、assert.h(断言指针有效性,调试用)。

②随机数生成:rand()结合time(0)生成随机食物坐标,确保食物位置不与蛇身或墙壁重叠。

四、Windows API的详解

4.1 win32API

简单来说:Windows 是多作业系统,除了协调程序、分配内存、管资源,还像个 "服务站"------ 提供各种函数(服务)。应用程序调用这些函数,就能实现开窗口、画图形、用外设等操作,这类服务应用的函数叫 API;而 WIN32 API,就是 32 位 Windows 平台的这类编程接口。

4.2控制台主程序

平常我们运⾏起来的⿊框程序其实就是控制台程序,如下图所示:

4.2.1设置窗口大小

我们可以使用一些cmd指令来设置控制台的长宽,将控制台的长,宽设置为100 和 30

例如:通过这段指令:mode con cols=100 lines=30

4.2.2设置控制台名称

同时我们也可以设置,控制台的名称。

通过如下指令:title 贪吃蛇

4.2.3利用代码实现

当然我们也可以通过C语言代码,来实现控制台的大小和标题设置,通过system("指令")这个函数来实现

温馨提示:system("指令") 这个函数需要包含<windows.h>这个头文件

cpp 复制代码
void test01()
{
	system("mode con cols=130 lines=40");

	system("title 贪吃蛇");

	system("pause");
}

4.3控制台屏幕上的坐标

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

在控制台上的坐标系如下图所示:

COORD类型的声明:

cpp 复制代码
typedef struct _COORD
{
    SHORT X;  // X坐标
    SHORT Y;  // Y坐标
} COORD, *PCOORD;
    
        
int main()
{
   //例如给坐标赋值:
   COORD  pos = { 10, 15 };
   return 0;
}

4.4通过句柄操作设备

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

简单来说就相当于一个手柄,通过该手柄就可以控制设备了,这里我们不需要过多与纠结其函数是如何实现,我们仅需要明白它的功能和如何调用就已经够用了。

GetStdHandle函数原型:

HANDLE GetStdHandle(DWORD nStdHandle);

它有三个参数:

1.STD_INPUT_HANDLE 获取标准输入设备

2.STD_OUTPUT_HANDLE 获取标准输出设备

3.STD_ERROR_HANDLE 获取标准错误设备

其中返回值HANDLE为一个void * 的指针,通过 typedef void *HANDLE 命名HANDLE。

这里我们只需要对控制台(标准输出)进行操作,所以我们仅需要用到获取标准输出设备,通过调用我们就可以进行操作控制台程序。

HANDLE GetStdHandle(STD_OUTPUT_HANDLE);

代码示例:

cpp 复制代码
HANDLE hOutput = NULL;

//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

4.5获取控制台光标信息

GetConsoleCursorInfo函数原型:

BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

**参数一:获取标准输出的句柄:**HANDLE hConsoleOutput

**参数二:指向存放光标信息的结构体:**PCONSOLE_CURSOR_INFO lpConsoleCursorInfo

结构体_CONSOLE_CURSOR_INFO:主要用来存放控制台光标信息

typedef struct _CONSOLE_CURSOR_INFO

{

DWORD dwSize; //成员一 设置光标的大小

BOOL bVisible; //成员二 设置光标是否可见

} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

成员一:dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条。

成员二:bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE

4.6设置控制台光标信息

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

函数原型为:

BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

**参数一:获取标准输出的句柄:**HANDLE hConsoleOutput

**参数二:指向存放光标信息的结构体:**PCONSOLE_CURSOR_INFO lpConsoleCursorInfo

4.7代码演示光标的设置

通过上面三个函数,我们就可以实现对光标大小和显示的操作

4.7.1设置光标大小

初始时光标的大小默认为25,如图所示:

代码示例:将默认的光标大小设置为100

cpp 复制代码
	//获得控制台窗口,进行使用
	HANDLE houtput = NULL;
	houtput=GetStdHandle(STD_OUTPUT_HANDLE);

	//定义储存控制台光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获得与houtput句柄相关的控制台光标的信息
	GetConsoleCursorInfo(houtput, &cursor_info);

	//修改光标的占比值
	cursor_info.dwSize = 100;


	//设置光标大小和光标可见度的函数
	SetConsoleCursorInfo(houtput, &cursor_info);

如图所示:

4.7.2设置光标是否可见

如图所示,在默认状态下光标为可见状态:

代码示例:将光标设置为不可见状态

cpp 复制代码
	//获得控制台窗口,进行使用
	HANDLE houtput = NULL;
	houtput=GetStdHandle(STD_OUTPUT_HANDLE);

	//定义储存控制台光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = { 0 };

	//获得与houtput句柄相关的控制台光标的信息
	GetConsoleCursorInfo(houtput, &cursor_info);

	//修改光标是否可见
	cursor_info.bVisible = false;

	//设置光标大小和光标可见度的函数
	SetConsoleCursorInfo(houtput, &cursor_info);

4.8设置光标的位置

SetConsoleCursorPosition:设置指定控制台屏幕缓冲区中的光标位置

函数原型如下:

BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);

**参数一:获取标准输出的句柄:**HANDLE hConsoleOutput

**参数二:存放位置信息的坐标:**COORD pos

通过该函数,我们就可以设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中。

调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。

4.8.1设置光标到指定的位置

cpp 复制代码
//获得控制台窗口,进行使用
HANDLE houtput = NULL;
houtput=GetStdHandle(STD_OUTPUT_HANDLE);

//定义储存控制台光标信息的结构体
CONSOLE_CURSOR_INFO cursor_info = { 0 };

//获得与houtput句柄相关的控制台光标的信息
GetConsoleCursorInfo(houtput, &cursor_info);
    
//设置控制台坐标
COORD pos = { 10, 20 };
    
//设置指定位置光标
SetConsoleCursorPosition(houtput, pos);
    
//进行暂停观察
getchar();

4.8.2封装设置光标位置的函数

cpp 复制代码
//封装一个函数,用来设置光标位置
void set_pos(short x, short y)
{
	HANDLE houtput= GetStdHandle(STD_OUTPUT_HANDLE);

	COORD pos = { x, y };

	SetConsoleCursorPosition(houtput, pos);
}

4.9获取按键情况

GetAsyncKeyState:将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

函数原型如下:

SHORT GetAsyncKeyState(int vKey);

参数分析:键盘上按键的虚拟键值****int vKey

返回值分析:

1.GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后。

2.如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;

3.可以将返回值&0x1来进行检测:GetAsyncKeyState返回值的最低值是否为1

参考:虚拟键码表

代码示例1:定义宏判断按键是否被按下

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

代码示例2:检测数字键0~9是否被按下

cpp 复制代码
//通过定义宏来判断
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk) & 1 ) ? 1 : 0 )
void test04()
{
	while (1)
	{
		if (KEY_PRESS(0x30))
		{
			printf("0\n");
		}
		else if (KEY_PRESS(0x31))
		{
			printf("1\n");
		}
		else if (KEY_PRESS(0x32))
		{
			printf("2\n");
		}
		else if (KEY_PRESS(0x33))
		{
			printf("3\n");
		}
		else if (KEY_PRESS(0x34))
		{
			printf("4\n");
		}
		else if (KEY_PRESS(0x35))
		{
			printf("5\n");
		}
		else if (KEY_PRESS(0x36))
		{
			printf("6\n");
		}
		else if (KEY_PRESS(0x37))
		{
			printf("7\n");
		}
		else if (KEY_PRESS(0x38))
		{
			printf("8\n");
		}
		else if (KEY_PRESS(0x39))
		{
			printf("9\n");
		}

	}

}

五、宽字符的打印

在贪吃蛇游戏中,我们采用宽字符进行界面渲染。游戏地图中的墙体使用宽字符□表示,蛇身使用●字符,食物则用★字符标识。与普通单字节字符不同,这些宽字符每个占据2个字节的存储空间。

对于宽字符的打印,需要进行本地化处理,通过如下函数进行本地化处理:

setlocale函数:进行本地化处理

函数原型如下所示:

char* setlocale (int category, const char* locale);

参数一:

• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。

• LC_CTYPE:影响字符处理函数的⾏为。

• LC_MONETARY:影响货币格式。

• LC_NUMERIC:影响 printf() 的数字格式。

• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。

• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

一般而言我们进行传入LC_ALL对所有类型进行修改。

参数二:

C标准仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)

温馨提示:使用该函数,需要包含<locale.h>头文件

宽字符打印的注意事项:

1.宽字符的字⾯量必须加上前缀"L",否则 C 语⾔会把字⾯量当作窄字符类型处理。

2.前缀"L"在单引号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;

3.在双引号前⾯,表⽰宽字符串,对应wprintf() 的占位符为 %ls

代码示例1:打印单个宽字符

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

    
int main()
{
    setlocale(LC_ALL, "");

	char a = 'a';
	char b = 'b';
	printf("%c%c\n", a, b);

	wchar_t wc1 = L'★';
	wchar_t wc2 = L'我';
	wprintf(L"%lc \n%lc", wc1, wc2);

    return 0;
}

代码示例2:打印宽字符串

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

int main()
{
    setlocale(LC_ALL, "");

	wprintf(L"Hello World\n");

    wchar_t wstr[] = L"宽字符字符串";
    
    wprintf(L"%ls",wstr);
		
    return 0;
}

既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

相关推荐
伊织code3 小时前
LLM - 命令行与Python库的大语言模型交互工具
开发语言·python·语言模型
春生黎至10053 小时前
Python列表
开发语言·python
小莞尔3 小时前
【51单片机】【protues仿真】基于51单片机秒表系统(LCD1602多功能、可保持30条记录)
c语言·stm32·单片机·嵌入式硬件·51单片机
我没想到原来他们都是一堆坏人3 小时前
java 动态代理
java·开发语言·动态代理
微笑尅乐3 小时前
多种解法全解析——力扣217. 存在重复元素
算法·leetcode·职场和发展
Jiezcode3 小时前
LeetCode 199.二叉树的右视图
c++·算法·leetcode·深度优先
10001hours3 小时前
C语言第23讲
c语言·开发语言·算法
h汉堡3 小时前
类和对象(二)
开发语言·数据结构·c++·学习
Coding_Doggy3 小时前
java面试day5 | 消息中间件、RabbitMQ、kafka、高可用机制、死信队列、消息不丢失、重复消费
java·开发语言·面试