项目实践:贪吃蛇

引言

贪吃蛇作为一项经典的游戏,想必大家应该玩过。贪吃蛇所涉及的知识也不是很难,涉及到一些C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32  API等。这里我会介绍贪吃蛇的一些思路。以及源代码也会给大家放到文章末尾。

我们最终的游戏的这样:

在真正的开始制作游戏之前,我们需要先了解一下制作贪吃蛇游戏的预备知识。如果已经知晓了这些预备知识,可以直接跳到"二"。 

一.Win32  API介绍 

1.Win32  API

Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为Application  Programming  Interface,简称  API  函数。WIN32 API也就是Microsoft  Windows 32位平台的应用程序编程接口。

2.控制台程序

平常我们运行起来的黑框程序其实就是控制台程序。我们可以通过cmd命令来控制台窗口的长宽。

#include<stdio.h>
int main()
{//设置控制台的窗口的行为30,列为30system("mode con cols=30 lines=30");//设置控制台的窗口的名字为贪吃蛇system("title 贪吃蛇");system("pause");return 0;
}

呈现出来的窗口为: 

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

COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。其实就跟我们数学里面学习的坐标差不多,只不过Y轴的正负不一样了:

 COORD类型的结构体声明:

typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;

我们就可以用它给坐标赋值:

COORD pos = { 10, 15 };

此时的pos代表的就是(10,15)的坐标。

4.GetStdHandle

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

简单的说,我们在炒菜的时候,需要拿着铲子的把手,进行炒菜的动作。相同的,我们在操作设备的时候,也是需要拿着一个“把手”,而这个把手我们称作“句柄”。从而进行对设备的操作。

HANDLE GetStdHandle(DWORD nStdHandle);

 nStdHandle参数有三种,可为其一:

 比如:

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

HANDLE可以被看作是一个指向资源的指针,其实质上是一个整数值。通过使用HANDLE,程序可以访问和操作操作系统提供的各种资源。

5.GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息

语法:

BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

第一个参数hConsoleOutput就是控制台屏幕缓冲区的句柄。

第二个参数lpConsoleCursorInfo是一个指向PCONSOLE_CURSOR_INFO类型的指针。

先看一下实例,后面在介绍什么是PCONSOLE_CURSOR_INFO

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 

5.1.CONSOLE_CURSOR_INFO

这个结构体包含了控制台与光标有关的信息

typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

dwSize由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条(比如dwSize的值为25,其实就是占一个单元格的25%)。

bVisible,其实也可以看到,它的类型是BOOL。它就是游标的可见性。如果光标可见,则此成员为TRUE。反之就是FALSE。

举个例子,比如我们在打字的时候,我们的光标就一闪一闪的。那么在控制台上我们就可以修改bVisible的值为false,就可以做到隐藏光标。

就像是上面我定义的一个结构体变量:CONSOLE_CURSOR_INFO CursorInfo;

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

6.SetConsoleCursorInfo

既然我们获得了光标的信息,上面我们也说了我们想修改bVisible的值,那么我们就需要有一个设置,真正的把光标给改变。

语法:

BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

它的结构体的内容跟上面的GetConsoleCursorInfo一样。

所以我们就可以得到一个隐藏或者改变光标占比的操作:

HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//先获得句柄
//影藏光标操作 
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息 
CursorInfo.bVisible = false; //隐藏控制台光标 
SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态 

7.SetConsoleCursorposition

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

拥有了这个函数我们就可以根据坐标随意的在屏幕的任何一个位置打印内容。

语法:

BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD pos
);

第一个参数依然是句柄,第二个参数就是我们想要光标出现的位置。

看一个实例:

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

接下来如果我们想要在控制台上输出内容,就是在我们设置的pos位置开始了。

8.GetAsyncKeyState

这个函数对于实现我们的贪吃蛇的项目十分重要。

语法:

SHORT GetAsyncKeyState(int vKey
);

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

GetAsyncKeyState 的返回值是short类型,在上一次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬 起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。

我们可以建立一个宏,来判断是否按键被按过:

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

关于虚拟键代码有很多:比如从F1到F12的

所有的键盘上的按键都可以用虚拟键代码来代替。

二.贪吃蛇游戏设计

1.地图

在我们开始游戏之前,我们需要有一些提示信息给玩家观看,那么我们就需要在屏幕上打印如下的信息:

 

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★普通的字符是占⼀个字节的,这类宽字符是占用2个字节。过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数(locale.h)。

1.1.setlocale函数

这个函数包含于头文件locale.h中。

它的作用就是修改当前地区

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

第一个参数:通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项

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

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

•LC_MONETARY:影响货币格式。

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

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

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

第二个参数:"C"(正常模式)和""(本地模式)。

注意:用""作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

例如:

setlocale(LC_ALL, " ");//切换到本地环境 

1.2.宽字符的打印

既然有了设置为本地模式,那么我们打印宽字符的方式也应该有一些改变了。

宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls。

比如:

这就是宽字符的打印。

1.3.地图坐标 

现在我们知晓了怎么样在屏幕上打印宽字符。我们发现我们在屏幕上打印这些汉字,一些两字节的宽字符的时候,它们的位置是需要我们自己来设置的(上面的Win32 API的6已经介绍了怎么找坐标)。那么我们应该需要知道我们控制台上的坐标是怎么样分布的。

我们假设一个27行58列的棋盘,真正在控制台上的分分布是这样的:

注意观察它们的横坐标和纵坐标的大小关系,差不多两个横坐标的长度才等于一个纵坐标的长度。

 2.蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的⼀个坐标处,比如(24,5)处开始出现蛇,连续5个节点。关于食物,就是在墙体内随机生成⼀个坐标,坐标不能和蛇的身体重合,然后打印★。

注意:不论是蛇身还是食物,它们的横坐标都必须是2的倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。

 3.数据结构设计

现在,我们知道了地图,蛇身,食物的设计。我们可以先大体的思考一下,我们该怎么样维护这条贪吃蛇,这条贪吃蛇的本质是什么?

3.1.贪吃蛇的节点结构

贪吃蛇的本质就是链表,后面我们要进行的贪吃蛇吃食物,实际上就是链表的插入。

typedef struct SnakeNode
{int x;//横坐标int y;//纵坐标struct SnakeNode* next;//下一个节点
}SnakeNode,* pSnakeNode;

 3.2.蛇的方向

enum DIRECTION
{UP = 1,//上DOWN,//下LEFT,//左RIGHT//右
};

 3.3.游戏状态

enum GAME_STATUS
{OK,//状态正常KILL_BY_WALL,//撞到墙KILL_BY_SELF,//撞到自己END_NORMAL//正常退出
};

3.4.维护贪吃蛇

typedef struct Snake
{pSnakeNode _pSnake;//指向蛇头的指针pSnakeNode _pFood;//指向食物节点的指针enum DIRECTION _dir;//蛇的方向enum GAME_STATUS _status;//蛇的状态int _food_weight;//食物分数int _score;//总分数int _sleep_time;//休息时间,时间越短,速度越快
}Snake,*pSnake;

到这里差不多贪吃蛇的前期的准备工作都做完了,后面的“三”我会详细的解释贪吃蛇实现的每一个步骤。

三.贪吃蛇的核心逻辑

在写整个游戏的代码过程中,我们大致分为三步:游戏开始(GameStar):完成游戏的初始化。游戏运行(GameRun):完成游戏运行逻辑的实现。游戏结束(GameEnd):完成游戏结束的说明,实现资源释放。

1.游戏开始(GameStar)

这个过程你,主要就是把给玩家看的东西给展现出来,比如地图的制作,地图上的文字,光标的隐藏,食物,蛇等等。

如下就是我们这个过程需要做的事情:

//游戏开始
void GameStart(pSnake ps)
{//把控制台窗口设置为行30,列100,并且改变名称为贪吃蛇system("mode con cols=100 lines=30");system("title 贪吃蛇");//获得句柄HANDLE houtpot = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;//得到光标信息GetConsoleCursorInfo(houtpot, &CursorInfo);CursorInfo.bVisible = false;//改变光标信息SetConsoleCursorInfo(houtpot, &CursorInfo);//打印欢迎界面WelcomeToGame();//打印地图CreateMap();//初始化蛇InitSnake(ps);//创造食物CreateFood(ps);
}

我们一步一步来做这些事情。

1.1.打印欢迎界面(WelcomeToGame)

为了方便我们的使用,我们把设置光标位置的方法,单独的分装一个函数。

void SetPos(short x, short y)
{COORD pos = { x,y };HANDLE houtpot = GetStdHandle(STD_OUTPUT_HANDLE);SetConsoleCursorPosition(houtpot, pos);
}

然后就是我们欢迎界面的打印:

//打印欢迎界面
void WelcomeToGame()
{SetPos(40, 15);//设置光标出现的位置printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause");system("cls");//清屏SetPos(25, 12);printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(25, 13);printf("加速将能得到更高的分数。\n");SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause");system("cls");
}

1.2.打印地图(CreatMap)

这里其实就是对墙的打印,我们需要用到对宽字符的打印方式。为了好表示我们可以用define定义一下。

#define WALL L'□'

后面依然是考验我们的数学能力,实际也就是数坐标:

上墙的坐标为:(0,0)——(56,0)

下墙的坐标为:(0,,26)——(56,26)

左墙的坐标为:(0,1)——(0,25)

右墙的坐标为:(56,1)到(56,25)

//打印地图
void CreateMap()
{int i = 0;//上(0,0)-(56, 0) SetPos(0, 0);for (i = 0; i < 58; i += 2)//因为宽字符一个占俩,所以要+2{wprintf(L"%lc", WALL);}//下(0,26)-(56, 26) SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%lc", WALL);}//左 //x是0,y从1开始增⻓ for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//x是56,y从1开始增⻓ for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}

1.3.初始化蛇(InitSnake)

同样的,我们定义一下蛇身:

#define BODY L'●'

我们一开始就让蛇的长度为5,吃掉食物让蛇的身体增长。其实就涉及到了链表,所谓让蛇增长就是让链表的长度增加(这里我们利用头插)。

我们定义一下身刚开始出现的位置:

#define POS_X 24
#define POS_Y 5

然后就是创建蛇身,打印蛇身,初始化其他的数据:

//初始化蛇
void InitSnake(pSnake ps)
{int i = 0;pSnakeNode cur = NULL;for (i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}cur->next = NULL;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->_dir = RIGHT;//默认向右ps->_score = 0;ps->_food_weight = 10;ps->_sleep_time = 200;//单位是毫秒ps->_status = OK;
}

1.4.创造食物(CreatFood)

我们依然是把食物定义一下:

#define FOOD L'★'

关于食物的创造,我们就需要注意一下随机性了:

//初始化食物的节点
void CreateFood(pSnake ps)
{int x = 0;int y = 0;//x必须是2的倍数//x:2~54//y: 1~25
again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x和y的坐标不能和蛇的身体坐标冲突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;}pFood->x = x;pFood->y = y;pFood->next = NULL;SetPos(x, y);//定位位置wprintf(L"%lc", FOOD);ps->_pFood = pFood; 
}

2.游戏运行(GameRun)

上面我们已经有了蛇身和食物,在这里就是我们要想办法让蛇给动起来,吃食物的过程,加速,减速,食物分数的变化都是在这里实现。

也是为了方便,获取按键信息的时候我们也定义一下

#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)

之后就是正式的游戏运行:

//游戏运行
void GameRun(pSnake ps)
{//打印右侧帮助信息 PrintHelpInfo();do{SetPos(64, 10);printf("得分:%d ", ps->_Score);printf("每个⻝物得分:%d分", 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_SPACE)){pause();}else if (KEY_PRESS(VK_ESCAPE)){ps->_Status = END_NOMAL;break;}else if (KEY_PRESS(VK_F3)){if (ps->_SleepTime >= 80){ps->_SleepTime -= 30;ps->_foodWeight += 2;//⼀个⻝物分数最⾼是20分 }}else if (KEY_PRESS(VK_F4)){if (ps->_SleepTime < 320){ps->_SleepTime += 30;ps->_foodWeight -= 2;//⼀个⻝物分数最低是2分 }}//蛇每次一定之间要休眠的时间,时间短,蛇移动速度就快 Sleep(ps->_SleepTime);SnakeMove(ps);} while (ps->_Status == OK);
}

接下来就一一介绍中间涉及到的一些函数 

2.1.打印右侧帮助信息(PrintHelpInfo)

这个就是想在游戏界面的右方提示一下,当前分数什么的:

//打印右侧帮助信息
void PrintHelpInfo()
{//打印提⽰信息 SetPos(64, 15);printf("不能穿墙,不能咬到自己\n");SetPos(64, 16);printf("用↑.↓.←.→分别控制蛇的移动.");SetPos(64, 17);printf("F3 为加速,F4 为减速\n");SetPos(64, 18);printf("ESC :退出游戏.space:暂停游戏.");
}

2.2.暂停响应(pause)

这个函数其实就是在我们暂停游戏之后,我们重新去运行游戏用的:

//暂停响应
void pause()//暂停 
{while (1){Sleep(300);if (KEY_PRESS(VK_SPACE)){break;}}
}

2.3.蛇身移动(SnakeMove)

这个地方所牵扯到的函数有点多,也是整个游戏最核心的地方

//蛇的移动
void SnakeMove(pSnake ps)
{//创建下⼀个节点 pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}//确定下⼀个节点的坐标,下⼀个节点的坐标根据蛇头的坐标和方向确定 switch (ps->_Dir){case UP:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;//如果是上,横坐标不变,纵坐标减一}break;case DOWN:{pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;}break;case LEFT:{pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;}break;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);
}

这里有牵扯到了五个函数下面一一来介绍

注意:下面提到的psn参数都是蛇要移动到下一个节点的位置。                 

2.3.1.下一个位置是不是食物(NextIsFood)

返回值是int,如果成立就返回1,不成立就返回0.

//判断下一个节点是否有食物
int NextIsFood(pSnakeNode pn, pSnake ps)
{return(ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
2.3.2.吃食物(EatFood)

因为食物的类型跟我们蛇节点的类型是一样的,所以如果有食物的话我们就不需要把蛇的结尾打印成空格。

//蛇的下一个节点有食物
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);
}
2.3.3.没有食物(NoFood)

没有食物的情况挺容易出错的,因为我们的循环条件变了。

//蛇的下一个节点没有食物
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;}//把最后一个结点打印成空格SetPos(cur->next->x, cur->next->y);printf("  ");//释放最后一个结点free(cur->next);//把倒数第二个节点的地址置为NULLcur->next = NULL;
}
2.3.4.撞到墙(KillByWall)
//撞到墙
void KillByWall(pSnake ps)
{if ((ps->_pSnake->x == 0)|| (ps->_pSnake->x == 56)|| (ps->_pSnake->y == 0)|| (ps->_pSnake->y == 26))//分别是上下左右墙的边界{ps->_Status = KILL_BY_WALL;//这里我们把蛇的状态改掉,后面就会跳出循环break;}}
2.3.5.撞到自己(KillBySelf)
//撞到自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnake->next;while (cur){if ((ps->_pSnake->x == cur->x)&& (ps->_pSnake->y == cur->y))//身体的每一个节点的坐标都不可以与蛇头的坐标相同{ps->_Status = KILL_BY_SELF;break;}cur = cur->next;}
}

到这里差不多所有运行需要的代码就结束了。接下来是结束工作。

3.游戏结束(GameEnd)

当游戏的状态不再是OK的时候,游戏就结束了。

//游戏的善后
void GameEnd(pSnake ps)
{SetPos(24, 12);switch (ps->_status){case END_NORMAL:wprintf(L"您主动结束游戏\n");break;case KILL_BY_WALL:wprintf(L"您撞到墙上,游戏结束\n");break;case KILL_BY_SELF:wprintf(L"您撞到了自己,游戏结束\n");break;}//释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}

四.整体代码分享

snake.h

#pragma once
#include<stdlib.h>
#include<stdio.h>
#include<locale.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define KEY_PRESS(vk)  ((GetAsyncKeyState(vk)&1)?1:0)
//蛇的状态
enum GAME_STATUS
{OK,KILL_BY_WALL,//撞到墙KILL_BY_SELF,//撞到自己END_NORMAL//正常退出
};
//蛇的方向
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;//指向食物节点的指针enum DIRECTION _dir;//蛇的方向enum GAME_STATUS _status;//蛇的状态int _food_weight;//食物分数int _score;//分数int _sleep_time;//休息时间,时间越短,速度越快
}Snake,*pSnake;//函数的声明//定位光标位置
void SetPos(short x, short y);//游戏的初始化
void GameStart(pSnake ps);//欢迎界面的打印
void WelcomeToGame();//创建地图
void CreateMap();//初始化蛇身
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//游戏运行的逻辑
void GameRun(pSnake ps);//蛇的移动-走一步
void SnakeMove(pSnake ps);//判断下一个坐标是否是食物
int 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);void test();

snake.c

#include"snack.h"
void SetPos(short x, short y)
{//获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标的位置COORD pos = { x,y };SetConsoleCursorPosition(houtput, pos);
}//打印欢迎界面
void WelcomeToGame()
{SetPos(40, 14);wprintf(L"欢迎来到贪吃蛇游戏\n");SetPos(42, 20);system("pause");system("cls");SetPos(40, 14);wprintf(L"用↑  ↓  ←  →来控制移动,按F3加速,F4减速\n");SetPos(40, 15);wprintf(L"加速可以获得更高的分数");SetPos(40, 20);system("pause");system("cls");
}
//打印地图
void CreateMap()
{int i = 0;//上(0,0)-(56, 0) SetPos(0, 0);for (i = 0; i < 58; i += 2)//因为宽字符一个占俩,所以要+2{wprintf(L"%lc", WALL);}//下(0,26)-(56, 26) SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%lc", WALL);}//左 //x是0,y从1开始增⻓ for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//x是56,y从1开始增⻓ for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}//初始化蛇
void InitSnake(pSnake ps)
{int i = 0;pSnakeNode cur = NULL;for (i = 0; i < 5; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake()::malloc()");return;}cur->next = NULL;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->_dir = RIGHT;//默认向右ps->_score = 0;ps->_food_weight = 10;ps->_sleep_time = 200;//单位是毫秒ps->_status = OK;
}
//初始化食物的节点
void CreateFood(pSnake ps)
{int x = 0;int y = 0;//x必须是2的倍数//x:2~54//y: 1~25
again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x和y的坐标不能和蛇的身体坐标冲突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;}pFood->x = x;pFood->y = y;pFood->next = NULL;SetPos(x, y);//定位位置wprintf(L"%lc", FOOD);ps->_pFood = pFood;                                                    
}void GameStart(pSnake ps)
{//1.先设置窗口大小,再进行光标隐藏system("mode con cols=100 lines=30");system("title 贪吃蛇");HANDLE houtpot = GetStdHandle(STD_OUTPUT_HANDLE);//光标隐藏CONSOLE_CURSOR_INFO Cursorinfo;GetConsoleCursorInfo(houtpot, &Cursorinfo);//获取控制台光标信息Cursorinfo.bVisible = false;//隐藏光标SetConsoleCursorInfo(houtpot, &Cursorinfo);//设置system("pause");//2.打印欢迎界面WelcomeToGame();//3.创建地图CreateMap();//4.创建蛇InitSnake(ps);//5.创建食物CreateFood(ps);
}//打印帮助信息
void PrintHelpInfo()
{SetPos(64, 14);wprintf(L"%ls", L"不能穿墙,不能咬到自己");SetPos(64, 15);wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");SetPos(64, 16);wprintf(L"%ls", L"按F3加速,F4减速");SetPos(64, 17);wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");SetPos(64, 18);wprintf(L"%ls", L"李制作");
}
//蛇的停顿的解除
void Pause()
{while (1){Sleep(200);if (KEY_PRESS(VK_SPACE)){break;}}
}
//判断下一个节点是否有食物
int 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;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;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);//把倒数第二个节点的地址置为NULLcur->next = NULL;
}
//检测蛇是否撞墙
void KillByWall(pSnake ps)
{if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56 ||ps->_pSnake->y == 0 || ps->_pSnake->y == 26){ps->_status = KILL_BY_WALL;}
}
//检测蛇是否撞到自己
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;}
}
//蛇的移动
void SnakeMove(pSnake ps)
{//创建一个结点,表示蛇即将到的下一个节点pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNextNode == NULL){perror("SnakeMove()::malloc()");return;}switch (ps->_dir){case UP:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y - 1;break;case DOWN:pNextNode->x = ps->_pSnake->x;pNextNode->y = ps->_pSnake->y + 1;break;case LEFT:pNextNode->x = ps->_pSnake->x - 2;pNextNode->y = ps->_pSnake->y;break;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);
}void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//打印总分数和食物的分值SetPos(64, 10);printf("总分数:%d\n", ps->_score);SetPos(64, 11);printf("当前食物的分数:%2d\n", ps->_food_weight);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_SPACE)){Pause();}else if (KEY_PRESS(VK_ESCAPE)){//正常退出游戏ps->_status = END_NORMAL;}else if (KEY_PRESS(VK_F3)){//加速if (ps->_sleep_time > 60){ps->_sleep_time -= 30;ps->_food_weight += 2;}}else if (KEY_PRESS(VK_F4)){//减速if (ps->_food_weight > 2){ps->_sleep_time += 30;ps->_food_weight -= 2;}}SnakeMove(ps);//蛇走一步的过程Sleep(ps->_sleep_time);} while (ps->_status == OK);
}//游戏的善后
void GameEnd(pSnake ps)
{SetPos(24, 12);switch (ps->_status){case END_NORMAL:wprintf(L"您主动结束游戏\n");break;case KILL_BY_WALL:wprintf(L"您撞到墙上,游戏结束\n");break;case KILL_BY_SELF:wprintf(L"您撞到了自己,游戏结束\n");break;}//释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}
}

test.c

#include"snack.h"
//完成的是游戏的测试逻辑
void test()
{int ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化游戏//1. 打印环境界面//2. 功能介绍//3. 绘制地图//4. 创建蛇//5. 创建食物//6. 设置游戏的相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏 - 善后工作GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}
int main()
{//先设置适配本地环境setlocale(LC_ALL, "");//创建随机种子srand((unsigned int)time(NULL));//调用函数test();return 0;
}

感谢大家的观看,如有错误,请多多指出

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/1080.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

优雅的最大公约数函数

记录一个极其优雅的最大公约数方法 // 递归形式 int gcd(int a, int b) {return b 0 ? a : gcd(b, a % b); }这里求最大公约数的方法使用了辗转相除法&#xff0c;只是比循环求最大公约数的方法更加优雅与简洁&#xff1a; // 迭代形式 int gcd(int a, int b) {while(b ! 0…

电大搜题微信公众号:福建开放大学学子的学习新篇章

在当今信息化时代&#xff0c;学习已经成为每个人不可或缺的一部分。福建开放大学&#xff0c;作为广播电视大学的重要一员&#xff0c;始终致力于为学生提供优质、灵活的教育资源。而电大搜题微信公众号的推出&#xff0c;更是为福建开放大学的学子们带来了全新的学习体验&…

【数学】常用等价无穷小及其注意事项示例

常用极限 lim ⁡ x → 0 sin ⁡ x x 1 \lim_{x \to 0} {\frac{\sin x}{x}}1 limx→0​xsinx​1 lim ⁡ x → 0 ( x 1 ) 1 x e \lim_{x \to 0} {(x1)^\frac{1}{x}}e limx→0​(x1)x1​e lim ⁡ n → ∞ a n 1 \lim_{n \to \infty} {\sqrt[n]{a}}1 limn→∞​na ​1 lim ⁡ n…

数组中两个字符串的最短距离---一题多解(贪心/二分)

点击跳转到题目 方法&#xff1a;贪心 / 二分 目录 贪心&#xff1a; 二分&#xff1a; 贪心&#xff1a; 要找出字符串数组中指定两个字符串的最小距离&#xff0c;即找出指定字符串对应下标之差的最小值 思考&#xff1a;如果是直接暴力求解&#xff0c;需要两层for循环…

VLOOKUP函数使用,为什么会报错“引用有问题”?

VLOOKUP函数的使用非常广泛&#xff0c;在excel2007版之后的软件中&#xff0c;使用VLOOKUP函数也许会遇到这样的场景&#xff0c;明明公式是没有问题的&#xff0c;公式还会报错“引用有问题”。 一、报错场景 输入公式后&#xff0c;回车确认&#xff0c;显示如下报错&…

xilinx cpri ip 开发记录

CPRI是无线通信里的一个标准协议&#xff0c;连接REC和RE的通信。 Xilinx有提供CPRI IP核。 区别于其它通信协议&#xff0c;如以太网等&#xff0c;CPRI是一个同步系统。 这就意味着两端的Master和Slave应当是同源时钟的&#xff0c;两边不存在频差&#xff0c;并且内部延时…

mysql 行锁,间隙锁,临键锁,锁范围和死锁实际例子实战

文章目录 背景锁介绍表默认数据测试唯一键记录存在事务1事务2结论 唯一键记录不存在事务1事务2结论 范围查询事务1事务2结论 普通索引存在事务1事务2总结 普通索引不存在事务A事务B结论 死锁例子 背景 想了解下RR事务如何防止幻读的&#xff0c;以及一个实际的死锁例子 锁介绍…

【计算机网络】面经

1.TCP&UDP 1.1TCP与UDP的区别 TCP传输数据稳定可靠&#xff0c;适用于对网络通信质量要求较高的场景。 面向连接。 每一条TCP有且只有两个端点&#xff0c;为一对一关系。 提供可靠交付。 全双工通信&#xff0c;全双工为即可传输又可接收。 面向字节流。 UDP的优点是速…

客户端动态降级系统

本文字数&#xff1a;4576字 预计阅读时间&#xff1a;20分钟 01 背景 无论是iOS还是Android系统的设备&#xff0c;在线上运行时受硬件、网络环境、代码质量等多方面因素影响&#xff0c;可能会导致性能问题&#xff0c;这一类问题有些在开发阶段是发现不了的。如何在线上始终…

微服务架构中的业务完整性验证设计

目录 1.概要设计 1.1 功能完整性与正确性验证 1.2 性能与响应速度验证 1.3 安全性验证 1.4 容错性与恢复能力验证 1.5 监控与日志记录验证 2.技术实现 2.1 测试策略与工具选择 2.2 身份验证与授权 2.3 数据一致性与事务管理 2.4 监控与日志 2.5 容错与恢复 2.6 数…

【linux kernel】 一文总结linux内核中的kobject、kset和ktype

文章目录 一、kobject、kset、ktype&#xff08;1-1&#xff09;kobject&#xff08;1-2&#xff09;ktype&#xff08;1-3&#xff09;kset 二、kobject操作API&#xff08;2-1&#xff09;kobject_init()&#xff08;2-2&#xff09;kobject_add()&#xff08;2-3&#xff09…

【命名空间详解】c++入门

目录 命名空间的定义 1.命名空间的正常定义 2.命名空间还可以嵌套 3. 命名空间可以合并 命名空间的使用 1.加命名空间名称及作用域限定符 2.使用using将命名空间中某个成员引入 3.使用using namespace 命名空间名称 引入 输入&#xff0c;输出 输出 命名空间的定义 …

linux命令ar使用说明

ar 建立或修改备存文件&#xff0c;或是从备存文件中抽取文件 补充说明 ar命令 是一个建立或修改备存文件&#xff0c;或是从备存文件中抽取文件的工具&#xff0c;ar可让您集合许多文件&#xff0c;成为单一的备存文件。在备存文件中&#xff0c;所有成员文件皆保有原来的属…

Java技术学习|Git

学习材料声明 尚硅谷Git入门到精通全套教程&#xff08;涵盖GitHub\Gitee码云\GitLab&#xff09; GIt Git 是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。Git 易于学习&#xff0c;占地面积小&#xff0c;性能极快。 它具有…

ARM_day8:基于iic总线的通信

一、IIC总线的基本概念&#xff1a; iic总线是一种带应答的同步的、串行、半双工的通信方式&#xff0c;支持一个主机对应多个从机。它有一根SCL&#xff08;时钟线&#xff09;和一根SDA&#xff08;数据线&#xff09;组成&#xff0c;由于只有一根数据线&#xff0c;所以它是…

英伟达大跳水!一夜暴跌10%,市值蒸发2000亿

相信大家已经在各大社交平台上看到了&#xff0c;英伟达一夜蒸发了2000亿美元&#xff01; GPT-3.5研究测试&#xff1a; https://hujiaoai.cn GPT-4研究测试&#xff1a; https://higpt4.cn Claude-3研究测试&#xff08;全面吊打GPT-4&#xff09;&#xff1a; https://hic…

大语言模型隐私防泄漏:差分隐私、参数高效化

大语言模型隐私防泄漏&#xff1a;差分隐私、参数高效化 写在最前面题目6&#xff1a;大语言模型隐私防泄漏Differentially Private Fine-tuning of Language Models其他初步和之前的基线微调模型1微调模型2通过低秩自适应进行微调&#xff08; 实例化元框架1&#xff09; 在隐…

Vue2 —— 学习(九)

目录 一、全局事件总线 &#xff08;一&#xff09;全局总线介绍 关系图 对图中的中间商 x 的要求 1.所有组件都能看到 2.有 $on $off $emit &#xff08;二&#xff09;案例 发送方 student 接收方 二、消息订阅和发布 &#xff08;一&#xff09;介绍 &#xff08…

虚拟机中的打印机,无法打印内容,打印的是白纸或英文和数字,打印不了中文

原因&#xff1a;打印机驱动设置不正确 解决方案&#xff1a; 打开打印机属性 -> 高级 -> 新驱动程序 下一页 -> Windows 更新 耐心等待&#xff0c;时间较长。 选择和打印机型号匹配的驱动&#xff0c;我选择的是&#xff1a; 虽然虚拟机和主机使用的驱动不…

跨境电商指南:防关联浏览器和云主机有什么区别?

跨境电商的卖家分为独立站卖家和平台卖家。前者会自己开设独立站点&#xff0c;比如通过 shopify&#xff1b;后者则是入驻亚马逊或 Tiktok 等平台&#xff0c;开设商铺。其中平台卖家为了扩大收益&#xff0c;往往不止开一个店铺&#xff0c;或者有店铺代运营的供应商&#xf…