C语言应用实例——贪吃蛇
(图片由AI生成)
0.贪吃蛇游戏背景
贪吃蛇游戏,最早可以追溯到1976年的“Blockade”游戏,是电子游戏历史上的一个经典。在这款游戏中,玩家操作一个不断增长的蛇,目标是吃掉出现在屏幕上的食物,同时避免撞到自己的身体或游戏边界。
随着时间的发展,贪吃蛇游戏出现了许多变体,但其核心玩法保持不变:玩家控制一个逐渐增长的蛇,在一个封闭的空间内移动。随着蛇身的增长,游戏的难度也相应增加。这种简单而紧张的游戏机制,使贪吃蛇成为了一种深受欢迎的休闲游戏。
在90年代,随着诺基亚手机的普及,贪吃蛇游戏被预装在许多手机上,从而获得了巨大的流行。这个版本的游戏通常由黑白屏幕和四个方向键控制,提供了简单而上瘾的游戏体验。
在技术层面上,贪吃蛇游戏是许多程序员入门的第一个项目之一。它涉及到诸如数据结构(如链表)、游戏循环、输入处理等基础编程概念,是理解这些概念的绝佳实践。
贪吃蛇不仅仅是一个游戏,它也是计算机编程和电子游戏设计历史的一个重要部分。通过编写自己的贪吃蛇游戏,我们不仅能学习编程基础,还能深入了解游戏设计的基本原则。
1.我们需要做哪些准备?
在开始编写贪吃蛇游戏之前,有两个主要方面需要准备:明确游戏需要实现的功能和积累必要的知识储备。
1.1贪吃蛇需要实现的功能
- 基本游戏逻辑: 贪吃蛇的核心逻辑包括蛇的移动、吃食物以及随着吃食物身体长度的增加。
- 控制系统: 玩家需要能够控制蛇的移动方向。
- 食物生成: 游戏需要在随机位置生成食物。
- 碰撞检测: 游戏需要能够检测蛇头是否碰到了自身的其他部分或游戏边界。
- 分数和等级系统: 随着蛇的增长,游戏可以设定分数和等级系统,以增加游戏的挑战性和吸引力。
- 游戏结束条件: 当蛇撞到边界或自身时,游戏应该结束。
1.2需要的知识储备
- C语言基础: 包括变量、循环、函数、数组和指针等基本概念。
- 数据结构: 特别是链表,这对于实现蛇身体的动态增长非常重要。
- Win32 API了解: 特别是与控制台输出和键盘输入相关的API,用于游戏界面的显示和玩家的输入处理。
- 简单的算法知识: 如随机数生成,用于食物的随机位置生成。
- 游戏设计概念: 包括游戏循环、状态管理和用户交互。
2.Win32 API入门
2.1Win32 API介绍
Win32 API(Application Programming Interface)是微软Windows操作系统的一个核心应用编程接口集合。它允许C/C++程序员在Windows环境下进行系统级别的编程。这些API涵盖了大量功能,包括窗口管理、文件操作、设备输入、进程和线程管理等。
对于游戏开发,尤其是如贪吃蛇这类简单的游戏,Win32 API提供了基础的图形界面功能和控制用户输入的方法。虽然Win32 API看起来可能有些过时,但它仍然是学习Windows系统编程和理解Windows操作系统工作原理的重要工具。
2.2控制台程序
在Win32 API中,控制台程序指的是运行在Windows命令提示符(cmd)或PowerShell中的应用程序。这些程序一般通过文本界面与用户交互,而非图形用户界面(GUI)。对于初学者来说,开发控制台程序是一种学习编程的好方法,因为它们相对简单,可以让我们专注于代码逻辑,而不是复杂的图形界面。
使用Win32 API开发控制台程序,通常涉及以下几个方面:
- 控制台窗口管理: 创建和管理控制台窗口,包括窗口的大小、缓冲区大小等。
- 输入和输出处理: 读取用户的键盘输入,并在控制台窗口显示输出。
- 字符和颜色控制: 设置文本和背景颜色,控制字符在控制台窗口中的显示方式。
- 光标管理: 控制光标的位置,用于在特定位置显示文本或字符。
控制台程序虽然简单,但通过Win32 API的合理使用,可以创建出交互性较强且逻辑稍复杂的应用程序,例如贪吃蛇游戏。通过控制台程序的开发,我们可以深入理解计算机程序的运行原理及操作系统的基本工作方式。
我们以设置控制台窗口的长和宽为例:
1.Win+R,呼出“运行”窗口,输入“cmd”,单击“确定”
2.在控制台窗口输入命令“mode con cols=100 lines=30”
3.单击回车,控制台窗口便变为指定大小
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
#include #include int main() { //设置控制台窗口的长宽:设置控制台窗口的大小,10行,30列 system("mode con cols=30 lines=10"); //设置cmd窗口名称 system("title 贪吃蛇"); //设置cmd窗口颜色 system("color 70");//灰白底黑字 return 0; }
运行结果如下图:
2.3控制台屏幕上的坐标COORD
在Windows控制台程序中,屏幕上的位置是通过COORD结构表示的。COORD是一个简单的结构体,定义在头文件中,用于指定一个字符在控制台屏幕缓冲区的坐标。它包含两个成员:X和Y,分别代表水平(列)和垂直(行)坐标。通过修改这些值,你可以控制文本或者其他输出在控制台窗口中的位置。
typedef struct _COORD { SHORT X; SHORT Y; } COORD,*PCOORD;
例如,在贪吃蛇游戏中,使用COORD结构可以精确地控制蛇在屏幕上的位置,实现其在屏幕上的移动。
COORD pos = {10,15};//给坐标赋值
2.4 GetStdHandle 获取句柄函数
GetStdHandle函数是用来获取一个标准设备(如控制台输入、输出或错误)的句柄。句柄可以被看作是一个指向特定资源(在这个场景下是设备)的指针或索引。在Windows编程中,句柄用于在各种API调用中标识和操作这些资源。
HANDLE GetStdHandle(DWORD nStdHandle);
例如,要在控制台程序中打印文本,你可能需要先使用GetStdHandle函数获取控制台输出的句柄。
HANDLE hOutput = NULL; //获取标准输出句柄 hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
2.5 GetConsoleCursorInfo 检索控制台屏幕缓冲区光标函数
GetConsoleCursorInfo函数用于检索控制台屏幕缓冲区光标的信息。这个函数允许你获取光标的大小和可见性。这在需要精细控制光标表现的场景中非常有用,比如在用户输入时隐藏光标,或者在特定操作时改变光标的大小。
WINBASEAPI WINBOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo );
示例如下:
HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
2.5.1 COUSOLE_CURSOR_INFO
CONSOLE_CURSOR_INFO是一个结构,用于指定控制台光标的大小和可见性。这个结构有两个成员:dwSize和bVisible。dwSize用于设置光标的大小(以百分比表示),bVisible是一个布尔值,用于控制光标是否可见。
typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
在使用GetConsoleCursorInfo和SetConsoleCursorInfo函数时,会用到CONSOLE_CURSOR_INFO结构。例如,在贪吃蛇游戏中,可能希望在游戏运行时隐藏光标,以避免光标干扰游戏视觉效果。通过设置CONSOLE_CURSOR_INFO结构的相应成员,然后使用SetConsoleCursorInfo函数,可以轻松实现这一点。
CursorInfo.bVisible = false; //隐藏控制台光标
2.6 SetConsoleCursorInfo 设置控制台屏幕缓冲区光标函数
BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo );
SetConsoleCursorInfo函数用于设置控制台屏幕缓冲区的光标信息,包括光标的大小和可见性。这对于控制台应用程序来说非常有用,特别是在需要精确控制光标表现的场合,如隐藏光标或调整其大小。
#include int main() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO cursorInfo; cursorInfo.dwSize = 10; // 设置光标大小为10 cursorInfo.bVisible = FALSE; // 设置光标为不可见 SetConsoleCursorInfo(hConsole, &cursorInfo); return 0; }
在这个例子中,SetConsoleCursorInfo用于设置一个不可见的光标,这在贪吃蛇游戏中是一个常见的需求,以避免光标干扰游戏的视觉效果。
2.7 SetConsoleCursorPosition 设置控制台屏幕缓冲区光标位置函数
BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD pos );
SetConsoleCursorPosition函数用于设置控制台屏幕缓冲区的光标位置。这是通过传递一个COORD结构来实现的,该结构指定了新的光标位置的X和Y坐标。
#include int main() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); COORD position; position.X = 10; // 设置光标的列位置 position.Y = 5; // 设置光标的行位置 SetConsoleCursorPosition(hConsole, position); return 0; }
在这个例子中,光标被设置到控制台窗口的特定位置。这在控制贪吃蛇游戏中的蛇的位置时特别有用。
2.8 GetAsyncKeyState 获取按键情况函数
SHORT GetAsyncKeyState(int vKey);
GetAsyncKeyState函数用于确定在调用函数时某个键的状态。这个函数可以检测按键是否被按下,即使窗口不在焦点上,它也能工作。这使得GetAsyncKeyState成为游戏开发中处理键盘输入的一个重要工具。
#include int main() { // 检测 "W" 键是否被按下 if(GetAsyncKeyState('W') & 0x8000) { // "W" 键被按下的处理 } return 0; }
在贪吃蛇游戏中,你可以使用GetAsyncKeyState来检测玩家的方向控制键(如WASD或方向键),并根据这些输入来移动蛇。
3.初步设计贪吃蛇游戏
在详细介绍游戏地图(3.1节)之前,让我们先了解SetPos函数,这是在贪吃蛇游戏代码中一个关键的辅助功能。
SetPos函数在程序中起到了非常重要的作用,它用于设置控制台光标的位置。这个功能对于在控制台程序中创建图形化输出(如游戏地图和蛇的图形)至关重要。下面是SetPos函数的实现:
void SetPos(int x, int y) { // 获取标准输出设备的句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); // 创建一个COORD结构体,用于存储光标位置 COORD pos = { x, y }; // 设置控制台光标位置 SetConsoleCursorPosition(handle, pos); }
在这个函数中,首先通过调用GetStdHandle函数获得标准输出设备(通常是控制台窗口)的句柄。接着,创建了一个COORD结构体实例pos,其中包含了要设置的新光标位置的x和y坐标。最后,使用SetConsoleCursorPosition函数将光标移动到指定位置。
通过使用SetPos函数,程序可以在控制台窗口的任何位置输出字符,这对于绘制游戏地图、蛇身和食物等元素至关重要。
3.1游戏地图
在游戏中,地图的创建是通过CreateMap函数实现的。这个函数的核心是在控制台窗口中绘制出贪吃蛇游戏的边界。具体实现方式如下:
void CreateMap() { // 打印上边界 SetPos(0, 0); for (int i = 0; i在这个函数中,WALL(定义为■)字符用于表示墙壁。函数先打印上下边界,然后打印左右边界。使用SetPos函数来定位控制台上的每个字符位置,从而形成一个封闭的矩形区域,作为游戏的主要场地。
3.2蛇身
蛇身的设计采用了链表数据结构。您定义了SnakeNode结构体来代表蛇的每一部分,每个节点包含坐标信息和指向下一个节点的指针。蛇的初始化在InitSnake函数中实现:
void InitSnake(pSnake ps) { // ...[模式选择代码]... for (int i = 0; i x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; // 头插法添加蛇身节点 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; } } // ...[其他初始化]... }在这个函数中,使用头插法来创建蛇身的链表。每次循环都创建一个新的SnakeNode,并将其插入到链表的头部。这样,链表的头部始终代表蛇头的当前位置。随着游戏进行,蛇头的位置会不断更新,而蛇身则跟随蛇头移动。
初始化时,蛇的位置由宏POS_X和POS_Y定义,这两个宏决定了蛇初始时在游戏地图上的位置。使用wprintf函数和BODY字符(定义为◎)在指定位置打印蛇身,从而在控制台上形成蛇的可视化表示。
3.3食物
在贪吃蛇游戏中,食物是玩家得分和蛇增长的关键元素。设计食物的主要考虑因素包括其在游戏地图上的生成、呈现和与蛇的交互机制。
3.3.1食物的生成
食物通常在游戏地图的随机位置生成,条件是该位置不与蛇身体的任何部分重叠。这可以通过生成随机坐标并检查这些坐标是否已被蛇占用来实现。如果坐标被占用,就需要重新生成,直到找到一个空闲的位置。
3.3.2食物的呈现
一旦确定了食物的位置,它就会在游戏地图上显示。通常,食物由一个特殊的字符或图形符号表示,这个符号在游戏地图上清晰可见,与蛇身和墙壁等其他元素区别开来。
3.3.3与蛇的交互
食物的主要功能是被蛇“吃掉”。当蛇头移动到包含食物的坐标时,游戏逻辑应识别这一事件,并触发相应的响应:增加玩家的得分,并使蛇的长度增加一个单位。此时,食物消失,游戏随后在另一个位置生成新的食物。
3.4数据结构设计
在贪吃蛇游戏中,数据结构的设计对于实现高效且可维护的代码至关重要。以下是常见的数据结构及其在游戏中的应用:
链表(用于蛇身表示):蛇的每个部分都可以看作是链表中的一个节点,每个节点包含自身的位置信息和指向下一个节点的指针。链表允许动态地添加或删除节点,从而模拟蛇吃食物变长和移动时身体的变化。
结构体(用于游戏元素封装):如蛇、食物等游戏元素可以通过结构体进行封装,结构体中包含相关的属性,如位置、大小等。结构体的使用有助于代码的组织和管理。
枚举类型(用于状态和方向管理):游戏状态(如进行中、结束)和蛇的移动方向(如上、下、左、右)可以用枚举类型表示,增强了代码的可读性和易管理性。
3.5游戏流程设计
游戏流程的设计涉及游戏的整体结构和玩家与游戏交互的方式。典型的贪吃蛇游戏流程包括:
初始化阶段:设置游戏环境,包括初始化地图、蛇的初始位置和长度、初始食物的位置等。
游戏开始界面:展示游戏欢迎界面,提供游戏规则说明、游戏开始的选项等。
主游戏循环:游戏的核心部分。在这个阶段,游戏根据用户输入更新蛇的位置,检测蛇是否吃到食物或碰撞到墙壁/自身,以及更新游戏状态(如分数、蛇的长度等)。
碰撞检测:检查蛇头是否碰撞到墙壁或自身其他部分。如果发生碰撞,则游戏结束。
食物重新生成:当蛇吃到食物后,需要在地图的另一个位置生成新的食物。
游戏结束处理:当游戏结束时(如蛇碰撞到墙壁或自身),显示游戏结束的界面,提供重新开始或退出游戏的选项。
用户输入处理:在游戏过程中,实时处理用户的输入,如蛇的移动控制和游戏暂停等。
3.6优化设计与图形编程初步
在贪吃蛇游戏的开发过程中,优化设计和引入图形编程是提高游戏质量和玩家体验的重要步骤。这里,我们将探讨如何通过EasyX库在Windows环境下实现图形编程。
3.6.1优化设计
性能优化:优化算法和数据结构以减少CPU和内存的使用。例如,优化蛇的移动算法,确保即使蛇变得非常长时,游戏仍然流畅运行。
用户界面优化:改善用户界面和用户体验,比如通过添加更直观的菜单、清晰的得分显示和响应玩家输入的即时反馈。
代码维护性和可扩展性:重构代码以提高其可读性和可维护性,确保可以轻松添加新功能或修改现有功能。
3.6.2图形编程初步 - EasyX
EasyX 是一个面向Windows平台的轻量级图形库,它提供了一系列简单易用的API,用于2D图形编程。使用EasyX,开发者可以在C/C++中方便地实现图形界面,它特别适合于游戏开发和图形应用的入门级学习。
初始化和设置图形窗口:使用EasyX提供的函数快速创建和配置图形窗口。可以设置窗口大小、标题等。
绘制图形:EasyX提供了丰富的图形绘制功能,如画线、画圆、填充颜色等,这些功能可以用来绘制游戏地图、蛇身和食物等。
事件处理:处理用户输入,如键盘和鼠标事件,来控制游戏中的蛇移动或响应其他游戏命令。
动画和定时器:通过定时器和连续的画面刷新实现动画效果,如蛇的移动和食物的出现。
资源清理:在游戏结束或关闭窗口时,正确释放图形资源。
使用EasyX作为图形编程的入门工具,可以使得原本基于文本的贪吃蛇游戏变得更加生动和吸引人。它不仅提供了丰富的图形绘制功能,而且相对简单易学,非常适合初学者和爱好者进行图形游戏的开发。
具体介绍可以在EasyX官网进一步了解哦!
4.进一步实现游戏设计
4.1游戏总逻辑
在进一步实现贪吃蛇游戏的设计时,理解和构建游戏的总逻辑是至关重要的。游戏的总逻辑涉及到如何将各个组成部分(如蛇的移动、食物的生成、碰撞检测等)整合到一个连贯的游戏流程中。
初始化阶段:这一阶段包括设置游戏窗口、初始化蛇的状态(位置、长度、方向)、生成首个食物的位置以及设置游戏的初始参数(如分数、速度等)。
游戏开始界面:显示游戏的开始界面,可能包括游戏标题、作者信息、游戏规则说明以及开始游戏的选项。在这里,玩家可以通过按键开始游戏。
主循环:游戏的核心是一个主循环,它持续运行,直到触发游戏结束的条件。在每次循环中,游戏需要更新蛇的位置、检测与食物或障碍物的碰撞、更新分数和显示游戏状态。
蛇的移动:根据用户的输入(键盘控制)来更新蛇的方向,然后基于这个方向移动蛇的头部,同时更新蛇身的位置。
食物处理:如果蛇头的新位置与食物位置相同,表示蛇吃到了食物,此时需要增加蛇的长度、增加分数,并在地图上生成新的食物。
碰撞检测:在每次移动后,检测蛇头是否碰到了游戏边界或蛇的身体其余部分。如果发生碰撞,游戏结束。
用户输入:在游戏运行期间实时监听用户的输入,以便控制蛇的移动、暂停游戏或触发其他命令。
游戏结束处理:当游戏结束条件满足时(如蛇撞到自己或墙壁),显示游戏结束界面,提供重新开始或退出游戏的选项。
清理和退出:在游戏结束后,适当清理资源,如释放内存,确保程序的整洁退出。
void test() { int choice = 0; srand((unsigned int)time(NULL)); do { Snake snake = { 0 }; GameStart(&snake);//游戏开始 GameRun(&snake);//游戏运行 GameEnd(&snake);//游戏结束 SetPos(20, 15); printf("再来一局吗?(1:是 0:否):"); scanf("%d", &choice); } while (choice); SetPos(0, 27); } int main() { //修改适配本地中文环境 setlocale(LC_ALL, ""); //测试函数 test_rainbow(); test(); test_rainbow(); return 0; }4.2 GameStart 游戏开始函数
GameStart函数是游戏开始的入口点。它负责初始化游戏的主要组成部分,并设置游戏的初始状态。具体实现可能包括设置控制台的属性、隐藏光标、调用初始化蛇身、创建地图和食物的函数等。
//游戏开始 void GameStart(pSnake ps) { system("mode con cols=100 lines=30"); system("color 0f"); system("title 贪吃蛇GreedySnake"); //隐藏光标 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(handle, &CursorInfo); CursorInfo.bVisible = false; SetConsoleCursorInfo(handle, &CursorInfo); //打印欢迎界面 WelcomeToGame(); //绘制地图 CreateMap(); //初始化蛇 InitSnake(ps); //创建食物 CreateFood(ps); }4.2.1 WelcomeToGame 欢迎界面
WelcomeToGame函数用于显示游戏的欢迎界面。它通常包括显示游戏的标题、作者信息、以及游戏规则的简要说明。此外,这个函数可能还包括一些如颜色设置或其他视觉元素的代码,用于提升玩家的初次体验。
//欢迎界面 void WelcomeToGame() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN); //打印欢迎界面 SetPos(35, 10); printf("欢迎来到贪吃蛇游戏"); SetPos(38, 20); system("pause"); system("cls"); //功能介绍信息 system("color e8"); SetPos(15, 10); printf("游戏规则:用 ↑.↓.←.→ 分别控制蛇的移动,Ctrl为加速,Alt为减速\n"); SetPos(15, 11); printf("游戏说明:蛇吃到食物长度加1,蛇撞到墙壁或者自己游戏结束\n"); SetPos(15, 13); system("pause"); system("cls"); system("color 6D"); }4.2.2 CreateMap 创建地图
CreateMap函数负责在控制台上创建游戏的地图。这通常涉及打印一系列字符以形成游戏区域的边界。例如,使用特定的字符来表示墙壁,并在控制台的边缘绘制这些字符,从而形成一个封闭的区域,作为蛇移动的游戏场地。
//创建地图 void CreateMap() { //打印上边界 SetPos(0, 0); int i = 0; for (i = 0; i4.2.3 InitSnake 初始化蛇身
InitSnake函数用于初始化蛇的状态。这包括设置蛇的初始长度、位置、移动方向等。蛇的身体可以通过一个链表来表示,其中每个节点代表蛇的一个部分。初始化时,需要创建这个链表,并将其各部分正确地放置在游戏地图上。
//初始化蛇 void InitSnake(pSnake ps) { //创建5个蛇身节点 pSnakeNode cur = NULL; SetPos(62, 5); printf("模式选择"); SetPos(62, 6); printf("1.简单 2.普通 3.困难 4.噩梦:"); int ch = 0; scanf("%d", &ch); switch (ch) { case 1: ps->SleepTime = 200; ps->FoodWeight = 100; break; case 2: ps->SleepTime = 150; ps->FoodWeight = 200; break; case 3: ps->SleepTime = 100; ps->FoodWeight = 300; break; case 4: ps->SleepTime = 50; ps->FoodWeight = 400; break; default: ps->SleepTime = 200; ps->FoodWeight = 100; break; } int i = 0; for (i = 0; i x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; //头插法 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->FoodWeight = 100; ps->pFood = NULL; ps->Status = OK; //ps->SleepTime = 200; ps->Score = 0; } }4.2.4 CreateFood 创建食物
CreateFood函数负责在游戏地图上的随机位置生成食物。这需要确保食物不会出现在蛇身所占据的位置。通常,这可以通过随机选择位置并检查该位置是否已被蛇占用来实现。一旦找到一个合适的位置,函数会在该位置上打印表示食物的字符。
void CreateFood(pSnake ps) { int x = 0; int y = 0; again: do { x = rand() % 53 + 2; y = rand() % 25 + 1; } while (x % 2 != 0); pSnakeNode cur = ps->pSnake; while (cur) { if (x == cur->x && y == cur->y) { goto again; } cur = cur->next; } pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pFood == NULL) { perror("CreateFood:malloc"); return; } else { pFood->x = x; pFood->y = y; SetPos(x, y); wprintf(L"%lc", FOOD); ps->pFood = pFood; } }4.3 GameRun 游戏运行函数
GameRun函数是贪吃蛇游戏中最关键的部分,负责处理整个游戏的运行逻辑。在这个函数中,游戏进入主循环,不断检查游戏状态、处理用户输入、更新游戏画面(如蛇的移动和食物的生成),并且根据游戏逻辑做出相应的反应(例如蛇吃到食物或撞到墙壁)。
void GameRun(pSnake ps) { //打印帮助信息 PrintHelpInfo(); do { SetPos(62, 10); printf("总得分:%5d\n", ps->Score); SetPos(62, 11); printf("每个食物得分:%5d\n", ps->FoodWeight); //检测按键 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 = ESC; break; } else if (KEY_PRESS(VK_SPACE)) { Pause();//暂停,恢复 } else if (KEY_PRESS(VK_LCONTROL) || KEY_PRESS(VK_RCONTROL)) { if (ps->SleepTime >= 80) { ps->SleepTime -= 20; ps->FoodWeight += 10; } } else if (KEY_PRESS(VK_LMENU) || KEY_PRESS(VK_RMENU)) { if (ps->FoodWeight >= 50) { ps->SleepTime += 20; ps->FoodWeight -= 10; } } //睡眠 Sleep(ps->SleepTime); //走一步 SnakeMove(ps); } while (ps->Status == OK); }4.3.1 KEY_PRESS 宏
KEY_PRESS宏是用来检测键盘输入的一个工具。它通过调用GetAsyncKeyState函数来检查特定键是否被按下。这个宏简化了键盘输入的处理,使得在游戏主循环中可以轻松检测玩家的按键操作,从而控制蛇的移动方向或处理其他如暂停、退出等功能。
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0) //按键按下4.3.2 PrintHelpInfo 打印帮助信息
PrintHelpInfo函数用于在游戏中显示帮助信息。这个信息通常包括游戏的操作指南(如如何控制蛇的移动),以及可能的其他提示(如如何暂停游戏、游戏目标等)。这个函数的目的是提供给玩家必要的信息,帮助他们更好地理解和享受游戏。
这个函数的实现可能包含多次调用SetPos或类似的函数来在控制台特定位置打印文本信息。这些信息旨在在游戏开始时向玩家提供指导,或者在游戏过程中通过按特定键触发。
//打印帮助信息 void PrintHelpInfo() { SetPos(62, 14); printf("注意事项:"); SetPos(62, 15); printf("1.不能撞墙,不能碰到自己"); SetPos(62, 16); printf("2.用 ↑.↓.←.→ 分别控制蛇移动"); SetPos(62, 17); printf("3.Ctrl为加速, Alt为减速"); SetPos(62, 18); printf("4.按空格键暂停"); SetPos(62, 19); printf("5.按ESC键退出游戏"); SetPos(62, 21); printf("游戏制作 @wxk2227814847"); SetPos(62, 22); printf("版权所有 侵权必究"); }4.3.3 SnakeMove 蛇身移动函数
SnakeMove函数负责处理蛇的移动逻辑。在每次游戏循环中,这个函数更新蛇的位置,根据蛇头的当前方向和位置来决定蛇下一步的动作。函数的关键部分包括判断蛇的下一个位置是空位、食物还是障碍物(如墙壁或蛇自身的其他部分),并据此进行相应的处理。
//暂停 void Pause() { while (1) { Sleep(300); if (KEY_PRESS(VK_SPACE)) { break; } } } //蛇身移动 void SnakeMove(pSnake ps) { pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNext == NULL) { perror("SnakeMove():malloc"); return; } pNext->next = NULL; switch (ps->Dir) { 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; 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; } //下一个坐标处是否为食物 if (NextIsFood(ps, pNext)) { //是食物就吃掉 EatFood(ps, pNext); } else { //不是就正常走一步 NotEatFood(ps, pNext); } //检测撞墙 KillByWall(ps); //检测撞到自己 KillBySelf(ps); }4.3.3.1 NextIsFood 下一个是食物函数
NextIsFood函数用于判断蛇头的下一个位置是否有食物。如果蛇头即将移动到的位置与食物的位置相同,这个函数返回真(true),指示蛇将在下一次移动中吃到食物。这个判断对于控制游戏流程(特别是蛇的增长和分数的增加)至关重要。
bool NextIsFood(pSnake ps, pSnakeNode pNext) { return (ps->pFood->x == pNext->x) && (ps->pFood->y == pNext->y); }4.3.3.2 EatFood 吃食物函数
当NextIsFood返回真时,EatFood函数被调用。这个函数处理蛇吃食物后的逻辑,包括增加蛇的长度、更新分数,并在游戏地图上生成新的食物。通常,这涉及向蛇的链表头部添加一个新节点(代表蛇头),同时保留尾部节点,从而使蛇身变长。
void EatFood(pSnake ps, pSnakeNode pNext) { //是食物就吃掉 //头插,不释放尾结点 pNext->next = ps->pSnake; ps->pSnake = pNext; pSnakeNode cur = ps->pSnake; //打印蛇 while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } ps->Score += ps->FoodWeight; //释放旧食物 free(ps->pFood); //创建新食物 CreateFood(ps); }4.3.3.3 NotEatFood 不吃食物函数
如果蛇头的下一个位置没有食物,NotEatFood函数被调用。这个函数处理蛇在没有吃到食物时的移动,包括向蛇头添加一个新节点,同时删除尾部节点,使得蛇的总长度保持不变。
void NotEatFood(pSnake ps, pSnakeNode pNext) { //不是就正常走一步 //头插 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(" "); //给1分 ps->Score++; //删除尾结点 free(cur->next); cur->next = NULL; }4.3.3.4 KillByWall 检测撞墙
KillByWall函数用于检测蛇头是否撞到墙壁。这涉及判断蛇头的新位置是否超出了游戏地图的边界。如果蛇头撞到墙壁,游戏将结束,通常这会触发游戏结束的逻辑并显示相应的游戏结束信息。
bool KillByWall(pSnake ps) { if (ps->pSnake->x pSnake->x >= 56 || ps->pSnake->y pSnake->y >= 26) { ps->Status = KILL_BY_WALL; return true; } return false; }4.3.3.5 KillBySelf 检测撞自身
类似地,KillBySelf函数检测蛇头是否撞到了蛇身的其它部分。这通常涉及遍历蛇身的链表,检查是否有任何节点的位置与蛇头的新位置相同。如果蛇头撞到了自身,游戏同样会结束。
bool 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; return true; } cur = cur->next; } return false; }4.4GameEnd 游戏结束函数
GameEnd函数在贪吃蛇游戏中扮演着关键角色,它负责处理游戏结束时的所有逻辑。这个函数在游戏的主循环检测到结束条件(如蛇撞墙、撞到自身或玩家主动退出)时被调用。以下是GameEnd函数包含的关键元素和步骤:
显示结束信息:通常,游戏结束时会在屏幕上显示相关信息,告知玩家游戏已结束。这可能包括玩家的最终得分、游戏结束的原因(例如“你撞到墙了”或“你撞到了自己”)以及一些友好的结束语。
资源清理:游戏结束时,需要适当地释放占用的资源。这可能包括释放蛇身链表占用的内存、关闭图形界面、释放其他占用的资源等。正确的资源清理是保证程序稳定性和防止内存泄漏的重要步骤。
重启或退出选项:通常,游戏结束后会提供给玩家重新开始或退出游戏的选项。这样,玩家可以选择再次挑战或结束游戏。
//游戏结束 void GameEnd(pSnake ps) { SetPos(30, 12); switch (ps->Status) { case ESC: printf("主动退出游戏\n"); break; case KILL_BY_WALL: printf("你撞到墙了哦!!\n"); break; case KILL_BY_SELF: printf("你咋撞到自己了呢?\n"); break; } SetPos(30, 13); printf("你的得分是:%d\n", ps->Score - 1); //释放贪吃蛇的链表资源 pSnakeNode cur = ps->pSnake; while (cur) { pSnakeNode del = cur; cur = cur->next; free(del); } }5.完整代码及程序运行示例
贪吃蛇完整代码如下:
5.1头文件Snake.h
Snake.h #pragma once #include #include #include #include #include #include #define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0) //按键按下 #define WALL L'■' #define BODY L'◎' #define FOOD L'★' //默认起始坐标 #define POS_X 24 #define POS_Y 5 enum GAME_STATUS { OK=1,//正常运行 ESC,//按ESC退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//撞自身 }; enum DIRECTION { UP=1, DOWN, LEFT, RIGHT }; //定义蛇身节点 typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode; //定义蛇 typedef struct Snake { pSnakeNode pSnake;//维护整条蛇的指针 pSnakeNode pFood;//维护食物的指针 int Score;//分数 long long FoodWeight;//一个食物的分数 int SleepTime;//蛇休眠的时间 enum GAME_STATUS Status;//游戏状态 enum DIRECTION Dir;//蛇的方向 }Snake, * pSnake; void SetPos(int x, int y); //设置光标位置 void WelcomeToGame(); //欢迎界面 void CreateMap(); //创建地图 void InitSnake(pSnake ps);//初始化蛇 void CreateFood(pSnake ps);//创建食物 void PrintHelpInfo();//打印帮助信息 void Pause();//暂停 void SnakeMove(pSnake ps);//蛇身运动 bool NextIsFood(pSnake ps, pSnakeNode pNext);//判断下一个位置是否为食物 void EatFood(pSnake ps, pSnakeNode pNext);//吃到食物 void NotEatFood(pSnake ps, pSnakeNode pNext);//没吃到食物 bool KillByWall(pSnake ps);//撞墙 bool KillBySelf(pSnake ps);//撞自己 void GameStart(pSnake ps); //游戏开始 void GameRun(pSnake ps);//游戏运行 void GameEnd(pSnake ps);//游戏结束5.2源文件Snake.cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include"snake.h" //设置光标位置 void SetPos(int x, int y) { //获得设备句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //根据句柄设置光标位置 COORD pos = { x, y }; SetConsoleCursorPosition(handle, pos); } //欢迎界面 void WelcomeToGame() { HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN); //打印欢迎界面 SetPos(35, 10); printf("欢迎来到贪吃蛇游戏"); SetPos(38, 20); system("pause"); system("cls"); //功能介绍信息 system("color e8"); SetPos(15, 10); printf("游戏规则:用 ↑.↓.←.→ 分别控制蛇的移动,Ctrl为加速,Alt为减速\n"); SetPos(15, 11); printf("游戏说明:蛇吃到食物长度加1,蛇撞到墙壁或者自己游戏结束\n"); SetPos(15, 13); system("pause"); system("cls"); system("color 6D"); } //创建地图 void CreateMap() { //打印上边界 SetPos(0, 0); int i = 0; for (i = 0; i SleepTime = 200; ps->FoodWeight = 100; break; case 2: ps->SleepTime = 150; ps->FoodWeight = 200; break; case 3: ps->SleepTime = 100; ps->FoodWeight = 300; break; case 4: ps->SleepTime = 50; ps->FoodWeight = 400; break; default: ps->SleepTime = 200; ps->FoodWeight = 100; break; } int i = 0; for (i = 0; i x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; //头插法 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->FoodWeight = 100; ps->pFood = NULL; ps->Status = OK; //ps->SleepTime = 200; ps->Score = 0; } } void CreateFood(pSnake ps) { int x = 0; int y = 0; again: do { x = rand() % 53 + 2; y = rand() % 25 + 1; } while (x % 2 != 0); pSnakeNode cur = ps->pSnake; while (cur) { if (x == cur->x && y == cur->y) { goto again; } cur = cur->next; } pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pFood == NULL) { perror("CreateFood:malloc"); return; } else { pFood->x = x; pFood->y = y; SetPos(x, y); wprintf(L"%lc", FOOD); ps->pFood = pFood; } } //游戏开始 void GameStart(pSnake ps) { system("mode con cols=100 lines=30"); system("color 0f"); system("title 贪吃蛇GreedySnake"); //隐藏光标 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(handle, &CursorInfo); CursorInfo.bVisible = false; SetConsoleCursorInfo(handle, &CursorInfo); //打印欢迎界面 WelcomeToGame(); //绘制地图 CreateMap(); //初始化蛇 InitSnake(ps); //创建食物 CreateFood(ps); } //打印帮助信息 void PrintHelpInfo() { SetPos(62, 14); printf("注意事项:"); SetPos(62, 15); printf("1.不能撞墙,不能碰到自己"); SetPos(62, 16); printf("2.用 ↑.↓.←.→ 分别控制蛇移动"); SetPos(62, 17); printf("3.Ctrl为加速, Alt为减速"); SetPos(62, 18); printf("4.按空格键暂停"); SetPos(62, 19); printf("5.按ESC键退出游戏"); SetPos(62, 21); printf("游戏制作 @wxk2227814847"); SetPos(62, 22); printf("版权所有 侵权必究"); } //游戏运行 void Pause() { while (1) { Sleep(300); if (KEY_PRESS(VK_SPACE)) { break; } } } void SnakeMove(pSnake ps) { pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNext == NULL) { perror("SnakeMove():malloc"); return; } pNext->next = NULL; switch (ps->Dir) { 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; 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; } //下一个坐标处是否为食物 if (NextIsFood(ps, pNext)) { //是食物就吃掉 EatFood(ps, pNext); } else { //不是就正常走一步 NotEatFood(ps, pNext); } //检测撞墙 KillByWall(ps); //检测撞到自己 KillBySelf(ps); } bool KillByWall(pSnake ps) { if (ps->pSnake->x pSnake->x >= 56 || ps->pSnake->y pSnake->y >= 26) { ps->Status = KILL_BY_WALL; return true; } return false; } bool 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; return true; } cur = cur->next; } return false; } void EatFood(pSnake ps, pSnakeNode pNext) { //是食物就吃掉 //头插,不释放尾结点 pNext->next = ps->pSnake; ps->pSnake = pNext; pSnakeNode cur = ps->pSnake; //打印蛇 while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } ps->Score += ps->FoodWeight; //释放旧食物 free(ps->pFood); //创建新食物 CreateFood(ps); } void NotEatFood(pSnake ps, pSnakeNode pNext) { //不是就正常走一步 //头插 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(" "); //给1分 ps->Score++; //删除尾结点 free(cur->next); cur->next = NULL; } bool NextIsFood(pSnake ps, pSnakeNode pNext) { return (ps->pFood->x == pNext->x) && (ps->pFood->y == pNext->y); } void GameRun(pSnake ps) { //打印帮助信息 PrintHelpInfo(); do { SetPos(62, 10); printf("总得分:%5d\n", ps->Score); SetPos(62, 11); printf("每个食物得分:%5d\n", ps->FoodWeight); //检测按键 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 = ESC; break; } else if (KEY_PRESS(VK_SPACE)) { Pause();//暂停,恢复 } else if (KEY_PRESS(VK_LCONTROL) || KEY_PRESS(VK_RCONTROL)) { if (ps->SleepTime >= 80) { ps->SleepTime -= 20; ps->FoodWeight += 10; } } else if (KEY_PRESS(VK_LMENU) || KEY_PRESS(VK_RMENU)) { if (ps->FoodWeight >= 50) { ps->SleepTime += 20; ps->FoodWeight -= 10; } } //睡眠 Sleep(ps->SleepTime); //走一步 SnakeMove(ps); } while (ps->Status == OK); } //游戏结束 void GameEnd(pSnake ps) { SetPos(30, 12); switch (ps->Status) { case ESC: printf("主动退出游戏\n"); break; case KILL_BY_WALL: printf("你撞到墙了哦!!\n"); break; case KILL_BY_SELF: printf("你咋撞到自己了呢?\n"); break; } SetPos(30, 13); printf("你的得分是:%d\n", ps->Score - 1); //释放贪吃蛇的链表资源 pSnakeNode cur = ps->pSnake; while (cur) { pSnakeNode del = cur; cur = cur->next; free(del); } }5.3源文件test.cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include"snake.h" #include #include #include void test_rainbow() { // 创建更大的绘图窗口 initgraph(1024, 768); // 画渐变的天空(通过亮度逐渐增加) float H = 190; // 色相 float S = 1; // 饱和度 float L = 0.7f; // 亮度 for (int y = 0; y 344; r--) { H += 5; setlinecolor(HSLtoRGB(H, S, L)); circle(512, 768, r); // 调整圆心位置以适应新窗口 } // 在中间打印“再见” settextcolor(WHITE); settextstyle(200, 0, _T("Consolas")); static int i = 0; if (i == 0) { outtextxy(200, 120, _T("欢迎!")); i++; } else outtextxy(200, 120, _T("再见!")); // 按任意键退出 //_getch(); Sleep(1500);//延迟1.5秒 closegraph(); } void test() { int choice = 0; srand((unsigned int)time(NULL)); do { Snake snake = { 0 }; GameStart(&snake);//游戏开始 GameRun(&snake);//游戏运行 GameEnd(&snake);//游戏结束 SetPos(20, 15); printf("再来一局吗?(1:是 0:否):"); scanf("%d", &choice); } while (choice); SetPos(0, 27); } int main() { //修改适配本地中文环境 setlocale(LC_ALL, ""); //测试函数 test_rainbow(); test(); test_rainbow(); return 0; }5.4程序运行截图
(有点让人眼前一黑的感觉,请见谅)
6.结语及拓展分析
6.1结语
通过本教程,我们深入探讨了如何使用C语言结合Win32 API来编写一个贪吃蛇游戏。这个项目不仅让我们回顾了一些基本的编程概念,如控制结构、数据结构(链表)、和内存管理,还让我们学习了如何在Windows环境下进行系统级编程,特别是如何处理屏幕输出和键盘输入。贪吃蛇游戏,虽然简单,却是理解编程基础和游戏开发原理的有效途径。
6.2拓展分析
对于希望进一步提升这个项目的开发者来说,有许多可能的拓展方向:
图形用户界面(GUI): 尽管控制台程序有其独特的魅力,但将游戏迁移到图形用户界面将大大增强其视觉吸引力和用户交互体验。可以考虑使用诸如SDL、SFML或OpenGL等库来创建更丰富的图形界面。
增加难度级别: 随着玩家的分数增加,游戏可以逐渐增加难度,比如增加更复杂的地图布局或设置障碍物等。
声音效果: 添加背景音乐和游戏效果音(如吃食物的声音、游戏结束的声音)可以增强游戏的沉浸感。
联网功能: 实现多人在线游戏或排行榜系统,允许玩家与他人竞争或分享他们的高分。
代码优化和重构: 随着功能的增加,代码的复杂性也会增加。定期对代码进行优化和重构,确保其可维护性和扩展性,是非常重要的。
移动平台适配: 考虑将游戏移植到移动平台,如Android或iOS,这需要使用相应的开发工具和语言,如Java、Kotlin或Swift。
增加用户自定义选项: 如允许玩家自定义蛇的颜色、游戏背景等,可以提高游戏的用户友好性和个性化。