admin 2026-01-05 22:20:14 世界杯足球现场

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.h(srand初始化随机数种子,确保食物位置随机)、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("指令") 这个函数需要包含这个头文件

代码语言:javascript复制void test01()

{

system("mode con cols=130 lines=40");

system("title 贪吃蛇");

system("pause");

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

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

COORD类型的声明:

代码语言:javascript复制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);

代码示例:

代码语言:javascript复制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

代码语言:javascript复制 //获得控制台窗口,进行使用

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设置光标是否可见如图所示,在默认状态下光标为可见状态:

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

代码语言:javascript复制 //获得控制台窗口,进行使用

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设置光标到指定的位置代码语言:javascript复制//获得控制台窗口,进行使用

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封装设置光标位置的函数代码语言:javascript复制//封装一个函数,用来设置光标位置

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:定义宏判断按键是否被按下

代码语言:javascript复制#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )代码示例2:检测数字键0~9是否被按下

代码语言:javascript复制//通过定义宏来判断

#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"(正常模式)和" "(本地模式)

温馨提示:使用该函数,需要包含头文件

宽字符打印的注意事项:

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

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

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

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

代码语言:javascript复制#include

#include

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:打印宽字符串

代码语言:javascript复制#include

#include

int main()

{

setlocale(LC_ALL, "");

wprintf(L"Hello World\n");

wchar_t wstr[] = L"宽字符字符串";

wprintf(L"%ls",wstr);

return 0;

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