贪吃蛇游戏C语言破解:成为编程高手的必修课!

                                                                                个人主页:秋风起,再归来~

                                                                                文章专栏:C语言实战项目                              

                                                                        个人格言:悟已往之不谏,知来者犹可追

                                                                                        克心守己,律己则安!

1、游戏效果演示

贪吃蛇游戏效果演示

2、win32 API介绍

这里实现贪吃蛇会使⽤到的⼀些Win32 API知识,接下来我介绍一下。

2.1 Win32 API

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

2.2 控制台程序

平常我们运⾏起来的⿊框程序其实就是控制台程序 我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:

设置控制台窗⼝的⼤⼩,30⾏,100列

mode con cols=100 lines=30

也可以通过命令设置控制台窗⼝的名字:

title 贪吃蛇

 这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。例如

#include<stdlib.h>
int main()
{//设置控制台相关属性//要包含头文件<stdlib.h>system("mode con cols=100 lines=25");system("title 贪吃蛇");//getchar();system("pause");return 0;
}

2.3 控制台屏幕上的坐标COORD

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

COORD类型的声明:
typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;

 默认光标的位置~

//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型
COORD pos = { 10,20 };

2.4 GetStdHandle

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

HANDLE GetStdHandle(DWORD nStdHandle);
//获得标准输出设备的句柄
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);

2.5 GetConsoleCursorInfo

GetConsoleCursorInfo检索有关指定控制台屏幕缓冲区的光标⼤⼩可⻅性的信息

BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 

2.5.1 CONSOLE_CURSOR_INFO

  CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度

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

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

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

2.6 SetConsoleCursorInfo

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

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

示例:

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include<stdbool.h>
int main()
{//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度CONSOLE_CURSOR_INFO cursorInfo = { 0 };//获得光标信息GetConsoleCursorInfo(houtput, &cursorInfo);//获得光标信息GetConsoleCursorInfo(houtput, &cursorInfo);//修改控制台的光标信息cursorInfo.dwSize = 100;//比例cursorInfo.bVisible = false;//可见度//设置光标的信息SetConsoleCursorInfo(houtput, &cursorInfo);system("pause");return 0;
}

光标被隐藏啦!   

2.7 SetConsoleCursorPosition

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

BOOL WINAPI SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD pos
);

示例: 

int main()
{//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型COORD pos = { 10,20 };//设置光标位置SetConsoleCursorPosition(houtput, pos);getchar();//system("pause");return 0;
}

SetPos:封装⼀个设置光标位置的函数

//把设置光标位置的操作封装成为一个函数
void _SetPos(short x, short y)
{//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型COORD pos = { x,y };//设置光标位置SetConsoleCursorPosition(houtput, pos);
}

2.8 GetAsyncKeyState

获取按键情况,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 )

虚拟键码 (Winuser.h) - Win32 apps

3、<locale.h>本地化

如上图游戏效果演示所示:

在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★ 普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。

这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。 C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。

C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7 位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语 国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字⺟上⽅有注⾳符 号,它就⽆法⽤ASCII码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符 号。⽐如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使⽤的编码体 系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪 怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。⽐如,130在法语编码中代表了é,在希 伯来语编码中却代表了字⺟Gimel,在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这 些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。 ⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号, 肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是GB2312,使 ⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。 

后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型 wchar_t 和宽字符的输⼊和输出函数,加⼊了头⽂件,其中提供了允许程序员针对特定 地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。在标准中,依赖地区的部分有以下⼏项:

• 数字量的格式

• 货币量的格式

• 字符集

• ⽇期和时间的表⽰形式

3.1 类项

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

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

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

• LC_MONETARY:影响货币格式。

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

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

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

3.2 setlocale函数

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

setlocale函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。

setlocale的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参 数是LC_ALL,就会影响所有的类项。

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

在任意程序执⾏开始,都会隐藏式执⾏调⽤:

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。

当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。

⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。 

setlocale(LC_ALL, " ");//切换到本地环境 
#include<locale.h>
int main()
{//C语言标准模式char* ret = setlocale(LC_ALL, NULL);printf("%s\n", ret);//本地化之后的模式ret = setlocale(LC_ALL, "");printf("%s\n", ret);return 0;
}

3.3宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?

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

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

#include<locale.h>
int main()
{//本地化setlocale(LC_ALL, "");//打印宽字符wchar_t ch1 = L'●';wchar_t ch2 = L'□';wchar_t ch3 = L'★';wprintf(L"%lc\n", ch1);wprintf(L"%lc\n", ch2);wprintf(L"%lc\n", ch3);return 0;
}

从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置 但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果 要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。

 4、游戏实现的整体思路

1. (第一界面)通过贪吃蛇游戏的视频演示我们可以知道我们首先要在屏幕上打印欢迎界面

2. (第二界面)然后再在屏幕上打印帮助手册

3. (第三界面)接着在屏幕上打印游戏运行时的界面

4. (第四界面)游戏结束后再在屏幕上打印结束原因和最后得分

5. (第五界面)打印玩家是否想在来一局

 4.1 我们要用到的数据结构

实现这些步骤前,我们先要思考并完成我们要用到的数据结构

在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信 息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,

所以蛇节点结构如下:

//蛇身的节点类型
typedef struct SnakeNode
{//蛇的身体的每个节点的坐标int x;int y;//指向下一个节点的指针struct SnakeNode* next;
}SnakeNode,*pSnakeNode;

要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:

//创建一个结构体类型来维护蛇的各种信息
typedef struct Snake
{pSnakeNode _pSnakeHead;//维护蛇头的指针pSnakeNode _pFood;//维护食物的指针enum DIRECTION _dri;//维护蛇的方向enum GAME_STATE _state;//维护蛇的状态int _score;//维护当前游戏的总分int _foodWeight;//维护一个食物默认的分数int _sleepTime;//维护蛇的速度
}Snake,*pSnake;

 蛇的⽅向,可以⼀⼀列举,使⽤枚举:

//枚举蛇的四种方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};

游戏状态,可以⼀⼀列举,使⽤枚举:

//枚举四种游戏状态
enum GAME_STATE
{OK,//正常运行END_NORMAL,//正常退出KILL_BY_WALL,//撞到墙了KILL_BY_SELF//撞到自己了
};

 4.2 封装三个文件

1.test.h(游戏测试逻辑)

2. snake.h(所有头文件的包含和函数的声明)

3 snake.c(函数的具体实现)

4.3 test.c

这里是游戏的整体逻辑,具体实现在snake.c中

#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"//完成游戏的测试逻辑
void test()
{int ch = 0;do{//创建贪吃蛇Snake snake = { 0 };pSnake ps = &snake;//初始化游戏//0. 光标隐藏//1. 打印欢迎界面//2. 绘制地图//3. 蛇身初始化//4. 食物初始化GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏(善后工作)GameEnd(&snake);/*SetPos(62, 18);printf("您要再来一局吗?(Y/N):");*/ch = getchar();//清理缓冲区里面的内容getchar(); system("cls");} while (ch == 'Y' || ch == 'y');
}int main()
{//设置适配本地环境setlocale(LC_ALL, "");srand((unsigned int)time(NULL));test();return 0;
}

4.4 snake.h

我先把会用到的头文件,结构类型和函数声明放到这里(看完这些之后后面的代码就更容易看懂)

下面有很详细的注释,先不需要知道函数具体怎么实现,这里只需要知道它们的功能即可。

当然,还有一些函数并没在这里声明(这些函数大多都是为实现某个功能为具体的另一个函数(被这个函数调用)服务却没有在整体上都用到)

#pragma once
//包含所需要的头文件
#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>
#include<conio.h>#define WALL L'□'
#define POS_X 24
#define POS_Y 5
#define BODY L'●'
#define FOOD L'★'
//定义宏判断键盘上的按键是否被按过
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)//类型的声明//枚举蛇的四种方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};//枚举四种游戏状态
enum GAME_STATE
{OK,//正常运行END_NORMAL,//正常退出KILL_BY_WALL,//撞到墙了KILL_BY_SELF//撞到自己了
};//蛇身的节点类型
typedef struct SnakeNode
{//蛇的身体的每个节点的坐标int x;int y;//指向下一个节点的指针struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
//typedef struct SnakeNode* pSnakeNode;//创建一个结构体类型来维护蛇的各种信息
typedef struct Snake
{pSnakeNode _pSnakeHead;//维护蛇头的指针pSnakeNode _pFood;//维护食物的指针enum DIRECTION _dri;//维护蛇的方向enum GAME_STATE _state;//维护蛇的状态int _score;//维护当前游戏的总分int _foodWeight;//维护一个食物默认的分数int _sleepTime;//维护蛇的速度
}Snake,*pSnake;//函数的声明//游戏初始化
void GameStart(pSnake ps);//定位光标位置
void SetPos(int x, int y);//打印地图
void CreatMap();//初始化蛇身
void InitSnake(pSnake ps);//食物初始化
void CreateFood(pSnake ps);//游戏运行
void GameRun(pSnake ps);//游戏结束(善后工作)
void GameEnd(pSnake ps);//蛇走一步
void SnakeMove(pSnake ps);//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);//下一个位置不是食物
void NoFood(pSnake ps,pSnakeNode pn);//检查是否撞墙
void KillByWall(pSnake ps);//检查是否撞到自己
void KillBySelf(pSnake ps);//检测是否有按键被按下
void KeyFun();

 4.5 GameStart(游戏初始化)

1.SetPos(定位光标位置)

//把设置光标位置的操作封装成为一个函数
void SetPos(int x, int y)
{//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型COORD pos = { x,y };//设置光标位置SetConsoleCursorPosition(houtput, pos);
}

2.游戏初始化逻辑 

//游戏初始化
void GameStart(pSnake ps)
{//0. 先设置窗口的大小再隐藏光标system("mode con cols=150 lines=40");system("title 贪吃蛇");//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度CONSOLE_CURSOR_INFO cursorInfo = { 0 };//获得光标信息GetConsoleCursorInfo(houtput, &cursorInfo);//修改控制台的光标信息cursorInfo.dwSize = 100;//比例cursorInfo.bVisible = false;//可见度//设置光标的信息SetConsoleCursorInfo(houtput, &cursorInfo);//1. 打印欢迎界面和游戏功能介绍WelcomeToGame();//2. 绘制地图CreatMap();//3. 蛇身初始化InitSnake(ps);//4. 食物初始化CreateFood(ps);
}

 4.5.1 WelcomeToGame(打印欢迎界面)

//打印欢迎界面
void WelcomeToGame()
{//第一界面(欢迎界面)SetPos(62, 14);printf("欢迎来到贪吃蛇小游戏!");SetPos(64, 16);system("pause");system("cls");//清理屏幕//第二界面(游戏功能介绍)SetPos(50, 12);printf("1、用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速!");SetPos(50, 13);printf("2、加速将可以得到更高的分数!");SetPos(65, 15);system("pause");system("cls");//清理屏幕
}

4.5.2 CreatMap(绘制地图)

//打印地图
void CreatMap()
{int i = 0;for (int i = 0; i <= 100; i += 2){wprintf(L"%lc", WALL);}SetPos(0, 36);for (int i = 0; i <= 100; i += 2){wprintf(L"%lc", WALL);}for (int i = 1; i <= 36; i ++){SetPos(0, i);wprintf(L"%lc", WALL);}for (int i = 1; i <= 36; i++){SetPos(100, i);wprintf(L"%lc", WALL);}
}

4.5.3 InitSnake(蛇身初始化)

//初始化蛇身
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;//创建5个蛇身的节点for (int i = 1; i <= 5; i++){cur = CreatSnakeNode();cur->next = NULL;cur->x = POS_X + i * 2;cur->y = POS_Y;//将5个蛇身节点串起来if (ps->_pSnakeHead == NULL){//蛇头为空就直接插入ps->_pSnakeHead = cur;}else//头插{cur->next = ps->_pSnakeHead;ps->_pSnakeHead = cur;}}cur = ps->_pSnakeHead;while (cur){SetPos(cur->x,cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_dri = RIGHT;//方向默认向右ps->_foodWeight = 10;ps->_score = 0;ps->_sleepTime = 200;ps->_state = OK;
}

4.5.4 CreateFood(食物初始化)

//初始化食物
void CreateFood(pSnake ps)
{//先随机生成食物的坐标int x = 0;int y = 0;
again://生成的位置必须在地图内部do{x = rand() % 97 + 2;y = rand() % 35 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->_pSnakeHead;//判断食物位置是否与蛇身重叠while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);pSnakeNode food = CreatSnakeNode();food->x = x;food->y = y;food->next = NULL;//将食物节点放到ps中维护起来ps->_pFood = food;
}

4.6 GameRun(游戏运行)

1.检测按键是否被按过

//定义宏判断键盘上的按键是否被按过
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)

2.检测是否有按键被按下

//检测是否有按键被按下
void KeyFun()
{while (_kbhit()){//使用_getch()获取按下的键,不阻塞程序int key = _getch();}
}

3.游戏暂停 

//游戏暂停
void Pause()
{while(1){if (KEY_PRESS(VK_SPACE)){break;}Sleep(200);KeyFun();}
}

游戏运行逻辑 

//游戏运行
void GameRun(pSnake ps)
{PrintHelpInfo();do {SetPos(104, 12);printf("按空格键开始游戏!");SetPos(104, 13);printf("您当前的总分是%3d", ps->_score);SetPos(104, 14);printf("当前每个食物的总分是%3d", ps->_foodWeight);if (KEY_PRESS(VK_UP) && ps->_dri != DOWN){ps->_dri = UP;//上}else if (KEY_PRESS(VK_DOWN) && ps->_dri != UP){ps->_dri = DOWN;//下}	else if(KEY_PRESS(VK_LEFT) && ps->_dri != RIGHT){ps->_dri = LEFT;//左}else if(KEY_PRESS(VK_RIGHT) && ps->_dri != LEFT){ps->_dri = RIGHT;//右}else if (KEY_PRESS(VK_SPACE) ){Pause();//暂停}else if (KEY_PRESS(VK_ESCAPE)){ps->_state = END_NORMAL;//正常退出}else if (KEY_PRESS(VK_F3)){//加速if (ps->_sleepTime>80){ps->_sleepTime -= 30;ps->_foodWeight += 2;}}else if (KEY_PRESS(VK_F4)){//减速if (ps->_foodWeight>2){ps->_sleepTime += 30;ps->_foodWeight -= 2;}}//走一步SnakeMove(ps);//每走一步休息一下Sleep(ps->_sleepTime);KeyFun();} while (ps->_state == OK);
}

 4.6.1 PrintHelpInfo(右侧打印帮助手册)

//打印帮助手册
void PrintHelpInfo()
{SetPos(104, 16);printf("--------------------------------------------");SetPos(104, 17);printf("|1、不能撞墙,不能咬到自己!               |");SetPos(104, 18);printf("|2、用 ↑ . ↓ . ← . → 分别控制蛇的移动! |");SetPos(104, 19);printf("|3、F3为加速,F4为减速!                   |");SetPos(104, 20);printf("|4、按Esc退出游戏,按空格暂停游戏 !        |");SetPos(104, 21);printf("--------------------------------------------");SetPos(60, 17);
}

  4.6.2 SnakeMove(蛇走一步)

1.蛇走一步逻辑

//蛇走一步
void SnakeMove(pSnake ps)
{//创建一个节点来记录蛇头的下一个位置pSnakeNode nextHead = CreatSnakeNode();nextHead->next = NULL;switch (ps->_dri){case UP:nextHead->x = ps->_pSnakeHead->x;nextHead->y = ps->_pSnakeHead->y - 1;break;case DOWN:nextHead->x = ps->_pSnakeHead->x;nextHead->y = ps->_pSnakeHead->y +1;break;case LEFT:nextHead->y = ps->_pSnakeHead->y;nextHead->x = ps->_pSnakeHead->x - 2;break;case RIGHT:nextHead->y = ps->_pSnakeHead->y;nextHead->x = ps->_pSnakeHead->x + 2;break;default:break;}//判断下一个节点是不是食物if((ps->_pFood->x == nextHead->x) && (ps->_pFood->y == nextHead->y)){//下一个是食物那就吃掉食物EatFood(nextHead, ps);}else{//下一个位置不是食物NoFood(ps, nextHead);}//KillByWall(ps);KillBySelf(ps);
}

 2. EatFood(吃掉食物)

//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{//头插吃掉食物ps->_pFood->next = ps->_pSnakeHead;ps->_pSnakeHead = ps->_pFood;ps->_score += ps->_foodWeight;//分数增加//打印蛇身pSnakeNode cur = ps->_pSnakeHead;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//释放刚刚malloc的位置free(pn);pn = NULL;//食物被吃掉了,那就再创建一个食物CreateFood(ps);
}

3. NoFood往前走一步(不是食物)

//下一个位置不是食物
void NoFood(pSnake ps,pSnakeNode pn)
{//头插pn->next = ps->_pSnakeHead;ps->_pSnakeHead = pn;pSnakeNode cur = ps->_pSnakeHead;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);cur->next = NULL;
}

 4. KillByWall(检测是否撞墙)

//检查是否撞墙
void KillByWall(pSnake ps)
{if (ps->_pSnakeHead->x == 0|| ps->_pSnakeHead->x == 98|| ps->_pSnakeHead->y == 0|| ps->_pSnakeHead->y == 36){ps->_state = KILL_BY_WALL;return;}return;
}

5.  KillBySelf(检测是否撞到自己)

//检查是否撞到自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnakeHead->next;while (cur){if ((cur->x == ps->_pSnakeHead->x) &&(cur->y == ps->_pSnakeHead->y)){ps->_state = KILL_BY_SELF;return;}cur = cur->next;}return;
}

4.7 GameEnd(游戏善后)

//游戏善后
void GameEnd(pSnake ps)
{pSnakeNode cur = ps->_pSnakeHead;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}system("cls");SetPos(62, 16);switch (ps->_state){case END_NORMAL:printf("您主动退出游戏!\n");break;case KILL_BY_WALL:printf("您撞墙了!\n");break;case KILL_BY_SELF:printf("您撞到自己了!\n");break;}SetPos(62, 17);printf("您最终的成绩是:%d", ps->_score);Sleep(2000);KeyFun();system("cls");SetPos(62, 18);printf("您要再来一局吗?(Y/N):");
}

 4.8 snake.c(完整代码)

#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"//检测是否有按键被按下
void KeyFun()
{while (_kbhit()){//使用_getch()获取按下的键,不阻塞程序int key = _getch();}
}//把设置光标位置的操作封装成为一个函数
void SetPos(int x, int y)
{//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//修改光标位置:COORD是win32自定义的关于光标在控制台的位置的结构体类型COORD pos = { x,y };//设置光标位置SetConsoleCursorPosition(houtput, pos);
}//打印地图
void CreatMap()
{int i = 0;for (int i = 0; i <= 100; i += 2){wprintf(L"%lc", WALL);}SetPos(0, 36);for (int i = 0; i <= 100; i += 2){wprintf(L"%lc", WALL);}for (int i = 1; i <= 36; i ++){SetPos(0, i);wprintf(L"%lc", WALL);}for (int i = 1; i <= 36; i++){SetPos(100, i);wprintf(L"%lc", WALL);}
}//打印欢迎界面
void WelcomeToGame()
{//第一界面(欢迎界面)SetPos(62, 14);printf("欢迎来到贪吃蛇小游戏!");SetPos(64, 16);system("pause");system("cls");//清理屏幕//第二界面(游戏功能介绍)SetPos(50, 12);printf("1、用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速!");SetPos(50, 13);printf("2、加速将可以得到更高的分数!");SetPos(65, 15);system("pause");system("cls");//清理屏幕
}//创建1个蛇身的节点
pSnakeNode CreatSnakeNode()
{pSnakeNode ret = (pSnakeNode)malloc(sizeof(SnakeNode));if (ret == NULL){perror("CreatSnakeNode::fail\n");return NULL;}return ret;
}//初始化蛇身
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;//创建5个蛇身的节点for (int i = 1; i <= 5; i++){cur = CreatSnakeNode();cur->next = NULL;cur->x = POS_X + i * 2;cur->y = POS_Y;//将5个蛇身节点串起来if (ps->_pSnakeHead == NULL){//蛇头为空就直接插入ps->_pSnakeHead = cur;}else//头插{cur->next = ps->_pSnakeHead;ps->_pSnakeHead = cur;}}cur = ps->_pSnakeHead;while (cur){SetPos(cur->x,cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_dri = RIGHT;//方向默认向右ps->_foodWeight = 10;ps->_score = 0;ps->_sleepTime = 200;ps->_state = OK;
}//初始化食物
void CreateFood(pSnake ps)
{//先随机生成食物的坐标int x = 0;int y = 0;
again://生成的位置必须在地图内部do{x = rand() % 97 + 2;y = rand() % 35 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->_pSnakeHead;//判断食物位置是否与蛇身重叠while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//打印食物SetPos(x, y);wprintf(L"%lc", FOOD);pSnakeNode food = CreatSnakeNode();food->x = x;food->y = y;food->next = NULL;//将食物节点放到ps中维护起来ps->_pFood = food;
}//打印帮助手册
void PrintHelpInfo()
{SetPos(104, 16);printf("--------------------------------------------");SetPos(104, 17);printf("|1、不能撞墙,不能咬到自己!               |");SetPos(104, 18);printf("|2、用 ↑ . ↓ . ← . → 分别控制蛇的移动! |");SetPos(104, 19);printf("|3、F3为加速,F4为减速!                   |");SetPos(104, 20);printf("|4、按Esc退出游戏,按空格暂停游戏 !        |");SetPos(104, 21);printf("--------------------------------------------");SetPos(60, 17);
}//游戏暂停
void Pause()
{while(1){if (KEY_PRESS(VK_SPACE)){break;}Sleep(200);KeyFun();}
}//吃掉食物
void EatFood(pSnakeNode pn, pSnake ps)
{//头插吃掉食物ps->_pFood->next = ps->_pSnakeHead;ps->_pSnakeHead = ps->_pFood;ps->_score += ps->_foodWeight;//分数增加//打印蛇身pSnakeNode cur = ps->_pSnakeHead;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//释放刚刚malloc的位置free(pn);pn = NULL;//食物被吃掉了,那就再创建一个食物CreateFood(ps);
}//下一个位置不是食物
void NoFood(pSnake ps,pSnakeNode pn)
{//头插pn->next = ps->_pSnakeHead;ps->_pSnakeHead = pn;pSnakeNode cur = ps->_pSnakeHead;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);cur->next = NULL;
}//蛇走一步
void SnakeMove(pSnake ps)
{//创建一个节点来记录蛇头的下一个位置pSnakeNode nextHead = CreatSnakeNode();nextHead->next = NULL;switch (ps->_dri){case UP:nextHead->x = ps->_pSnakeHead->x;nextHead->y = ps->_pSnakeHead->y - 1;break;case DOWN:nextHead->x = ps->_pSnakeHead->x;nextHead->y = ps->_pSnakeHead->y +1;break;case LEFT:nextHead->y = ps->_pSnakeHead->y;nextHead->x = ps->_pSnakeHead->x - 2;break;case RIGHT:nextHead->y = ps->_pSnakeHead->y;nextHead->x = ps->_pSnakeHead->x + 2;break;default:break;}//判断下一个节点是不是食物if((ps->_pFood->x == nextHead->x) && (ps->_pFood->y == nextHead->y)){//下一个是食物那就吃掉食物EatFood(nextHead, ps);}else{//下一个位置不是食物NoFood(ps, nextHead);}//KillByWall(ps);KillBySelf(ps);
}//检查是否撞墙
void KillByWall(pSnake ps)
{if (ps->_pSnakeHead->x == 0|| ps->_pSnakeHead->x == 98|| ps->_pSnakeHead->y == 0|| ps->_pSnakeHead->y == 36){ps->_state = KILL_BY_WALL;return;}return;
}//检查是否撞到自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->_pSnakeHead->next;while (cur){if ((cur->x == ps->_pSnakeHead->x) &&(cur->y == ps->_pSnakeHead->y)){ps->_state = KILL_BY_SELF;return;}cur = cur->next;}return;
}//游戏初始化
void GameStart(pSnake ps)
{//0. 先设置窗口的大小再隐藏光标system("mode con cols=150 lines=40");system("title 贪吃蛇");//获得标准输出设备的句柄HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//CONSOLE_CURSOR_INFO是win32自定义的一个结构体类型,里面包含了光标的比例和可见度CONSOLE_CURSOR_INFO cursorInfo = { 0 };//获得光标信息GetConsoleCursorInfo(houtput, &cursorInfo);//修改控制台的光标信息cursorInfo.dwSize = 100;//比例cursorInfo.bVisible = false;//可见度//设置光标的信息SetConsoleCursorInfo(houtput, &cursorInfo);//1. 打印欢迎界面和游戏功能介绍WelcomeToGame();//2. 绘制地图CreatMap();//3. 蛇身初始化InitSnake(ps);//4. 食物初始化CreateFood(ps);
}//游戏运行
void GameRun(pSnake ps)
{PrintHelpInfo();do {SetPos(104, 12);printf("按空格键开始游戏!");SetPos(104, 13);printf("您当前的总分是%3d", ps->_score);SetPos(104, 14);printf("当前每个食物的总分是%3d", ps->_foodWeight);if (KEY_PRESS(VK_UP) && ps->_dri != DOWN){ps->_dri = UP;//上}else if (KEY_PRESS(VK_DOWN) && ps->_dri != UP){ps->_dri = DOWN;//下}	else if(KEY_PRESS(VK_LEFT) && ps->_dri != RIGHT){ps->_dri = LEFT;//左}else if(KEY_PRESS(VK_RIGHT) && ps->_dri != LEFT){ps->_dri = RIGHT;//右}else if (KEY_PRESS(VK_SPACE) ){Pause();//暂停}else if (KEY_PRESS(VK_ESCAPE)){ps->_state = END_NORMAL;//正常退出}else if (KEY_PRESS(VK_F3)){//加速if (ps->_sleepTime>80){ps->_sleepTime -= 30;ps->_foodWeight += 2;}}else if (KEY_PRESS(VK_F4)){//减速if (ps->_foodWeight>2){ps->_sleepTime += 30;ps->_foodWeight -= 2;}}//走一步SnakeMove(ps);//每走一步休息一下Sleep(ps->_sleepTime);KeyFun();} while (ps->_state == OK);
}//游戏善后
void GameEnd(pSnake ps)
{pSnakeNode cur = ps->_pSnakeHead;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}system("cls");SetPos(62, 16);switch (ps->_state){case END_NORMAL:printf("您主动退出游戏!\n");break;case KILL_BY_WALL:printf("您撞墙了!\n");break;case KILL_BY_SELF:printf("您撞到自己了!\n");break;}SetPos(62, 17);printf("您最终的成绩是:%d", ps->_score);Sleep(2000);KeyFun();system("cls");SetPos(62, 18);printf("您要再来一局吗?(Y/N):");
}

5、 完结散花

好了,这期的分享到这里就结束了~

如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~

如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~

我们下期不见不散~~

​​​​

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

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

相关文章

20240423给飞凌的OK3588-C开发板适配OV13855【绿屏】linux

20240423给飞凌的OK3588-C开发板适配OV13855【绿屏】 2024/4/22 20:29 开发板&#xff1a;飞凌的OK3588-C OS操作系统&#xff1a;linux R4/Buildroot 【OV13855接到CAM1上&#xff0c;如果要接到CAM2上请修改相关的DTS即可】 https://item.taobao.com/item.htm?_unju3ku2f4…

kerberos:适配华为FI

文章目录 一、hive1、hive thrift连接方式 一、hive 1、hive thrift连接方式 kerberos认证失败信息 缺少配置&#xff1a;{“hadoop.rpc.protection”:“privacy”}&#xff0c;具体可参考&#xff1a;kerbros认证相关问题 华为FI参考资料&#xff1a; https://github.com…

【MySQL 数据宝典】【磁盘结构】- 004 redolog 重做日志

一、背景介绍 持久性要求&#xff1a; 对于已提交的事务&#xff0c;即使系统发生崩溃&#xff0c;其对数据库的更改也不能丢失。问题&#xff1a; 在事务提交前将所有修改的页面刷新到磁盘浪费资源。随机IO导致刷新速度慢。 解决方案&#xff1a; 【数据副本】记录事务执行过…

linux信号机制分析

概念 信号递达&#xff1a;实际执行信号的处理动作就是信号递达 信号未决&#xff1a;信号从产生到递达之间的状态就是信号未决&#xff08;未决就是没有解决&#xff09; 收到某信号后&#xff0c;把未决信号集中的此信号置为1&#xff08;1表示未解决的信号&#xff09;&a…

【Camera Sensor Driver笔记】四、点亮指南之EEPROM配置

很久之前写的一版&#xff1a; 【Qcom Camera】微距eeprom调试_cam_vio-supply <&l7p>-CSDN博客 <slaveInfo> EEPROMName cat24c64_imx585 eeprom型_sensor名字 slaveAddress 0xa0 i2c write address regAddrType …

国产PLC有哪些,哪个牌子比较好用?

你知道国产PLC有哪些吗,哪个牌子更好用吗&#xff1f; 今天拿出国产先锋的汇川与台达对比&#xff0c;注&#xff1a;视频后方有各品牌学习资料免费送&#xff0c;需要的移步自取。话说回来&#xff0c;只要基于Codesys开发的都比较好用&#xff0c;只是使用底层芯片不同&…

国产软件不背黑锅:4款功能强大的黑科技软件,且用且珍惜

国内软件常被冠以“流氓软件、需要额外付费、广告繁多”等负面标签&#xff0c;但实际上&#xff0c;其中不乏一些小众却功能强大、用户体验极佳的软件。 布丁扫描——免费专业的扫描APP&#xff08;安卓、ios&#xff09; 布丁扫描&#xff0c;无疑是我今年的最爱&#xff0…

服务器还在长期泄密,保护数据IPSSL证书必不可少

IP SSL&#xff0c;或称为安全套接层协议&#xff08;Secure Sockets Layer&#xff09;&#xff0c;是一种用于在互联网上进行通信加密的技术标准&#xff0c;它通过为数据提供加密服务&#xff0c;确保了数据在传输过程中的安全与完整。其工作方式是在客户端和服务器之间建立…

你们项目日志是如何处理的???

ELK日志采集系统 1.什么是ELK ELK 是一套流行的数据搜索、分析和可视化解决方案&#xff0c;由三个开源项目组成&#xff0c;每个项目的首字母合起来形成了“ELK”这一术语&#xff1a; Elasticsearch (ES): Elasticsearch 是一个基于 Apache Lucene 构建的分布式、实时搜索与…

Java 字符

Java 字符 Java教程 - Java字符 在Java中&#xff0c;char存储字符。Java使用Unicode来表示字符。Unicode可以表示在所有人类语言中找到的所有字符。 Java char是16位类型。 字符的范围是 0 到 65,536 。没有负字符。 Char文字 Java中的字符是Unicode字符集的索引。字符表…

OpenHarmony实战开发-减小应用包大小。

简介 减小应用包大小是提升应用下载、安装体验的重要方式之一。通过压缩、精简或者复用应用中的代码或资源&#xff0c;可以有效降低应用的大小&#xff0c;提升应用下载和安装速度&#xff0c;减少系统空间占用。 开发者可以参考下面三种方法减小应用包大小&#xff1a; 配…

介绍TCP三次握手、传输数据、四次挥手标志为确认号变化规律

TCP协议的三次握手是一个关键过程&#xff0c;用于在客户端和服务器之间建立可靠的连接。以下是三次握手的详细过程&#xff0c;包括标志位、序列号以及ACK的变化规律&#xff1a; 第一次握手&#xff1a; 客户端&#xff1a; 标志位&#xff1a;SYN1&#xff08;表示请求建立…

【行为型模型】迭代器模式

一、迭代器模式概述 迭代器模式定义&#xff1a;提供一种方法顺序访问一个聚合对象中的各个元素&#xff0c;而又不暴露其内部的表示。把游走的任务放在送代器上&#xff0c;而不是聚合上。这样简化了聚含的接口和实现,也让责任各得其所。(对象行为型) 迭代器模式的优缺点&…

数据结构之顺序表的实现(C语言版)

Hello, 大家好&#xff0c;我是一代&#xff0c;今天给大家带来有关顺序表的有关知识 所属专栏&#xff1a;数据结构 创作不易&#xff0c;望得到各位佬们的互三呦 一.前言 1.首先在讲顺序表之前我们先来了解什么是数据结构 数据结构是由“数据”和“结构”两词组合⽽来。 什…

C语言—深度剖析函数指针,函数指针数组

我们先来看一段代码 #include <stdio.h> void test() {printf("hehe\n"); } int main() {printf("%p\n", test);printf("%p\n", &test);return 0; }输出的是两个地址&#xff0c;这两个地址是 test 函数的地址。 那我们的函数的地址…

Electron 30.0.0 发布,升级 Node 和 V8 引擎

近日&#xff0c;Electron 30.0.0 正式发布&#xff01;你可以通过 npm install electronlatest 进行安装&#xff0c;或者从 Electron 的发布网站下载&#xff0c;继续阅读了解此版本的详细信息。 &#x1f525; 主要更新 Windows 上支持 ASAR 完整性融合。如果未正确配置&am…

软件测试——Postman Script脚本功能

Postman作为软件测试里一款非常流行的调试工具&#xff0c;给我们提供了一个执行JavaScript脚本的环境&#xff0c;所以我们可以使用js语言编写脚本来解决一些接口自动化的问题&#xff0c;比如接口依赖、接口断言等等。Postman有Pre-RequestScript和Tests两个编写js脚本的模块…

Jenkins 哲学 - 插件初始化安装失败

到Jenkins官网查找最新的LST版本 最后的版本号一定要带&#xff0c;指定下载具体的版本号 docker pull jenkins/jenkins:2.426.1 自定义挂载目录&#xff0c;修改权限 mkdir /jenkins/jenkins_homechmod 777 /data/jenkins

Ansible安装基本原理及操作(初识)

作者主页&#xff1a;点击&#xff01; Ansible专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月23日15点18分 Ansible 是一款功能强大且易于使用的IT自动化工具&#xff0c;可用于配置管理、应用程序部署和云端管理。它使用无代理模式&#xff08;agentles…

谈谈mysql中的各个关键字

1.为什么学习mysql mysql是当今最主流且开放源码的关系型数据库&#xff0c;开发者为瑞典 MySQL AB 公司。目前 MySQL 被广泛地应用在 Internet 上的中小型网站中。由于其体积小、速度快、总体拥有成本低&#xff0c;尤其是开放源码这一特点&#xff0c;许多中小型网站为了降低…