前言:
大家都玩过贪吃蛇大作战吧,和扫雷,俄罗斯方块一样,作为世界上最负盛名且历史最为悠久的游戏之一,可以说,它几乎成为了人类游戏史上经典之作之一,从早期的贪吃蛇到现在的即时战略游戏,策略游戏,FPS游戏,MOBA游戏,TGA系列,角色扮演…游戏事业的发展几乎成为了最新科技的符号之一,程序是游戏的载体,那当我们已经掌握了C语言的很多知识之后,我们能否实现一个贪吃蛇游戏呢?下面让我们从无到有,来完整的书写一个C语言为基础的贪吃蛇游戏。
1.贪吃蛇游戏的大体实现思路:
任何一个游戏,我们首先都需要书写一个大体的游戏构建思路,对于一个游戏来说,常规的构建思路大致分为三个步骤(我们首先面向客户端来说):游戏选择界面(菜单),游戏运行,游戏结束。所以,我们由此需要构建的东西有:游戏的菜单窗口,游戏的地图窗口,以及配套的一些游戏提示性的话语和文字。但作为游戏的设计者,我们仅仅站在客户的角度是不够的,除了用户能看到的前端外,我们也要进行后端的处理。在后端,我们要处理的问题是:蛇如何按照我们的电脑按键输入去对应移动,如何控制蛇的速度,游戏运行方式如何判定,数据如何初始化,蛇吃掉食物后如何增加蛇的长度….针对这些问题,我大致将其整理为如下的一个游戏实现逻辑:
由上面的游戏逻辑,我们大致将我们的主程序分为三个阶段的函数来运行,第一阶段函数,第二阶段函数,第三阶段函数。
故我们的主程序就为如下:
//我这里将其放在void test()函数里,方便调试和测试 void game() { //贪吃蛇游戏大概分为三个部分进行,故我们分为三个大函数来进行 //1.游戏开始--初始化游戏的过程,将蛇自身结构体里面的数据进行初始化,地图的构造,蛇的出生点设置以及蛇的打印,游戏界面的生成等 GameStart(&snake);//传地址直接改变实参 //2.游戏运行--游戏的正常运行过程 GameRun(&snake); //3.游戏结束--对游戏中的一些占用内存的资源进行释放回收和清理 GameEnd(&snake); } int main() { test(); return 0; }
2.对象属性配置:
好,接下来让我们继续,当我们大致将大块分好后,我们就要创建我们的人物:蛇
在任何一门语言中,我们都有专门用来描述一个对象自身属性的自定义类型,在C语言中即是结构体,而在C++中则升级为了更为高级的类。故在C语言中,我们就用结构体来记录蛇的信息。
如下:
typedef struct SnakeNode { //描述蛇身节点的坐标 int x; int y; struct SnakeNode* next; }SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名 typedef struct Snake { pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键 pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理 int _Score;//累计的得分 int _FoodWeight;//吃一个食物的分数 int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快 enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种 enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了 }Snake,*pSnake;
我们首先把蛇分为两个部分,由于我们的移动涉及到蛇的每一个节点,故我们首先对蛇的每一个节点构建一个结构体,同时我们要注意,我们在控制台屏幕上的运行,本质上类似我们数学上的平面直角坐标系,其大致的坐标系如下:
以控制台屏幕的左上角为0 0顶点为中心,向右为x轴向延展,向下为y轴向延展,而我们的每一个数据的打印,构建都是以这种坐标的形式展开的,其展开的方式就类似一个二维数组。故我们蛇的移动也是以坐标为前提的移动,因此,我们对于蛇的每一个节点都要存储其坐标,这样保证了我们之后处理移动,撞墙,吃掉自身等情况时基于坐标进行处理,同时,我们的蛇是一个整体,每一个节点都要连在一起,现实中我们可以用绳子串起来,在C语言中我们就可以利用单链表来将其连起来,故我们的每一个节点还要存储一个指向next指针。
故我们的节点结构体如下:
typedef struct SnakeNode { //描述蛇身节点的坐标 int x; int y; struct SnakeNode* next; }SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名
单个节点写好后,我们还不够,我们还需要对蛇的整体可以进行掌控,故我们还需要建立一个蛇整体的结构体,来掌控蛇以及关于蛇的一切信息,在这里,让我们认真讨论一下蛇的结构体内部应该都有些什么:
typedef struct Snake { pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键 pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理 int _Score;//累计的得分 int _FoodWeight;//吃一个食物的分数 int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快 enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种 enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了 }Snake,*pSnake;
首先根据我们之前玩过的贪吃蛇的经历,我们知道我们控制的是蛇的第一个头节点,通过头节点的方向改变来控制蛇的移动,故我们的蛇结构体的第一个数据就要 是指向蛇头节点的指针_pSnake,同时,我们整个贪吃蛇游戏中和蛇交互的只有对应的食物,故我们完全可以将其放在蛇的结构体内部,从而实现蛇头位置和食物位置的判断_pFood,倘若单独写就要单独传参数,那样很麻烦。我们同时在蛇结构体里统计蛇的累计得分,每一个食物的分数(因为我们设计速度加快可以使吃一个食物所得的分数改变的机制),蛇的速度,本质上蛇的速度就是延时刷新率的改变,延时参数越大,速度越慢,反之速度越快,同时我们还需要记录游戏的状态(是撞墙还是正常游戏结束还是吃掉了自身)和蛇移动的方向,由于我们每次的移动方向只能有一个,但我们可以选择我们的4个移动方向,故我们利用一个枚举体来为存储我们每次的移动方向的选择,同理,我们的游戏状态每次也只能有一个,故我们也利用一个枚举体去存储可供我们选择的游戏状态,我们应该积累这个方法:**!!将为一个多种选择的变量存储在枚举体里,这样我们就可以实现一个选择的功能!!**由此,我们的游戏的基本对象属性配置就完成了,如下:
enum DIRECTION//蛇移动方向枚举体 { UP=1, DOWN, LEFT, RIGHT }; enum GAME_STATUS//游戏状态枚举体 { OK,//游戏正常运行 END_NORMAL,//正常退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//自己吃自己了 }; //贪吃蛇单个节点的结构体 typedef struct SnakeNode { //描述蛇身节点的坐标 int x; int y; struct SnakeNode* next; }SnakeNode,*pSnakeNode;//*pSnakeNode为这个结构体的指针重命名 //贪吃蛇个体的结构体 //我们整个游戏要控制的是蛇 typedef struct Snake { pSnakeNode _pSnake;//指向贪吃蛇头节点的指针,我们在游戏过程中是操控蛇头进行游戏,利用蛇头来判定游戏状态,故控制蛇头很关键 pSnakeNode _pFood;//指向食物的节点,本质上食物被蛇吃了后也算蛇的节点的一部分,故我们也用一个指针来管理 int _Score;//累计的得分 int _FoodWeight;//吃一个食物的分数 int _SleepTime;//蛇的速度,本质上为一个延时函数,休眠的时间越长速度越慢,反之速度越快 enum DIRECTION _Dir;//描述蛇的方向,由于蛇的方向固定,且每次只能按一个按键,故我们利用枚举体来处理,未来的Dir取值只可能是4个方向中的一种 enum GAME_STATUS _Status;//游戏状态,判断蛇是撞墙还是自己吃到了自己还是按ESC键退出了 }Snake,*pSnake;
3.游戏第一阶段———初始化阶段的具体实现:
在这个阶段:我们要实现的如下:
void GameStart(pSnake ps);//游戏第一阶段:游戏初始化阶段函数 void HideCursor();//隐藏光标 void Setpos(int x, int y);//光标位置调整 void WelcomeToGme();//打印欢迎界面 void CreateMap();//创建地图 void InitSnake(pSnake snake);//初始化蛇并在出生点打印出蛇 void CreateFood(pSnake snake);//设置第一个食物
我们统一将其放在GameStart函数里面:
那首先我们就需要学会掌控控制台:
何为控制台,即是我们或许运行过无数次跳出来的黑色框框,对于控制台,C语言使用system(“ ”)来调用windows指令来执行控制台的指令(其实Windows控制台和LINUX差不多,其基本的原理是相同的)在这里,我们需要掌握的几条指令如下:
1.system(“mode con cols=x lines=y”):对控制台的大小进行控制
2.system("title 对应名字“):用来改变控制台的名字
3…system(“pause”):负责暂停程序,并打印出按任意键继续,可控制输出坐标
4.system(“cls”):刷新界面,然后执行接下来的程序
注意:对于双引号里面的命令,其实就是对应着windows控制台的操作指令,只不过C语言用这种方式命令控制台执行!!
然后我们还要介绍一下何为API,这对于我们理解我们贪吃蛇的页面操作很关键:
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤ 的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程式达到开启 视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。
1.游戏欢迎界面以及游戏玩法介绍的函数:
我们大致要实现的效果如下:
你可以看到,这两张游戏界面中,我们首先修改了我们的控制台的名称,其次我们隐藏了我们的光标,然后我同时做到了在控制台上的任意位置打印输出我们的文字。
前面的改名字和暂停我已经说过,接下来让我们来说说如何改变控制台坐标以及如何隐藏光标。
1.改变控制台坐标
在windows API 中提供了一种结构体类型名为COORD,表示一个字符在控制台屏幕缓冲区上的坐标,坐标(0.0)的原点位于缓冲区顶部的左侧单元格。
结构体具体如下:
typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD;
故我们可以给坐标赋值COORD pos={x,y}.
但这样的处理控制台是不相应的,我们还需要另一个函数:SetConsoleCursorPosition
SetConsoleCursorPosition函数:
其函数的基本格式为:
BOOL WINAPI SetConsoleCursoPosition(HANDLE hconsoleOutput,COORD pos);
这个函数的作用是将我们设置的光标坐标配置到我们的屏幕设备缓冲区,我们的参数除了一个COORD的坐标值外,还需要一个获取到另一个参数,即我们的设备缓冲区的句柄,在这里我们就又需要一个函数:GetStdHandle
GetStdHandle函数:
其函数的基本格式为:
HANDLE GetStdHandle(DWORD nStdHandle);
这个函数的作用即是得到一个我们想要的设备的句柄,注意,在计算机世界中,任何设备的获得都需要得到其权限,你可以立即为这个函数是用来获取对应设备的权限来使我们可以继续使用设备的。,我们需要传入一个DWORD nstdHandle即一个标准的设备参数,在这里,我们选择STD_OUTPUT_HANDLE,即标准输出的DWORD参数作为设备参数即可。
故现在,我们将他们结合起来,就可以组成一个可以改变位置的函数–Setpos如下:
void Setpos(int x, int y)//光标位置调整 { HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { x,y }; SetConsoleCursorPosition(houtput, pos); }
这样,我们只要出入对应的坐标,下一次我们就可以从对应的位置开始输出。
2.隐藏光标:
我们在贪吃蛇游戏中是不需要使用光标的,故我们直接将其隐藏即可,其方法如下:
同样,我们首先需要先获取控制台输出缓冲区的句柄,然后,我们还需要获取到控制台光标的大小和可见性信息,这就需要我们引入下一个函数:GetConsoleCursorInfo
GetConsoleCursorInfo函数:
函数的对应参数如下:
BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO* lpConsoleCursorInfo);
这个函数的作用即是获得对应的缓冲区的光标信息(但前提是,我们的这个句柄指向的应该是一个对应的输出缓冲区,这样才有光标的概念,指向音频设备等是没法使用这个函数的),而我们的第二个参数要注意,它是一个指向PCONSOLE_CURSOR_INFO类型变量的指针,而不是这个变量本身,故我们首先需要一个对应的变量,然后我们对其传地址。
光标信息的结构体PCONSOL_CURSOR_INFO结构体的的形式如下:
typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
1.dwSize:光标填充的字符单元格的百分比,其值介于1-100之间,外观会随着百分比的变化而变化,其范围会从完全填充单元格到只剩下单元格底下的水平细线。
2.bvisible:控制光标的可见性:设置为TRUE(非0)即为可见(在不改变的情况下,默认bvisible都是TRUE),设置为false(0)即为不可见
故在这里,我们想要隐藏光标,就需要我们设置bvisible为false.
但与改变光标位置一样,我们同样也需要一个函数来配置我们的光标属性导入到屏幕缓冲区中—SetConsoleCursorInfo
SetConsoleCursorInfo函数:
其函数基本格式为:
BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO* lpConsoleCursorInfo);
这个函数的作用就是配置我们之前的关于光标的属性调整,将其导入到屏幕缓冲区中,我们的参数和前面的获取光标信息的参数一样,别忘了传指针即可。
由此,我们将上面的知识结合起来,即可组成一个隐藏光标的函数:
void HideCursor()//隐藏光标 { HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);// 获取输出设备句柄 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标属性信息 CursorInfo.bVisible = false;//调整属性隐藏光标 SetConsoleCursorInfo(houtput, &CursorInfo);//重置光标属性 }
3.打印欢迎界面和游戏提示界面:
前面的铺垫工作做好,我们就可以正式打印出来我们的界面了,其具体的函数实现如下:
void WelcomeToGame()//打印欢迎界面 { Setpos(40, 15); printf("欢迎来到贪吃蛇小游戏!"); Setpos(40, 25);//让任意继续出现在别的位置,注意pause本质上也是要打印在屏幕上的,故也要调整位置 system("pause");//按任意键继续 system("cls");//刷新界面 Setpos(25, 12); printf(" ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); Setpos(25, 13); printf("加速将能得到更多的分数。\n"); Setpos(40, 25); system("pause"); system("cls"); }
注意,我们这里是反复利用cls和pause来进行停顿的页面切换以及页面的刷新,同时利用Setpos来改变我们的打印位置
先pause后cls,就可以做到按任意键切换页面的功能,这个在制作游戏的时候很常用,要积累下来