贪吃蛇游戏运行画面-CSDN直播
目录
[1. 实验目标](#1. 实验目标)
[2. Win32 API介绍](#2. Win32 API介绍)
[2.1 Win32 API](#2.1 Win32 API)
[2.2 控制台程序(Console)](#2.2 控制台程序(Console))
[2.3 控制台屏幕上的坐标COORD](#2.3 控制台屏幕上的坐标COORD)
[2.4 GetStdHandle](#2.4 GetStdHandle)
[2.5 GetConsoleCursorlnfo](#2.5 GetConsoleCursorlnfo)
[2.5.1 CONSOLE_CURSOR_INFO](#2.5.1 CONSOLE_CURSOR_INFO)
[2.6 SetConsoleCursorlnfo](#2.6 SetConsoleCursorlnfo)
[2.7 SetConsoleCursorPosition](#2.7 SetConsoleCursorPosition)
[2.8 GetAsyncKeyState](#2.8 GetAsyncKeyState)
[3. 贪吃蛇准备阶段](#3. 贪吃蛇准备阶段)
[3.1 地图](#3.1 地图)
[3.1.1 本地化](#3.1.1 本地化)
[3.1.2 类型](#3.1.2 类型)
[3.1.3 setlocale函数](#3.1.3 setlocale函数)
[3.14 宽字符的打印](#3.14 宽字符的打印)
[3.1.5 地图坐标](#3.1.5 地图坐标)
[3.2 蛇身和食物](#3.2 蛇身和食物)
[3.3 链表定义蛇身](#3.3 链表定义蛇身)
[3.4 结构体维护贪吃蛇游戏](#3.4 结构体维护贪吃蛇游戏)
[3.5 枚举定义蛇的方向和游戏状态](#3.5 枚举定义蛇的方向和游戏状态)
[3.6 确定游戏流程设计](#3.6 确定游戏流程设计)
[4. 游戏开始(GameStart)](#4. 游戏开始(GameStart))
[4.1 设置游戏窗口大小和名字以及隐藏光标](#4.1 设置游戏窗口大小和名字以及隐藏光标)
[4.2 打印欢迎界面](#4.2 打印欢迎界面)
[4.3 绘制地图](#4.3 绘制地图)
[4.4 初始化蛇身](#4.4 初始化蛇身)
[4.5 创建食物](#4.5 创建食物)
[5. 游戏运行(GameRun)](#5. 游戏运行(GameRun))
[5.1 打印帮助信息(PrintHelpInfo)](#5.1 打印帮助信息(PrintHelpInfo))
[5.2 按键判断与打印得分](#5.2 按键判断与打印得分)
[5.3 蛇身移动(SnakeMove)](#5.3 蛇身移动(SnakeMove))
[5.3.1 判断移动过程中是否遇到食物(NextIsFood)](#5.3.1 判断移动过程中是否遇到食物(NextIsFood))
[5.3.1.1 吃食物(EatFood)](#5.3.1.1 吃食物(EatFood))
[5.3.1.2 不吃食物(NoFood)](#5.3.1.2 不吃食物(NoFood))
[5.3.2 撞到墙游戏结束(KillByWall)](#5.3.2 撞到墙游戏结束(KillByWall))
[5.3.3 咬到自身游戏结束(KillBySelf)](#5.3.3 咬到自身游戏结束(KillBySelf))
[6. 游戏结束(GameEnd)](#6. 游戏结束(GameEnd))
[6.1 总代码](#6.1 总代码)
1. 实验目标
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇
实现的基本功能为:
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上、下、左、右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
要实现这些功能,首先我们必须具备一些知识的储备。例如: C语言函数、枚举、结构体、动态
内存管理、预处理指令、链表、Win32 API等。
2. Win32 API介绍
本次实现贪吃蛇会使用到的一些Win32 API知识,那么就学习一下
2.1 Win32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API 也就是 Microsoft Windows 32 位平台的应用程序编程接口。
其实说人话就是:如果你要基于Windows操作系统来编写一些程序,则Windows会提供各种接口,便于你完成一些功能
2.2 控制台程序(Console)
平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,30行,100列
cpp
mode con cols=100 lines=30
也可以通过命令设置控制台窗口的名字:
cpp
title 贪吃蛇
这些能在控制台窗口执行的命令,也可以调用C语言函数system来执行。例如:
注意:system是执行系统命令,使用system需要包含头文件<stdlib.h>
cpp
#include <stdlib.h>
int main()
{
//设置控制台窗口的长度:设置控制台窗口的大小,30行,100列
system("mode con cols=100 lines=30");
//设置cmd窗口名称
system("title 贪吃蛇");
return 0;
}
运行效果图
执行完后我们会发现窗口大小调制好了,但窗口名却没有,这是因为程序已经结束了。
解决方法:
- getchar(); 执行到这行会停下来,等待接收一个字符
- system("pause") 执行到这行命令程序会暂停
2.3 控制台屏幕上的坐标COORD
注意:使用COORD需要包含头文件<windows.h>
COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
cpp
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
cpp
COORD pos = { 10, 15 };
2.4 GetStdHandle
使用需要包含头文件<windows.h>
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
cppHANDLE GetStdHandle(DWORD nStdHandle);
说人话:要操作特定的控制台程序就要获得它的操作权限,能要识别你在操作谁
实例:
2.5 GetConsoleCursorlnfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO是指向CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息
总结:使用GetConsoleCursorlnfo传入的第一个参数是句柄,为何需要句柄?原因是如要隐藏光标首先需要获得当前控制台对应的光标信息。第二个参数是结构体指针
cpp
HANDLE hOutput = NULL;
//获取标准输出设备的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义结构体变量
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
//调用完这个函数后就能把hOutput对应的光标信息填充到这个结构体变量中去
2.5.1 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
cs
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
-
dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
-
bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
cppCursorInfo.bVisible = false; //隐藏控制台光标
注意:使用false需要包含头文件<stdbool.h>
2.6 SetConsoleCursorlnfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
cpp
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
实例:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <stdbool.h>
int main()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获得控制台光标信息
CursorInfo.bVisible = false;//隐藏控制台光标,false需包含头文件stdbool.h
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
return 0;
}
调试:
可以看出当执行完CursorInfo.bVisible = false;的时候光标还是会显示,只有执行完SetConsoleCursorInfo函数后,光标才会隐藏。
在举一个例子:修改光标可见大小
运行结果:
2.7 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
cpp
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
第一个参数传入的是句柄,第二个参数传入的是坐标信息,也就是COORD类型的结构体变量。
实例:
cpp
#include <stdio.h>
#include <windows.h>
int main()
{
COORD pos = { 10,5 };
HANDLE hOutput = NULL;
//获得标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
printf("haha\n");
return 0;
}
运行结果:
如果我们不去设置指定光标位置,那么haha就会在这里被输出
由于日后我们可能会多次使用设置指定光标位置,所以我们不妨封装一个设置光标位置的函数Setpos
cpp
//设置光标位置
void SetPos(short x, short y)
{
COORD pos = { x,y };
HANDLE hOutput = NULL;
//获得标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
2.8 GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
cpp
SHORT GetAsyncKeyState(
int vKey
);
这个函数需要你传一个虚拟键值进去,然后该函数会检测,传进去的虚拟键值所代表的按键是否被按过,函数通过返回值来分辨按键的状态。(返回值类似是short)
如果返回的这个数据的二进制位的最高位为1,则代表按键状态是按下
如果返回的这个数据的二进制位的最高位为0,则代表按键状态是抬起
如果返回的这个数据的二进制位的最低位为1,则代表该键被按过
如果返回的这个数据的二进制位的最低位为0,则代表该键没被按过
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
cpp
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
举个例子
虚拟键值表如下
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
3. 贪吃蛇准备阶段
3.1 地图
我们最终的贪吃蛇大纲要是这个样子,那我们的地图如何布置呢?
这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍一下控制台窗口的坐标知识。
控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel, 在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。
至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以理论上最多可以表 256 x 256 = 65536 个符号。
后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入和<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
3.1.1 <locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准可以中,依赖地区的部分有以下几项:
- 数字量的格式
- 货币量的格式
- 字符集
- 日期和时间的表示形式
3.1.2 类型
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
- LC_COLLATE:影响字符串比较函数 strco1l()和 strxfrm()。
- LC_CTYPE:影响字符处理函数的行为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响printf()的数字格式。
- LC_TIME:影响时间格式strftime()和wcsftime()。
- LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语言环境。
3.1.3 setlocale函数
使用setlocale函数需要包含头文件<locale.h>
cpp
char* setlocale (int category, const char* locale);
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值:"C"和""。"C"是正常模式,C语言默认的模式;""则是本地模式
在任意程序执行开始,都会隐藏式执行调用:
cpp
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用""作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
cpp
setlocale(LC_ALL, "");//切换本地环境
扩展:
- setlocale的返回值是一个字符串指针,表示已经设置好的格式。如果调用失败,则返回空指针NULL。
- setlocale()可以用来查询当前地区,这时第二个参数设为NULL就可以了。
cpp
#include <locale.h>
int main()
{
char* loc;
loc = setlocale(LC_ALL, NULL);
printf("默认的本地信息:%s\n", loc);
loc = setlocale(LC_ALL, "");
printf("设置后的本地信息:%s\n", loc);
return 0;
}
运行结果:
3.14 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?宽字符的字面量必须加上前缀L,否则C语言会把字面量当作窄字符类型处理。前缀L在单引号前面,表示宽字符,宽字符的打印使用wprintf,对应wprintf()的占位符为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls
cpp
#include <stdio.h>
#include <locale.h>
int main()
{
setlocale(LC_ALL, "");
wchar_t ch1 = L'中';
wchar_t ch2 = L'国';
wchar_t ch3 = L'□';
wchar_t ch4 = L'☆';
printf("%c%c\n", 'a', 'b');
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
wprintf(L"%lc\n", ch3);
wprintf(L"%lc\n", ch4);
return 0;
}
运行结果:
从输出的结果来看,我们发现一个普通字符占一个字符的位置但是打印一个汉字字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。
普通字符和宽字符打印出宽度的展示如下:
3.1.5 地图坐标
我们假设实现一个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,
如下:
注意:
- 列:最好是2的倍数,因为宽字符占2位
- 我们可以看一个正常字符占的大小,我们不难发现,一个字符占的大小的宽度是比较窄的,但是高度是比较长的
3.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外一半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
3.3 链表定义蛇身
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信
息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
cpp
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
3.4 结构体维护贪吃蛇游戏
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
cpp
typedef struct Snake //定义贪吃蛇
{
pSnakeNode pSnake;//维护整条蛇的指针
pSnakeNode pFood;//维护⻝物的指针
enum DIRECTION Dir;//蛇头的⽅向默认是向右
enum GAME_STATUS Status;//游戏状态
int Socre;//当前获得分数
int foodWeight;//默认每个⻝物10分
int SleepTime;//每⾛⼀步休眠时间(蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢)
}Snake, * pSnake;
3.5 枚举定义蛇的方向和游戏状态
蛇的方向,可以一一列举,使用枚举
cpp
//方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
游戏状态,可以一一列举,使用枚举
cpp
//游戏状态
enum GAME_STATUS
{
OK,//正常运⾏
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到⾃⼰
ESC//强制退出游戏
};
3.6 确定游戏流程设计
4. 游戏开始(GameStart)
首先我们先创建3个文件
snake.h ----> 贪吃蛇游戏中类型的声明,函数的声明
snake.c ----> 函数的实现
test.c ----> 贪吃蛇游戏的测试
游戏主逻辑(test.c)
cpp
#include "snake.h"
void test()
{
srand((unsigned int)time(NULL));
int ch = 0;
do
{
Snake ps = { 0 };
//游戏开始前的初始化
GameStart(&ps);
//游戏玩的过程
GameRun(&ps);
//游戏结束
GameEnd(&ps);
SetPos(16,13);
printf("再来一局吗?(Y/N):");
scanf(" %c", &ch);
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//适配本地中文环境
setlocale(LC_ALL, "");
test();
SetPos(0, 27);
return 0;
}
4.1 设置游戏窗口大小和名字以及隐藏光标
cpp
//游戏开始
void GameStart(pSnake ps)
{
//设置控制台的信息,窗口大小,窗口名
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
//打印欢迎信息
WelcomeToGame();
}
首先我们要让窗口大小100行,30列,有人是不是会疑惑为什么不是58行,27列,我们不是之前给的图片就是这样的嘛?
那是因为这只是地图的大小,地图大小外还有提示的信息,就比如下图一样,所以我们给的大小就会比地图大小还要大一些
4.2 打印欢迎界面
效果图:
要实现这个效果图,首先肯定要用到COORD和SetConsoleCursorPosition来设置光标位置
cpp
//定位光标位置
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//在屏幕上设置指定光标位置
SetConsoleCursorPosition(hOutput, pos);
}
接下来就是打印欢迎信息
cpp
//打印欢迎信息
void WelcomeToGame()
{
//欢迎信息
SetPos(35,10);
printf("欢迎来带贪吃蛇小游戏\n");
SetPos(38, 20);
//暂停
system("pause");
//清屏
system("cls");
//功能介绍
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(15, 11);
printf("加速将得到更高的分数。");
SetPos(38, 20);
system("pause");
system("cls");
}
4.3 绘制地图
创建地图就是将墙打印出来,因为是宽字符打印,所以使用wprintf函数,打印格式串前使用L
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
因为打印墙体字符L'□'我们后续会经常使用,所以我们把它封住成宏
cpp
#define WALL L'□'
打印墙体代码:
cpp
//绘制地图
void CreateMap()
{
SetPos(0, 0);
int i = 0;
//一个宽字符占2位,所以i加的是2
//i到56就行了,因为输出WALL就能把56个57空间给占了
//上框框
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下框框
SetPos(0, 26);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左框框
//这里i只要加1的原因是宽字符和正常字符的高度是一样的
//只有宽字符和正常字符的宽度有差2倍
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右框框
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
效果图:
4.4 初始化蛇身
我们可以按照下面这张图来进行初始化蛇身与食物
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。
再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,蛇的状态,每个食物的分数。
蛇身打印的宽字符(由于我们后面可能会多次打印蛇身宽字符L'●',所以我们索性把它封装成宏):
cpp
#define BODY L'●'
1.打印蛇身
cpp
//初始化蛇身
//ps是维护整条蛇的地址
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
//按照图片上的指示创建5个节点
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//创建好后就进行头插
cur->next = ps->pSnake;
ps->pSnake = cur;
}
我把行和列封装成一个宏,这样的好处是方便后续修改
cpp
#define POS_X 24
#define POS_Y 5
头插法的解析
2.创建好后打印蛇身
cpp
//创建好后就打印蛇的身体
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
3.初始化贪吃蛇的其他数据
cpp
//初始化贪吃蛇的数据
ps->SleepTime = 200;
ps->Socre = 0;
ps->Status = OK;
ps->Dir = RIGHT;
ps->foodWeight = 10;
ps->pFood = NULL;
4.初始化蛇身总代码
cpp
//初始化蛇身
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
//按照图片上的指示创建5个节点
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//创建好后就进行头插
cur->next = ps->pSnake;
ps->pSnake = cur;
}
//创建好后就打印蛇的身体
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//初始化贪吃蛇的数据
ps->SleepTime = 200;
ps->Socre = 0;
ps->Status = OK;
ps->Dir = RIGHT;
ps->foodWeight = 10;
ps->pFood = NULL;
}
4.5 创建食物
首先创建食物我们需要考虑的问题
- 食物是随机出现的,所以坐标就是随机的(但是生成的坐标x必须是2的倍数)
- 生成的坐标必须在墙内
- 生成的坐标不能在蛇的身上
由于打印食物宽字符L'★'后续可能会多次用到,所以我们把它封装成一个宏
cpp
#define FOOD L'★'
1.创建食物坐标
cpp
int x = 0;
int y = 0;
agin:
//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
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 agin;
}
cur = cur->next;
}
x = rand() % 53 + 2;
rand() % 53生成范围是0~52,后面加2,所以x的范围则是2~54
y = rand() % 25 + 1;
rand() % 25 + 1;生成范围是0~24,后面加1,所以y的范围则是1到25
首先我们已经保证生成的坐标是在墙内
然后我们还要判断x是否是2的倍数,所以我们用了个do-while循环来进行判断
后续我们又用了while循环来判断生成的食物节点是否与蛇身的某个节点重合,如果一样,我们就用goto语句来进行跳转,使坐标重新生成
|--------------------------------|
| 使用rand(),我们需要包含头文件<stdlib.h> |为了防止生成的随机数与后续再次执行的程序一样,我用了srand((unsigned int)time(NULL));来修改种子,如果有不懂srand和time的同学可以看我之前写的博客(猜数字游戏),然后使用srand函数需要包含头文件<stdlib.h>;使用time需要包含头文件<time.h>
走到这一步我们生成的x与y坐标就已经符合要求了,接下来就是生成一个节点(pFood),让x与y赋值给pFood中的x与y,最后让维护蛇指针内的pFood指针来指向这个节点
cpp
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = pFood;
创建食物总代码
cpp
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
agin:
//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
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 agin;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
pFood->x = x;
pFood->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->pFood = pFood;
}
5. 游戏运行(GameRun)
首先我们再次看一遍游戏流程设计
- 游戏运行期间,右侧打印帮助信息,提示玩家
- 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
- 如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
- 确定了蛇的方向和速度,蛇就可以移动了。
5.1 打印帮助信息(PrintHelpInfo)
首先我们可以看到程序运行起来后右侧的帮助信息
因为我们设置游戏窗口的时候设置的是100列,30行,所以设置帮助信息我是一开始是把光标定位到62列17行,大家可以按自己的想法来,不一定要和我一样
cpp
//打印帮助信息
void PrintHelpInfo()
{
SetPos(62, 17);
printf("1.不能穿墙,不能咬到自己");
SetPos(62, 18);
printf("2.用↑.↓.←.→分别控制蛇的移动。");
SetPos(62, 19);
printf("3.F3是加速,F4是减速");
SetPos(62, 20);
printf("ESC:退出游戏 SPACE:暂停游戏");
}
5.2 按键判断与打印得分
首先,我们需要知道每个键的虚拟键值,如下:
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
然后我整理出了本程序需要用到的虚拟键值
然后我们就要判断我们是否有按过这些键,就可以用到前面GetAsyncKeyState中定义的宏
cpp
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
游戏玩的过程总代码
cpp
//游戏玩的过程
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印当前得分
SetPos(62, 10);
printf("得分:%d", ps->Socre);
SetPos(62, 11);
printf("每个食物得分:%-2d", 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_F3))
{
//加速
if (ps->SleepTime >= 50)
{
ps->SleepTime -= 30;
ps->foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->foodWeight > 2)
{
ps->SleepTime += 30;
ps->foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
//按ESC键退出
ps->Status = ESC;
break;
}
//蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
Sleep(ps->SleepTime);
SnakeMove(ps);
//判断是否撞墙了
KillByWall(ps);
//判断是否撞到自己了
KillBySelf(ps);
} while (ps->Status == OK);
}
当我们按下了上下左右的时候,我们还要与当前蛇行走的方向进行判断
- 当我们按上的时候,蛇当前运动方向是不能向下的
- 当我们按下的时候,蛇当前运动方向是不能向上的
- 当我们按左的时候,蛇当前运动方向是不能向右的
- 当我们按右的时候,蛇当前运动方向是不能向左的
- 当我们按ESC键的时候,我们就把当前游戏状态进行修改,然后直接退出循环
- 当我们按下F3就会加速,休眠时间变短,但时间不可能减成负数,索性我们就规定当休眠时间大于等于50的时候,我们才能加速,当然休眠时间变短一次吃食物分数就会变多
- 当我们按下F4就会减速,休眠时变长,一次吃的食物分数变少,但是我们不能把分数减到0吧,不能吃一个食物一分都不得,所以我们规定只有食物分数大于2时我们按了F4才有效果
注意事项(打印每个食物得分):
打印每个食物的得分有个小细节就是要用%2d(或者%-2d,%3d,%-4d都可以),就是不要用%d来打印,因为一开始默认每个食物的得分是10分,后续我们是可以用F3和F4来控制蛇移动的速度,速度决定每吃一个食物所得分数。如果我们速度慢下来了,食物所得分数就会变少,由原来的10分变成8分或者更低,但因为一开始我们默认打印了10分,然后减了一次速度按理来说是要打印8分,但用%d打印的结果是80,原因是10后面的0没有被覆盖掉
5.3 蛇身移动(SnakeMove)
当然我们在蛇身移动前我们可以让蛇先休眠一下
蛇身移动的主要思想:
- 先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
- 确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理
(EatFood),如果不是食物则做前进一步的处理(NoFood)。 - 蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
移动过程:
向上和向下
- 向上走:首先创建的新节点是在蛇头上面,由图可以看出新节点y坐标就是蛇头坐标y减1,而新节点x坐标则是和蛇头x坐标一样
- 向下走:首先创建的新节点是在蛇头下面,由图可以看出新节点y坐标就是蛇头坐标y加1,而新节点x坐标则是和蛇头x坐标一样
向左和向右
- 向左走:首先创建的新节点是在蛇头左边,由图可以看出新节点x坐标是蛇头节点x坐标减2(减2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
- 向右走:首先创建的新节点是在蛇头右边,由图可以看出新节点x坐标是蛇头节点x坐标加2(加2的原因是宽字符打印宽度占2位),新节点y坐标则是和蛇头节点y坐标一样
cpp
//蛇移动过程
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
{
//没遇到食物
NotEatFood(pNextNode, ps);
}
}
5.3.1 判断移动过程中是否遇到食物(NextIsFood)
当然在蛇移动的过程中,可能移动的下一个节点就是食物,所以我们还要判断是否遇到了食物
cpp
//判断移动过程中是否碰到食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
}
相等返回1,不相等返回0
5.3.1.1 吃食物(EatFood)
如果返回值是1,则我们遇到了食物,那么我们就把新节点头插到贪吃蛇身上,然后打印蛇身
同时,我们之前定义食物的时候还动态malloc了一块空间,我们既然用新节点的空间头插到贪吃蛇上面,那我们就应该将原本食物的节点给销毁(free)
吃掉了食之后,我们原先在地图上的食物就被覆盖了,那么此时我们就应该再创建一个食物
同时,我们吃掉了一个食物之后,我们的分数也应该变高,就让原先的分数加上一个食物的分数
cpp
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
pSnakeNode cur = ps->pSnake;
ps->Socre += ps->foodWeight;
//打印蛇身
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
free(ps->pFood);
CreateFood(ps);
}
首先我不知道看到这里的小伙伴会不会有这个疑问:
我们知道食物和新节点是不同的地址(两次malloc开辟出来的),但是他们两个空间重合了(食物和下一个节点在地图上存放空间重合),所以会不会不理解释放食物为什么不会把下一个节点空间也释放了?
原因:
创建出来的下一个节点和食物这个节点只是x和y坐标一样,节点是完全不同的两个节点,所以释放食物对创建出来的下一个节点没有影响
5.3.1.2 不吃食物(NoFood)
当下一个节点不是食物的时候,我们就先将下一个节点头插到贪吃蛇上面
同时我们需要知道,本来贪吃蛇是已经被打印出来了的(初始化蛇身的时候),所以我们只需要将新的头节点打印出来,同时将尾节点打印成空格并释放,我们就能在视觉上达到贪吃蛇走一步的效果
至于找到尾结点对我们来说可是很轻松的,用while循环就能办到
**注意:**SetPos到尾结点位置之后要打印两个空格,注意,是两个空格,因为尾结点宽字符占2位
cpp
//pSnakeNode pNextNode 是下⼀个节点的地址
//pSnake ps 维护蛇的指针
//下一步要走的位置处不是食物,就不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
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(" ");
free(cur->next);
cur->next = NULL;
}
我不知道有没有小伙伴没看我前面介绍有这一个疑问,反正当时我是有这个疑问的:
原因是:这里就是不进行打印的,使用的是上一次打印留下的图形,走到cur->next->next是为了释放下一个节点,所以这个函数没有进行清屏,而是用printf(" ");来把最后一个图形覆盖掉
5.3.2 撞到墙游戏结束(KillByWall)
判断蛇是否撞到墙,我们只需要判断贪吃蛇的头节点是否在墙壁所圈定的范围之内,如果不在这个范围内,那就证明蛇已经撞到墙了
接着,我们需要将游戏的状态更改为 KILL_BY_WALL
当蛇向后走了一步时,判断到状态不为 OK ,就会跳出循环,游戏结束
cpp
//判断是否撞墙了
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;
}
5.3.3 咬到自身游戏结束(KillBySelf)
我们要判断蛇是否会撞到自己,我们只需要将头节点和蛇身的每一个坐标一一比对,当发现有相同的时候,就说明蛇已经咬到自己了
接着,我们需要将游戏的状态更改为 KILL_BY_SELF
当蛇向后走了一步时,判断到状态不为 OK ,就会跳出循环,游戏结束
cpp
//检测是否撞自己
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;
return;
}
cur = cur->next;
}
}
6. 游戏结束(GameEnd)
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点与食物节点。
至于删除蛇身,我们可以定义两个指针,一个指向要删除的节点,一个指向下一个节点
这是因为如果我们将该节点的空间释放掉之后,我们就找不到下一个节点了,所以我们才需要两个节点,而循环的条件就是当指针cur指向 NULL 的时候,循环停止
在释放完之后,不忘释放食物的空间
cpp
/游戏结束
void GameEnd(pSnake ps)
{
SetPos(18, 10);
switch (ps->Status)
{
case KILL_BY_WALL:
printf("很遗憾撞墙了,游戏结束");
break;
case KILL_BY_SELF:
printf("很遗憾撞到自身了,游戏结束");
break;
case ESC:
printf("按了ESC键,正常退出");
break;
}
//释放蛇身节点和食物节点
pSnakeNode cur = ps->pSnake;
pSnakeNode pNextNode = NULL;
while (cur)
{
pNextNode = cur;
cur = cur->next;
free(pNextNode);
}
free(ps->pFood);
ps = NULL;
}
6.1 总代码
test.c
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
void test()
{
srand((unsigned int)time(NULL));
int ch = 0;
do
{
Snake ps = { 0 };
//游戏开始前的初始化
GameStart(&ps);
//游戏玩的过程
GameRun(&ps);
//游戏结束
GameEnd(&ps);
SetPos(16,13);
printf("再来一局吗?(Y/N):");
scanf(" %c", &ch);
} while (ch == 'Y' || ch == 'y');
}
int main()
{
//适配本地中文环境
setlocale(LC_ALL, "");
test();
SetPos(0, 27);
return 0;
}
snake.h
cpp
#pragma once
#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#include <locale.h>
#include <stdlib.h>//随机数
#include <time.h>
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//定义蛇身节点
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 Socre;//当前获得分数
int foodWeight;//默认每个食物10分
int SleepTime;//每走一步休眠时间
}Snake, * pSnake;
//定义蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//游戏状态
enum GAME_STATUS
{
OK,//正常运行
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到自己
ESC, //按了ESC键退出,正常退出
};
//游戏开始前的准备
void GameStart(pSnake ps);
//打印欢迎信息
void WelcomeToGame();
//定位控制台光标位置
void SetPos(int x, int y);
//绘制地图
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行的整个逻辑
void GameRun(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//蛇移动过程
void SnakeMove(pSnake ps);
//判断蛇头的下一步要走的位置处是否是食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps);
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps);
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞自己
void KillBySelf(pSnake ps);
//游戏结束,处理善后工作
void GameEnd(pSnake ps);
snake.c
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
//定位光标位置
void SetPos(int x, int y)
{
COORD pos = { x,y };
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//在屏幕上设置指定光标位置
SetConsoleCursorPosition(hOutput, pos);
}
//打印欢迎信息
void WelcomeToGame()
{
//欢迎信息
SetPos(35,10);
printf("欢迎来带贪吃蛇小游戏\n");
SetPos(38, 20);
//暂停
system("pause");
//清屏
system("cls");
//功能介绍
SetPos(15, 10);
printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");
SetPos(15, 11);
printf("加速将得到更高的分数。");
SetPos(38, 20);
system("pause");
system("cls");
}
//绘制地图
void CreateMap()
{
SetPos(0, 0);
int i = 0;
//一个宽字符占2位,所以i加的是2
//i到56就行了,因为输出WALL就能把56个57空间给占了
//上框框
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//下框框
SetPos(0, 26);
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%lc", WALL);
}
//左框框
//这里i只要加1的原因是宽字符和正常字符的高度是一样的
//只有宽字符和正常字符的宽度有差2倍
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//右框框
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
//初始化蛇身
void InitSnake(pSnake ps)
{
pSnakeNode cur = NULL;
//按照图片上的指示创建5个节点
int i = 0;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake():malloc()");
return;
}
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
cur->next = NULL;
//创建好后就进行头插
cur->next = ps->pSnake;
ps->pSnake = cur;
}
//创建好后就打印蛇的身体
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//初始化贪吃蛇的数据
ps->SleepTime = 200;
ps->Socre = 0;
ps->Status = OK;
ps->Dir = RIGHT;
ps->foodWeight = 10;
ps->pFood = NULL;
}
//创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
agin:
//使食物坐标必须要在墙内,并且x的坐标必须要是2的倍数
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 agin;
}
cur = cur->next;
}
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pFood == NULL)
{
perror("CreateFood()::malloc()");
return;
}
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("title 贪吃蛇");
//隐藏光标
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(hOutput, &CursorInfo);
//打印欢迎信息
WelcomeToGame();
//绘制地图
CreateMap();
//初始化蛇身
InitSnake(ps);
//创建食物
CreateFood(ps);
}
//打印帮助信息
void PrintHelpInfo()
{
SetPos(62, 17);
printf("1.不能穿墙,不能咬到自己");
SetPos(62, 18);
printf("2.用↑.↓.←.→分别控制蛇的移动。");
SetPos(62, 19);
printf("3.F3是加速,F4是减速");
SetPos(62, 20);
printf("ESC:退出游戏 SPACE:暂停游戏");
}
//暂停过程
void pause()
{
while (1)
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//判断移动过程中是否碰到食物
int NextIsFood(pSnakeNode pNextNode, pSnake ps)
{
return (pNextNode->x == ps->pFood->x && pNextNode->y == ps->pFood->y);
}
//下一步要走的位置处就是食物,就吃掉食物
void EatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
pSnakeNode cur = ps->pSnake;
ps->Socre += ps->foodWeight;
//打印蛇身
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
free(ps->pFood);
CreateFood(ps);
}
//下一步要走的位置处不是食物,不吃食物
void NotEatFood(pSnakeNode pNextNode, pSnake ps)
{
pNextNode->next = ps->pSnake;
ps->pSnake = pNextNode;
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(" ");
free(cur->next);
cur->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 (ps->pSnake->x == cur->x && ps->pSnake->y == cur->y)
{
ps->Status = KILL_BY_SELF;
return;
}
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
{
//没遇到食物
NotEatFood(pNextNode, ps);
}
}
//游戏玩的过程
void GameRun(pSnake ps)
{
//打印帮助信息
PrintHelpInfo();
do
{
//打印当前得分
SetPos(62, 10);
printf("得分:%d", ps->Socre);
SetPos(62, 11);
printf("每个食物得分:%-2d", 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_F3))
{
//加速
if (ps->SleepTime >= 50)
{
ps->SleepTime -= 30;
ps->foodWeight += 2;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->foodWeight > 2)
{
ps->SleepTime += 30;
ps->foodWeight -= 2;
}
}
else if (KEY_PRESS(VK_ESCAPE))
{
//按ESC键退出
ps->Status = ESC;
break;
}
//蛇每走一步都需要休眠,休眠时间越短,蛇移动速度就越快
Sleep(ps->SleepTime);
SnakeMove(ps);
//判断是否撞墙了
KillByWall(ps);
//判断是否撞到自己了
KillBySelf(ps);
} while (ps->Status == OK);
}
//游戏结束
void GameEnd(pSnake ps)
{
SetPos(18, 10);
switch (ps->Status)
{
case KILL_BY_WALL:
printf("很遗憾撞墙了,游戏结束");
break;
case KILL_BY_SELF:
printf("很遗憾撞到自身了,游戏结束");
break;
case ESC:
printf("按了ESC键,正常退出");
break;
}
//释放蛇身节点和食物节点
pSnakeNode cur = ps->pSnake;
pSnakeNode pNextNode = NULL;
while (cur)
{
pNextNode = cur;
cur = cur->next;
free(pNextNode);
}
free(ps->pFood);
ps = NULL;
}