行业知识
【C语言】手把手教你用C语言实现贪吃蛇小游戏(2W字详解)
目录
前言
游戏效果演示
游戏功能
技术要点
Windows 32 API
控制台程序
控制台屏幕上的坐标COORD
GetStdHandle
GetConsoleCursorInfo
CONSOLE_CURSOR_INFO
SetConsoleCursorInfo
SetConsoleCursorPosition
GetAsyncKeyState
贪吃蛇游戏设计与分析
宽字符的了解
前引
本地化
类项
setlocale函数
宽字符打印
地图坐标
蛇身和食物及数据结构设计
游戏流程设计
游戏主逻辑设计
游戏开始
打印欢迎界面及功能键介绍
绘制地图
初始化贪吃蛇
创建食物
游戏运行
打印得分情况
获取按键状态
蛇每走一步的过程
游戏结束
Snake.h
Snake.c
Test.c
前言
贪吃蛇游戏想必大部分人在童年的时候都略有玩过,今天这篇博客将教你用C语言手把手实现这款同年经典的小游戏
废话不多说,先看一下实现后的效果
游戏效果演示
贪吃蛇视频
游戏功能
- 贪吃蛇地图绘制
- 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
- 蛇撞墙***亡
- 蛇撞自身***亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
技术要点
在实现游戏代码之前,还需要学习一下 Windows 32 API 的知识,以便我们更好理解等下写的代码
Windows 32 API
我们说Windows 是一个强大的操作系统,它除了协调各个应用程序的执行、分配内存、管理资源之外,它还有一个很大的服务中心:Windows 32 API ,这里面有很多种服务,每一个种服务本质就是一个函数,这些函数服务于应用,帮助应用实现开启视窗、描绘图形、使用周边设备等目的,所以又称为 Application Programming Interface ,即 API函数,Windows 32 API 也就是Microsoft Windows 32位平台的应用程序编程接口
控制台程序
平常我们写代码运行起来的黑框程序就是控制台程序
我们可以使用Windows + R 键打开运行,输入cmd,来到控制台,利用cmd命令来设置控制台窗口的长宽
mode con cols=x lines=y
x是要设置的列大小,y是要设置的行大小
演示:
cmd命令设置控制台行列大小
利用cmd命令还可以对控制台名字进行修改
title 修改后的名字
演示:
平常写代码运行起来的黑方框没有对其进行设置是做不到的,不过稍微对其进行设置就行了
如果要让我们的代码运行起来的控制台可以设置,我们肯定不能直接输入cmd命令,那要怎么办呢?
我们可以使用 system 函数,这个函数是用来执行系统命令的,这样我们可以利用这个函数来设置我们控制台的属性,该函数需要包含头文件
可以发现,运行起来后名字和大小都没有改
这是因为程序已经结束,程序一旦结束就会恢复到了原本的状态
所以我们要让system 函数执行完,让程序停下来,这时使用getchar(),或system("pause")都可以
getchar()需要再输入一个字符程序继续,而 system("pause") 可以程序停下来,直到输入一个任意键程序继续
控制台屏幕上的坐标COORD
COORD 是Windows 32 API 中定义的一个结构体,用来表示一个字符在屏幕缓冲区中的坐标位置\
类型声明
typedef struct _COORD { SHORT X; SHORT Y;} COORD, *PCOORD;控制台本质可以看成是一个直角坐标系,不过X轴和Y轴的方向和位置与我们数学上的不一样,并且只要是在控制台上的字符 ,都可用一个坐标表示出其位置
给坐标赋值
COORD pos = { 4,44 };GetStdHandle
GetStdHandle 函数是用来获得指定标准设备{(标准输入设备 - 键盘)(标准输出设备 - 屏幕)(标准错误设备- 屏幕)}的句柄
句柄就是个数字,一般和当前系统下的整数的位数一样,比如32bit系统下就是4个字节,用来标识和操作 系统中的资源,如文件 ,
窗口,画笔,光标等,就跟把柄一个意思,掌握了你的把柄,你就难逃法网(系统),(系统)要找到你就可以根据句柄来找你
HANDLE GetStdHandle(_In_ DWORD nStdHandle);
参数
nStdHandle的取值有以下几种
返回值
返回一个 HANDLE 类型的值,即返回指定标准设备的句柄
使用:
HANDLE houtput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
GetConsoleCursorInfo 函数是用来检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
参数
hConsoleOutput 的类型为 HANDLE ,是我们要检索的控制台,即要检索的控制台相关联的句柄
lpConsoleCursorInfo 的类型为 PCONSOLE_CURSOR_INFO ,是一个指向CONSOLE_CURSOR_INFO结构体的指针
使用:
HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//获取控制台光标信息
CONSOLE_CURSOR_INFO
CONSOLE_CURSOR_INFO 是Windows 32 API 中定义的一个结构体,用来表示指定控制台屏幕缓冲区的光标大小和可见性的信息
类型声明:
typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible;} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;参数
dwSize 是游标填充的字符单元的百分比, 该值介于 1 到 100 之间
bVisible 是游标的可见性,两个取值,如果游标可见,则此成员为 true,不可见,则成员为 false
#include //包含头文件CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo
SetConsoleCursorInfo 函数是用来设置指定的控制台屏幕缓冲区光标的大小和可见性
BOOL WINAPI SetConsoleCursorInfo( _In_ HANDLE hConsoleOutput, _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo);
参数
hConsoleOutput 的类型为 HANDLE , 是我们要检索的控制台,即要检索的控制台相关联的句柄
lpConsoleCursorInfo 的类型为 PCONSOLE_CURSOR_INFO ,是一个指向CONSOLE_CURSOR_INFO结构体的指针
使用:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//获得指定标准设备的句柄//隐藏光标操作CONSOLE_CURSOR_INFO CursorInfo;//定义一个CONSOLE_CURSOR_INFO类型结构体,存储光标信息GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
SetConsoleCursorPosition
SetConsoleCursorPosition 函数是用来设置指定控制台屏幕缓冲区上光标的坐标位置
BOOL WINAPI SetConsoleCursorPosition( _In_ HANDLE hConsoleOutput, _In_ COORD dwCursorPosition);
参数
hConsoleOutput 的类型为 HANDLE ,是我们要检索的控制台,即要检索的控制台相关联的句柄
dwCursorPosition 的类型为COORD,是一个结构体,存储了一个字符在屏幕中的坐标位置
要设置光标或字符位置,在调用这个函数之前,得先创建一个COORD变量,存储修改后的光标坐标位置,之后在调用这个函数
使用:
COORD pos = { 10, 5};//定义一个COORD结构体变量HANDLE houtput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(houtput, pos);如果一个项目或程序中需要频繁修改光标位置,我们可以把这些步骤封装成一个函数SetPos(short x ,short y) ,以后如果需要改光标位置,直接调用这个函数就行, 参数就是修改后的光标坐标
void SetPos(short x, short y)//设置光标的坐标{ COORD pos = { x, y }; HANDLE houtput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) houtput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(houtput, pos);}GetAsyncKeyState
GetAsyncKeyState 函数是用来查看键盘上按键的状态
SHORT GetAsyncKeyState([in] int vKey);
在Windows 系统中,键盘上的每一个键都会有一个与之对应的虚拟键码,大家可以看看这些键盘的虚拟键码虚拟键码 https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes 调用该函数,传入要查看的键对应的虚拟键码,这个函数就会返回一个short 类型的值,返回值会在原本虚拟键码的基础上把其二进制的最高位值进行修改,按键状态按下时,最高位修改为 1,按键状态是抬时,最高位修改为 0
定义一个宏来获取按键状态,返回值按位与上一个 1,如果(GetAsyncKeyState(VK) & 0x1)表达式值为1,则最后的整个表达式就是1,表示按键按下,反之就是没有按下
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
使用:
#include #include #include #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )int main(){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");}}}到这为止,已经贪吃蛇游戏里面要用到的Windows 32 API 知识进行简单了解了,接下来就是要实现贪吃蛇的游戏代码
正片开始
贪吃蛇游戏设计与分析
宽字符的了解
相信大家在学习完前面的技术要点后,应该对贪吃蛇的欢迎界面和地图打印的代码实现有一些想法了,应该就是把光标位置设定到指定位置后,在进行打印
前引
还有一个问题:这些特殊字符是怎么打印出来的,我们如果使用 printf 函数直接进行打印发现是打印不出来的,所以在这里还需要学习一个小小的知识点
C语言最开始是由美国人发明的,美国人的母语是英语,所以在C语言最开始使用时只有这些26个英文字母即及一些字符,也就是ASCLL码,总共128(0 ~ 127)个字符,要全部标识这些字符,只需要一个字节大小就够用了(8个比特位,2 * 8 == 256),但是一些国家的母语不是英语,其国家的语言可能有上百上千,甚至上万个字符,如果让这些国家的程序员使用C语言,用起来极其不方便,之后,C语言为了适应国际化需求,不断在C标准中加入国际化的支持,如创建了宽字符的类型 wchar_t 以及专门打印宽字符的函数 wprintf ,除此之外,还编写了一个头文件
本地化
C语言为支持国际化,头文件应运而生,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数
提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分
在标准中,依赖地区的部分有以下几项:- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进行修改,下面的一个宏, 指定一个类项:- LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。
- LC_CTYPE:影响字符处理函数的行为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响 printf() 的数字格式。
- LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
- LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境
setlocale函数
setlocale 函数用来用于修改当前地区,可以针对一个类项修改,也可以针对所有类项
char *setlocale(int category, const char *locale);
参数
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项 C标准给第二个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式) 任意程序执行开始,都会隐藏式执行调用正常模式setlocale(LC_ALL, "C");
如果要修改为本地模式,则需调用
setlocale(LC_ALL, "");
除此之外,这个函数还会返回一个常量字符串的首地址,字符串内容为当前模式,以后有需要就可以通过这个函数查看当前模式
第二个参数也可以传空指针,也是用来查询当前模式的
宽字符打印
窄字符(普通字符):简单理解的话就是ASCLL码,占1个字节大小
宽字符:简单理解就是除ASCLL码之外的字符,包括汉字,占2个字节大小
现在如何来打印这些特殊字符,就是要用到以上提到的知识,先将模式设置为本地模式,在利用宽字符的打印函数进行打印就行
宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引 号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应 wprintf() 的占位符为 %ls地图坐标
补充一知识点,控制台的坐标行大小是列的两倍 :列 * 2 == 行
贪吃蛇地图大小我们设置成为58 * 27,在围着地图画墙,下来大家如果写的话,可以自行设置成想要的大小
构思完地图后,接下来就是要考虑贪吃蛇以及食物的怎么设计了
蛇身和食物及数据结构设计
从最开始视频里面的游戏效果也知,蛇身是由一节一节的同等大小圆点组成的,进去游戏后会有一条贪吃蛇以及一个随机生成的食物,每吃下一个食物后,蛇身就会多出一节同等大小的圆点,之后再随机生成一个食物,如果我们用链表来表示贪吃蛇,那么就可以把蛇看成是一条链表,一节一节的节点就组成了我们的贪吃蛇,节点里面就存储该节点的位置坐标以及下一个节点的地址,这样,蛇的数据结构想完了,同理,食物我们也可以看成是一个节点,特殊之处就是独立于贪吃蛇之外,随机出现在地图内,节点里面也是存储位置坐标
接下来就开始写贪吃蛇的实现代码
贪吃蛇的实现我们采用多文件模式
Snake.h
- 类型及函数的声明
- 宏的定义
- 所需头文件
Snake.c
- 函数的实现
Test.c
- 游戏主要逻辑
- 测试游戏代码
//蛇和食物可以看成是一条链表//定义节点类型typedef struct SnakeNode{ int x;//X轴坐标位置int y;//Y轴坐标位置struct SnakeNode* next;//下一个节点地址}SnakeNode, * pSnakeNode;//pSnakeNode 是重定义后的结构体指针类型,相当于以下写法//typedef struct SnakeNode* pSnakeNode;当然,贪吃蛇的还有很多属性要进行描述,比如蛇的位置,状态,速度,方向,以及当前的得分情况,每吃一个食物的得分,这些属性可能随着贪吃蛇的移动不断变化,如果要一一设置变量来描述这些属性,到后面写代码的时候就会非常乱,用起来麻烦,这样,我们对他们就行打包吧,因为都是用来描述贪吃蛇的属性,我们定义一个结构体,把他们封装成一个对象,这个对象的名称就叫做贪吃蛇
//贪吃蛇//维护贪吃蛇就是在维护这些结构体成员typedef struct Snake{pSnakeNode _psnake;//指向贪吃蛇蛇头的指针pSnakeNode _pfood; //指向食物位置的指针enum direction _direction;//枚举蛇的方向enum state _state;//枚举蛇的状态int _food_weight;//事物的权重int _score; //总成绩int _sleep_time; //休眠时间}Snake,*psanke;因为蛇的方向和状态是可以一一进行列举的,所有就用枚举来定义其数据类型
方向
//枚举蛇的方向enum DIRECTION{UP = 1,//上DOWN, //下LEFT, //左RIGHT //右};状态
//枚举蛇的状态enum STATE{OK, //正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//咬到自己END_NORMAL //正常退出};游戏流程设计
游戏主逻辑设计
将C环境设置为本地模式,进入游戏主逻辑
主逻辑包括 3 部分:
- 游戏开始(GameStart):完成初始化游戏
- 游戏运行(GameRun) :完成游戏运行逻辑实现
- 游戏结束(GameEnd) :完成游戏善后工作
代码实现:
#include //主要的游戏逻辑void test(){char ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//游戏开始Game_Start(&snake);//运行游戏Game_Run(&snake);//结束游戏(善后工作 -- 释放动态开辟的内存空间)Game_Over(&snake);SetPos(25, 17);printf("要再来一局吗(Y/N):");ch = getchar();getchar();} while ((ch == 'Y') || (ch == 'y'));SetPos(0, 28);}int main(){//环境本地化设置setlocale(LC_ALL, "");//初始化随机数种子srand((unsigned)time(NULL));test();return 0;}游戏开始
实现的功能:
- 打印欢迎界面及功能键介绍
- 绘制地图
- 初始化贪吃蛇
- 创建食物
打印欢迎界面及功能键介绍
代码实现:
//修改光标坐标位置void SetPos(short x, short y){ //定义一个COORD变量,存储光标坐标信息COORD pos = { x, y }; //获取标准输出的句柄(用来标识不同设备的数值)HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为posSetConsoleCursorPosition(houtput, pos);}//打印欢迎界面void Welcome_Game(){SetPos(40, 12);wprintf(L"欢迎来到贪吃蛇游戏\n");SetPos(41, 20);system("pause");//暂停system("cls");//清空屏幕SetPos(25, 12);printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(40, 13);printf("加速将能得到更高的分数。\n");SetPos(41, 20);system("pause");//暂停system("cls");//清空屏幕}绘制地图
代码实现:
//绘制地图void Create_Map(){//上for (int i = 0; i初始化贪吃蛇
代码实现:
void InitSnake(psanke ps){//头插五个节点pSnakeNode cur = NULL;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;}}//初始化蛇的其他属性ps->_direction = UP;//蛇的方向ps->_state = OK; //蛇的状态ps->_food_weight = 10;//食物的权重ps->_score = 0; //总得分ps->_sleep_time = 200;//休眠时间//打印起始蛇身while (cur){ //定位到蛇身节点对应的坐标位置进行打印SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}}创建食物
因为宽字符的存在,食物的位置不能是随机的,在X轴上的坐标必须是2的倍数
食物的坐标还必须满足以下两个条件
- 必须在创建的墙体以内
- 不能与贪吃蛇蛇身重叠
代码实现:
//创建食物void Create_Food(psanke ps){int x = 0;int y = 0;//生成食物的随机坐标again://x: 2 ~ 54//y: 1 ~ 25do {x = rand() % 53 + 2;//生成的不一定是x的2倍y = rand() % 25 + 1;} while (x % 2);//避免与贪吃蛇重叠pSnakeNode cur = ps->_psnake;while (cur){if (cur->x == x && cur->y == y){goto again;//跳回again位置}cur = cur->next;}//创建食物pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pfood == NULL){perror("Create_Food()::malloc()");exit(1);}else{pfood->x = x;pfood->y = y;pfood->next = NULL; //光标定位到食物坐标位置SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pfood = pfood;}//这样的写法会报错,因为刚开始的时候只是创建贪吃蛇变量,并初始化结构体为0,所以ps->pfood指向的是NULL,后面就会对NULL指针解引用,报错/*SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pfood->x = x;ps->_pfood->y = y;ps->_pfood->next = NULL;*/}游戏运行
实现的功能:
- 打印得分情况
- 获取按键状态
- 蛇每走一步的过程
再次观察开头的视频,游戏运行后,贪吃蛇就可以开始到处移动了,那这里是怎么实现贪吃蛇的打印?
仔细观察贪吃蛇的移动就会发现,蛇的移动是每走一步打印一下,休息一下,后继续打印,继续休息如此循环往复,只是说休息的时间极短,这样使我们在视觉上看起来好似移动,所以我们要每走一步,就检查一下贪吃蛇的状态,状态如果是OK的话就继续走下一步
打印得分情况
因为得分和食物权重是根据蛇的移动行为可能随时发生变化,所以在蛇每走一步前就要检查一下打印一下这些信息
获取按键状态
贪吃蛇每走一步之前我们都需要获取按键的状态,之后根据按键的状态,贪吃蛇才能做出相应的反应,比如按的是方向键,就可以确定蛇的下一个坐标位置,按的如果是加速减速键,蛇就在原本的方向基础上,进行加速或减速
代码实现:
do{//因为食物权重和得分会随着蛇的每一步变化,所以没走一步重新打印得分情况SetPos(64, 10);printf("总得分:%d", ps->_score);SetPos(64, 11);printf("食物权重:%2d", ps->_food_weight);//每走一步检查游戏状态(即键盘虚拟按键的返回值)if (KEY_PRESS(VK_UP) && ps->_direction != DOWN)//按向上键,蛇头的方向自然不能向下{ps->_direction = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP){ps->_direction = DOWN;}else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT){ps->_direction = RIGHT;}else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT){ps->_direction = LEFT;}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->_sleep_time > 80){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->_state == OK);//状态OK的话继续循环}暂停代码:
//暂停void Pause(){while (1)//一直***循环的休眠下去,直到空格键被再一次按下,退出循环{if (KEY_PRESS(VK_SPACE)){break;}}}蛇每走一步的过程
蛇每走一步的过程:确定蛇的方向后,创建出一个节点,这个节点表示蛇下一步移向的位置,如果下一个位置刚好是食物,则把食物头接到蛇头,重新创建食物,如果不是食物,则将用来表示下一个位置的节点头接到蛇头,当然为了保持蛇长度不变,还要再释放掉尾节点
代码实现:
//蛇走一步的过程void SnakeMove(psanke ps){//创建一个节点,用来表示蛇下一步移向的位置pSnakeNode Next = (pSnakeNode)malloc(sizeof(SnakeNode));if (Next == NULL){perror("SnakeMove()::malloc()");exit(1);}switch (ps->_direction)//根据方向确定蛇下一步移向的下位置坐标{case UP:Next->x = ps->_psnake->x;Next->y = ps->_psnake->y - 1;break;case DOWN:Next->x = ps->_psnake->x;Next->y = ps->_psnake->y + 1;break;case LEFT:Next->x = ps->_psnake->x - 2;Next->y = ps->_psnake->y;break;case RIGHT:Next->x = ps->_psnake->x + 2;Next->y = ps->_psnake->y;break;}//检测移向的下一个位置是否为食物if (Cheak_Next_Is_Food(Next, ps))//是食物Is_Food(Next, ps);elseNot_Food(Next, ps);//不是食物//检测是否咬到自己if (KillBySelf(ps)){SetPos(25, 15);printf("您咬到自己,游戏结束");}//检测是否撞墙if (KillByWall(ps)){SetPos(25, 15);printf("您已经撞到墙,游戏结束");}}int Cheak_Next_Is_Food(pSnakeNode Next, psanke ps)
比较食物的位置坐标是否与下一个位置的坐标一样
代码实现:
//检测下一个位置是否为食物int Cheak_Next_Is_Food(pSnakeNode pn, psanke ps){return ((ps->_pfood->x == pn->x) && (ps->_pfood->y == pn->y));}void Is_Food(pSnakeNode pn, psanke ps)
代码实现:
//是食物void Is_Food(pSnakeNode pn, psanke ps){//将食物头接到蛇头上ps->_pfood->next = ps->_psnake;ps->_psnake = ps->_pfood;pSnakeNode cur = ps->_psnake;//遍历链表打印while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_score += ps->_food_weight;//修改得分//释放下一个节点free(pn);pn = NULL;//重新创建食物Create_Food(ps);}void Not_Food(pSnakeNode pn, psanke ps)
代码实现:
//不是食物void Not_Food(pSnakeNode pn, psanke ps){//1.将食物头接到蛇头上pn->next = ps->_psnake;ps->_psnake = pn;//2.从蛇头开始打印pSnakeNode cur = ps->_psnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//3.在尾节点相连的位置打印空白字符,覆盖掉之前在该位置打印的图形SetPos(cur->next->x, cur->next->y);printf(" ");//4.释放尾节点,并将前一个结点的next = NULLfree(cur->next);cur->next = NULL;}int KillBySelf(psanke ps)
代码实现:
//检测是否咬到自己int KillBySelf(psanke ps){pSnakeNode cur = ps->_psnake->next;if (cur == NULL){perror("KillBySelf()::malloc()");exit(1);}//遍历自己,看是否有没有与自身节点位置重叠while (cur){if ((cur->x == ps->_psnake->x) && (ps->_psnake->y == cur->y)){ps->_state = KILL_BY_SELF;return 1;}cur = cur->next;}return 0;}int KillByWall(psanke ps)
代码实现:
//检测是否撞墙int KillByWall(psanke ps){if ( (ps->_psnake->x == 0)|| (ps->_psnake->x == 56)|| (ps->_psnake->y == 0)|| (ps->_psnake->y == 26)){ps->_state = KILL_BY_WALL;return 1;}return 0;}游戏结束
链表的节点是动态开辟的,所以要对动态开辟的内存空间进行释放
代码实现:
//结束游戏(善后工作 -- 释放动态开辟的内存空间)void Game_Over(psanke ps){pSnakeNode cur = ps->_psnake;pSnakeNode del = NULL;while (cur){del = cur->next;free(cur);cur = del;}}OK,今天的贪吃蛇讲到这里就结束了,当然游戏还能再进行优化,比如用户历史成绩排名,贪吃蛇能不能穿墙,拓展食物种类等,大家有兴趣的话可以再此基础上继续完善游戏功能
下面是游戏代码:
Snake.h
#include #include #include #include #include #define pos_x 24#define pos_y 5#define WALL L'□'#define BODY L'●'#define FOOD L'★'//蛇可以看成是一条链表,贪吃蛇的蛇身(链表节点)//定义节点类型typedef struct SnakeNode{ int x;int y;struct SnakeNode* next;}SnakeNode, * pSnakeNode;//typedef struct SnakeNode* pSnakeNode;//枚举蛇的方向enum DIRECTION{UP = 1,DOWN,LEFT,RIGHT};//枚举蛇的状态enum STATE{OK,KILL_BY_WALL,KILL_BY_SELF,END_NORMAL};//贪吃蛇//维护贪吃蛇就是在维护这些结构体成员typedef struct Snake{pSnakeNode _psnake;//指向贪吃蛇蛇头的指针pSnakeNode _pfood; //指向食物位置的指针enum direction _direction;//枚举蛇的方向enum state _state;//枚举蛇的状态int _food_weight;int _score;int _sleep_time;}Snake,*psanke;//初始化游戏void Game_Start(psanke snake);//绘制地图void Create_Map();//初始化贪吃蛇void InitSnake(psanke ps);//创建食物void Create_Food(psanke ps);//运行游戏void Game_Run(psanke ps);//蛇走一步的过程void SnakeMove(psanke ps);//检测下一个位置是否为食物int Cheak_Next_Is_Food(pSnakeNode pn, psanke ps);//是食物void Is_Food(pSnakeNode pn, psanke ps);//不是食物void Not_Food(pSnakeNode pn, psanke ps);//检测是否咬到自己int KillBySelf(psanke ps);//检测是否撞墙int KillByWall(psanke ps);//结束游戏(善后工作 -- 释放动态开辟的内存空间)void Game_Over(psanke ps);Snake.c
#include "snake.h"//修改字符打印坐标void SetPos(short x, short y){COORD pos = { x, y };HANDLE houtput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(houtput, pos);}//打印欢迎界面void Welcome_Game(){SetPos(40, 12);wprintf(L"欢迎来到贪吃蛇游戏\n");SetPos(41, 20);system("pause");system("cls");SetPos(25, 12);printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");SetPos(40, 13);printf("加速将能得到更高的分数。\n");SetPos(41, 20);system("pause");system("cls");}//绘制地图void Create_Map(){//上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;}}//初始化蛇的其他属性ps->_direction = UP;ps->_state = OK;ps->_food_weight = 10;ps->_score = 0;ps->_sleep_time = 200;//打印默认蛇身while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}}//创建食物void Create_Food(psanke ps){int x = 0;int y = 0;//生成食物的随机坐标again://x: 2 ~ 54//y: 1 ~ 25do {x = rand() % 53 + 2;//生成的不一定是x的2倍y = rand() % 25 + 1;} while (x % 2);//避免与贪吃蛇重叠pSnakeNode cur = ps->_psnake;while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//创建食物pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pfood == NULL){perror("Create_Food()::malloc()");exit(1);}else{pfood->x = x;pfood->y = y;pfood->next = NULL;SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pfood = pfood;}//这样的写法会报错,因为刚开始的时候只是创建贪吃蛇变量,并初始化结构体为0,所以ps->pfood指向的是NULL,后面就会对NULL指针解引用,报错/*SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pfood->x = x;ps->_pfood->y = y;ps->_pfood->next = NULL;*/}//初始化游戏void Game_Start(psanke ps){//1.设置控制台属性system("mode con cols=100 lines=35");system("title 贪吃蛇");HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//隐藏光标CursorInfo.bVisible = false;SetConsoleCursorInfo(houtput, &CursorInfo);//2.打印欢迎界面Welcome_Game();//3.绘制地图Create_Map();//4.初始化贪吃蛇InitSnake(ps);//5.创建食物Create_Food(ps);}//打印帮助信息void PrintHelpInfo(){SetPos(64, 15);wprintf(L"不能穿墙,不能咬到自己");SetPos(64, 16);wprintf(L"用↑.↓.←.→分别控制蛇的移动");SetPos(64, 17);wprintf(L"F3 为加速,F4 为减速");SetPos(64, 18);wprintf(L"ESC :退出游戏.space:暂停游戏");}#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//暂停void Pause(){while (1){if (KEY_PRESS(VK_SPACE)){break;}}}//是食物void Is_Food(pSnakeNode pn, psanke ps){//将食物头接到蛇头上ps->_pfood->next = ps->_psnake;ps->_psnake = ps->_pfood;pSnakeNode cur = ps->_psnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->_score += ps->_food_weight;//释放下一个节点free(pn);pn = NULL;//重新创建食物Create_Food(ps);}//不是食物void Not_Food(pSnakeNode pn, psanke ps){//1.将食物头接到蛇头上pn->next = ps->_psnake;ps->_psnake = pn;//2.从蛇头开始打印pSnakeNode cur = ps->_psnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//3.在尾节点相连的位置打印空白字符,覆盖掉之前在该位置打印的图形SetPos(cur->next->x, cur->next->y);printf(" ");//4.释放尾节点,并将前一个结点的next = NULLfree(cur->next);cur->next = NULL;}//检测下一个位置是否为食物int Cheak_Next_Is_Food(pSnakeNode pn, psanke ps){return ((ps->_pfood->x == pn->x) && (ps->_pfood->y == pn->y));}//检测是否咬到自己int KillBySelf(psanke ps){pSnakeNode cur = ps->_psnake->next;if (cur == NULL){perror("KillBySelf()::malloc()");exit(1);}//遍历自己,看是否有没有与自身节点位置重叠while (cur){if ((cur->x == ps->_psnake->x) && (ps->_psnake->y == cur->y)){ps->_state = KILL_BY_SELF;return 1;}cur = cur->next;}return 0;}//检测是否撞墙int KillByWall(psanke ps){if ( (ps->_psnake->x == 0)|| (ps->_psnake->x == 56)|| (ps->_psnake->y == 0)|| (ps->_psnake->y == 26)){ps->_state = KILL_BY_WALL;return 1;}return 0;}//蛇走一步的过程void SnakeMove(psanke ps){//创建一个节点,用来表示蛇下一步移向的位置pSnakeNode Next = (pSnakeNode)malloc(sizeof(SnakeNode));if (Next == NULL){perror("SnakeMove()::malloc()");exit(1);}switch (ps->_direction){case UP:Next->x = ps->_psnake->x;Next->y = ps->_psnake->y - 1;break;case DOWN:Next->x = ps->_psnake->x;Next->y = ps->_psnake->y + 1;break;case LEFT:Next->x = ps->_psnake->x - 2;Next->y = ps->_psnake->y;break;case RIGHT:Next->x = ps->_psnake->x + 2;Next->y = ps->_psnake->y;break;}//检测下一个位置是否为食物if (Cheak_Next_Is_Food(Next, ps))Is_Food(Next, ps);elseNot_Food(Next, ps);//检测是否咬到自己if (KillBySelf(ps)){SetPos(25, 15);printf("您咬到自己,游戏结束");}//检测是否撞墙if (KillByWall(ps)){SetPos(25, 15);printf("您已经撞到墙,游戏结束");}}//运行游戏void Game_Run(psanke ps){//打印帮助信息PrintHelpInfo();do{//因为食物权重和得分会随着蛇的每一步变化,所以没走一步重新打印得分情况SetPos(64, 10);printf("总得分:%d", ps->_score);SetPos(64, 11);printf("食物权重:%2d", ps->_food_weight);//每走一步检查游戏状态(即键盘虚拟按键的返回值)if (KEY_PRESS(VK_UP) && ps->_direction != DOWN){ps->_direction = UP;}else if (KEY_PRESS(VK_DOWN) && ps->_direction != UP){ps->_direction = DOWN;}else if (KEY_PRESS(VK_RIGHT) && ps->_direction != LEFT){ps->_direction = RIGHT;}else if (KEY_PRESS(VK_LEFT) && ps->_direction != RIGHT){ps->_direction = LEFT;}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->_sleep_time > 80){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->_state == OK);}//结束游戏(善后工作 -- 释放动态开辟的内存空间)void Game_Over(psanke ps){pSnakeNode cur = ps->_psnake;pSnakeNode del = NULL;while (cur){del = cur->next;free(cur);cur = del;}}Test.c
#include "snake.h"#include //主要的游戏逻辑void test(){char ch = 0;do{system("cls");//创建贪吃蛇Snake snake = { 0 };//初始化游戏Game_Start(&snake);//运行游戏Game_Run(&snake);//结束游戏(善后工作 -- 释放动态开辟的内存空间)Game_Over(&snake);SetPos(25, 17);printf("要再来一局吗(Y/N):");ch = getchar();getchar();} while ((ch == 'Y') || (ch == 'y'));SetPos(0, 28);}int main(){//环境本地化设置setlocale(LC_ALL, "");//初始化随机数种子srand((unsigned)time(NULL));test();return 0;}今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是作者前进的动力!
- 函数的实现
上一篇:unturned服务器怎么开a?
下一篇:澳大利亚服务器怎么加速?