找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程(ಥ_ಥ)-CSDN博客
所属专栏:C语言
游戏演示:
贪吃蛇游戏演示
目录
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
游戏前期准备:
本次实现贪吃蛇会使用到的一些Win32 API知识,接下来我们就学习一下。
背景介绍:
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
设置控制台相关的信息
平常我们运行起来的黑框程序其实就是控制台程序(如下图所示)。
我们可以使用cmd命令来设置控制台窗口的长宽。例如:设置控制台窗口的大小,30行,100列。
//格式: 列 行
mode con cols=100 lines=30
注意:
1. 列和行在赋值时,不能带有空格。例如:cols = 100,这就是不行的,没有影响到控制台的大小。
2. 使用这个命令之前,需要把这个控制台改为让Windows决定或者Windows 控制台主机
演示:
改变VS编译器的控制台
3. 使用system函数所需要包含的头文件既可以是stdlib.h,也可以是Windows.h(不分大小写的,因此可以使用"windows.h"、"WINDOWS.H"或者"Windows.h"等形式来引用该头文件。不过,一般约定使用"Windows.h"的形式来引用该头文件,以保持代码的一致性和可读性。)
下面就来使用这个来改变控制台的大小。
从上面的结果来看:行列对应不一致。没错,一行的宽度是一列的宽度的二倍。
也可以通过命令设置控制台窗口的名字:
//格式:
title 要修改的名字
注意:在更改之后要观察到的话,就不能让程序运行结束,也就是说只能在程序运行期间才能够观察的到。
控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。
COORD类型的声明:
typedef struct _COORD {
SHORT X;//短整型
SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };
GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
标准输入是指键盘,标准输出和标准错误是指屏幕。
这个句柄就类似一个遥控器,可以通过句柄来操作标准设备。而我们想要操作标准设备也得通过GetStdHandle这个函数来获得句柄。再通过句柄来操作。
HANDLE GetStdHandle(DWORD nStdHandle);
例如:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息。
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
CONSOLE_CURSOR_INFO 这个结构体,包含有关控制台光标的信息。
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;//光标的宽度占比
BOOL bVisible;//光标的可见性
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
举例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo = {0};
//获取控制台光标信息
GetConsoleCursorInfo(hOutput, &CursorInfo);
SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
举例:
//获取标准输出的句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//定义一个存放光标信息的结构体
CONSOLE_CURSOR_INFO CursorInfo = {0};
//获取控制台光标信息存放到这个结构体中
GetConsoleCursorInfo(hOutput, &CursorInfo);
//隐藏控制台光标
CursorInfo.bVisible = false;
//把控制台光标大小调到最大
CursorInfo.dwSize = 100;
//设置控制台光标状态(按照上面的设置调)
SetConsoleCursorInfo(hOutput, &CursorInfo);
SetConsoleCursorPosition
设置控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的变量中,调 用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的位置设置有可能不一定会成功。
GetAsyncKeyState
获取按键情况,GetAsyncKeyState的函数原型如下:
SHORT GetAsyncKeyState(int vKey);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。GetAsyncKeyState 的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低位是否为1 。下面是虚拟键代码虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn虚拟键码
举例:检测数字键是否被摁过。(字母键上面的数字键)
//判断一个键是否被摁过
//如果被摁过结果就是1,否则就是0
#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1)?1:0)
int main()
{
while (1)
{
if (KEY_PRESS(0x30))
{
printf("%d\n", 0);
}
else if (KEY_PRESS(0x31))
{
printf("%d\n", 1);
}
else if (KEY_PRESS(0x32))
{
printf("%d\n", 2);
}
else if (KEY_PRESS(0x33))
{
printf("%d\n", 3);
}
else if (KEY_PRESS(0x34))
{
printf("%d\n", 4);
}
else if (KEY_PRESS(0x35))
{
printf("%d\n", 5);
}
else if (KEY_PRESS(0x36))
{
printf("%d\n", 6);
}
else if (KEY_PRESS(0x37))
{
printf("%d\n", 7);
}
else if (KEY_PRESS(0x38))
{
printf("%d\n", 8);
}
else if (KEY_PRESS(0x39))
{
printf("%d\n", 9);
}
}
}
贪吃蛇游戏设计与分析
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞自身死亡
• 计算得分
• 蛇身加速、减速
• 暂停游戏
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
加入了宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
<locale.h>本地化
setlocale函数原型:
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 - 针对所有类项修改,将以上所有类别设置为给定的语言环境。
setlocale 函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。 setlocale 的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。 C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。 在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是一个点。 当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
setlocale(LC_ALL, "");//切换到本地环境(注意这里双引号里不能有空格)
那如果想在屏幕上打印宽字符,怎么打印呢? 宽字符的字面量必须加上前缀“L”,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前面,表示宽字符串,对应 wprintf() 的占位符为 %ls 。
举例:
#include <stdio.h>
#include <locale.h>
int main()
{
//切换到本地环境
setlocale(LC_ALL, "");
wprintf(L"%s\n", L"我要学编程");
wchar_t wc1 = L'我';
wchar_t wc2 = L'要';
wchar_t wc3 = L'学';
wchar_t wc4 = L'编';
wchar_t wc5 = L'程';
wprintf(L"%lc", wc1);
wprintf(L"%lc", wc2);
wprintf(L"%lc", wc3);
wprintf(L"%lc", wc4);
wprintf(L"%lc", wc5);
return 0;
}
地图,食物和蛇身的设计
我们假设实现一个棋盘27行,58列的棋盘,再围绕地图画出墙。
由于1行的宽度是一列宽度的二倍,就可以按照上面的样式绘制地图。这些都用宽字符来打印。
初始化状态:假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。 注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中, 另外一半在墙外的现象。 关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。食物的x坐标也要是2的倍数。
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信 息,那么蛇的每一节其实就是链表的一个节点。每个节点只要记录好蛇身节点在地图上的坐标就行, 所以蛇节点结构如下:
//蛇身的节点类型
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
还得创建一些变量:指向蛇头的指针,初始时蛇的速度,蛇的方向,食物,食物分数,总分,贪吃蛇的状态。但是这些都比较麻烦,我们就创建一个贪吃蛇的结构体来管理这些变量。
//贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物的指针
DIRECTION _dir;//蛇的方向
GAME_STATE _state;//游戏的运行状态
int _FoodWeight;//一个食物的分数
int _score;//总分数
int _SleepTime;//休眠时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
蛇的方向有四种:上,下,左,右。我们就可以枚举出来。
//蛇的方向
typedef enum DIRECTION
{
UP = 1,//上
DOWN,//下
LEFT,//左
RIGHT//右
}DIRECTION;
游戏的运行状态:正常运行,正常退出,撞墙死亡,撞到自己死亡。
//游戏的状态
typedef enum GAME_STATE
{
OK,//正常运行
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞到自己
END_NORMAL//正常退出
}GAME_STATE;
接下来就是正式的游戏设计。
首先分装三个大的函数。
GameStart()—— 游戏的初始化
初始化的内容:1,打印欢迎界面 2,绘制贪吃蛇地图 3,初始化贪吃蛇和食物 。
打印欢迎界面
首先得分装一个函数用来定位坐标。
//定位光标
void SetPos(short x, short y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);//设置光标的位置
}
system("pause")是一个用于暂停控制台的函数。它会在控制台输出一个提示信息,等待用户按下任意键后才会继续执行程序。
system("pause");
由上面的界面切换到下面这个界面,就需要用到一个清理控制台界面的函数。
system("cls");//用于清理当前控制台的界面所有信息
打印欢迎界面:
//打印欢迎界面
void WelcomToGame()
{
//首先得定位光标
SetPos(39, 10);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(40, 15);
system("pause");//暂停
system("cls");//清理屏幕
SetPos(28, 10);
printf("用↑.↓.←.→ 来控制蛇的移动!摁F3加速!摁F4减速!\n");
SetPos(38, 11);
printf("游戏即将开始,请做好准备!\n");
SetPos(40, 15);
system("pause");
system("cls");
}
绘制贪吃蛇地图
上面这个就是我们要绘制的地图。值得一提的是:这个方块是宽字符,而使用wprintf来打印宽字符就得先将C语言环境转化到本地环境。至于后面的打印,就是通过定位来循环打印。
先打印上下两行,再打印左右两列。
上一行的坐标是(2*i,0),i 的范围是0~28。 下一行的坐标是(2*i,25),i 的范围是0~28。
左一列的坐标是(0,i),i 的范围是1~25。 右一列的坐标是(56,i),i 的范围是1~25。
注意:
1. 一行的打印就相当于是打印了一列中的一个,因此用总列数-2就是我们要打印的列坐标。
2. 列在打印时,需要先定位好坐标。因为打印的顺序是默认从左到右的;而我们是要实现从上到下的打印。
#define WALL L'□'
//绘制地图
void CreatMap()
{
//打印上体墙
for (int i = 0; i <= 28; i++)
{
wprintf(L"%lc", WALL);
}
SetPos(0, 26);//定位到下体墙的位置
//打印下体墙
for (int i = 0; i <= 28; i++)
{
wprintf(L"%lc", WALL);
}
//打印左体墙
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);//用换行不行,因此加不加换行无所谓
}
//打印右体墙
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);//用换行不行,因此加不加换行无所谓
}
}
初始化贪吃蛇
既然要初始化蛇,首先就得有蛇,而蛇是用节点串起来的。因此我们只要创建5个节点,并且传入我们想要的坐标(我们想要蛇出现在哪个位置,这个位置最好是固定的,这里也是采用固定的位置。),最后把这些节点串起来就行了。
//创建一条蛇
pSnakeNode pcur = NULL;
pSnakeNode prev = NULL;
for (int i = 0; i < 5; i++)
{
pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));
if (tmp == NULL)
{
perror("InitSnake():malloc:");
return;
}
pcur = tmp;
pcur->x = POS_X + 2 * i;
pcur->y = POS_Y;
pcur->next = NULL;
if (ps->_pSnake == NULL)
{
//直接插入即可
ps->_pSnake = pcur;
}
else
{
//尾插
if (prev != NULL)
prev->next = pcur;
}
prev = pcur;
}
打印蛇(打印整个链表)
#define BODY L'●'
#define HEAD L'◆'
//开始在控制台上打印蛇
int count = 0;
while (pcur)
{
SetPos(pcur->x, pcur->y);//定位
if (count == 0)
{
count++;
wprintf(L"%lc", HEAD);//打印蛇头
}
else
{
wprintf(L"%lc", BODY);//打印蛇身
}
pcur = pcur->next;
}
设置贪吃蛇的属性。
//设置贪吃蛇的属性
ps->_dir = LEFT;//初始时蛇的方向向左
ps->_FoodWeight = 50;//一个食物50分
ps->_score = 0;//总分为0
ps->_SleepTime = 200;//单位是毫秒
ps->_state = OK;//正常运行
因为这里的蛇头是在最左边,所以这个蛇的初始方向不能是向右走,除此之外都可以。
初始化食物
初始化食物其实就是创建一个食物并且打印出来。
这个食物为了能够被蛇给吃掉,x坐标也必须是2的倍数,并且这个食物应该是要随机生成的,还要在这个墙体中。
int x = 0;
int y = 0;
//随机创建食物(食物的x坐标必须是2的倍数,因此要判断)
again:
do
{
//为了食物出现在墙内
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2);
//食物的坐标不能和蛇身冲突
pSnakeNode pcur = ps->_pSnake;
//开始寻找看看是否与蛇身冲突
while (pcur)
{
if (pcur->x == x && pcur->y == y)
{
goto again;//如果冲突了,就要回炉重造
}
pcur = pcur->next;
}
//开始创建食物的节点
pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));
if (tmp == NULL)
{
perror("CreatFood():malloc:");
return;
}
tmp->x = x;
tmp->y = y;
tmp->next = NULL;
ps->_pFood = tmp;
打印食物
//打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
GameRun()——游戏的运行
1,打印帮助手册 2,贪吃蛇的相关运行信息 3,判断贪吃蛇是否死亡
上图就是游戏运行时的界面。
打印右侧的帮助信息
//打印帮助信息
void PrintHelpInfo()
{
SetPos(68, 10);
printf("小提示:");
SetPos(68, 13);
printf("不能穿墙,不能咬到自己!");
SetPos(68, 14);
printf("用↑.↓.←.→ 来控制蛇的移动!");
SetPos(68, 15);
printf("摁F3加速!摁F4减速!");
SetPos(68, 16);
printf("加速将增加单个食物的分数!");
SetPos(68, 17);
printf("减速将减少单个食物的分数!");
SetPos(68, 18);
printf("摁Esc退出游戏!摁空格暂停游戏!");
}
贪吃蛇的相关运行信息
贪吃蛇要运行起来,就得需要我们摁键来实现贪吃蛇的走动。所以接下来就是判断哪个键是否摁过来判断蛇的走向。而只要是蛇的状态不等于OK时,此时就不需要再走了。
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
//接下来就是通过按键来判断贪吃蛇的运行状态
do
{
//打印分数显示
SetPos(68, 7);
printf("当前总分数:%08d", ps->_score);
SetPos(68, 8);
printf("当前食物分数:%02d", 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->_state = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速(减少休眠时间)
if (ps->_SleepTime > 80)//设置为4档速度
{
ps->_SleepTime -= 30;
ps->_FoodWeight += 10;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_FoodWeight > 10)//设置为4档速度
{
ps->_SleepTime += 20;
ps->_FoodWeight -= 10;
}
}
//蛇开始走
//走一步,就休息一下
SnakeMove(ps);//蛇走一步的过程
Sleep(ps->_SleepTime);
//检测是否撞墙
KillByWall(ps);
//检测是否撞到自己
KillBySelf(ps);
//通过总分数来判断游戏是否结束
if (ps->_score > 30000)
{
SetPos(20, 13);
printf("恭喜你!成功通关!");
}
} while (ps->_state == OK);//只有蛇的状态正常才走
暂停的实现就只需要系统一直处于休眠状态。
void Pause()
{
while (1)
{
//休眠200毫秒
Sleep(200);
//这个只能放到休眠的后面
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
当进入这个暂停函数就需要休眠,即使再次摁了空格键,也得先休眠一下。
蛇在走的时候,就是根据我们摁的键位来判断蛇头应该出现在哪个地方。再通过链接蛇头的下一个位置和蛇身以及释放蛇身的尾节点。还有一个小细节:如果蛇头下一个位置是食物的话,就要吃掉食物,并且再创建一个食物,而如果不是食物的话,就只需要按照上面的步骤走就行了。因此就得先判断是否为食物。
//蛇走一步的过程
void SnakeMove(pSnake ps)
{
//创建一个节点来存放蛇要走的下一个节点
pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));
if (next == NULL)
{
perror("SnakeMove():malloc:");
return;
}
//根据方向来判断蛇是怎么走的
switch (ps->_dir)
{
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 (NextIsFood(next, ps))
{
//是食物就吃掉食物
EatFood(next, ps);
}
else
{
//不是就不吃
NoFood(next, ps);
}
}
下一个位置是食物
//下一个位置是食物,就可以吃掉
void EatFood(pSnakeNode next, pSnake ps)
{
//把这个节点(就是食物节点)头插到蛇身就行
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放掉这个节点(因为创建了两个节点:一个食物节点,一个蛇头的下一个节点)
free(next);
next = NULL;
//打印蛇身
pSnakeNode pcur = ps->_pSnake;
int count = 0;
while (pcur)
{
SetPos(pcur->x, pcur->y);//定位
if (count == 0)
{
count++;
wprintf(L"%lc", HEAD);//打印蛇头
}
else
{
wprintf(L"%lc", BODY);//打印蛇身
}
pcur = pcur->next;
}
//分数的增加
ps->_score += ps->_FoodWeight;
//重新创建食物
CreatFood(ps);
}
下一个位置不是食物
//下一个位置不是食物
void NoFood(pSnakeNode next, pSnake ps)
{
//把下一个位置的节点头插到蛇身
next->next = ps->_pSnake;
ps->_pSnake = next;
//把蛇身最后一个节点的空间释放掉,顺便打印蛇身
pSnakeNode pcur = ps->_pSnake;
int count = 0;
while (pcur->next->next != NULL)
{
//遍历时,可以直接打印
SetPos(pcur->x, pcur->y);
if (count == 0)
{
count++;
wprintf(L"%lc", HEAD);
}
else
{
wprintf(L"%lc", BODY);
}
pcur = pcur->next;
}
//把最后一个节点的位置打印成空格
SetPos(pcur->next->x, pcur->next->y);
printf(" ");
free(pcur->next);
pcur->next = NULL;
}
如果不把蛇身的尾节点的位置打印成空格,那么上一次的痕迹就不会被消除。 会导致蛇身一直变长。 走一步,再休息两百毫秒,可以让我们有时间来判断贪吃蛇下一步需怎么走,如果不休息就会直接撞墙。
检测是否撞墙只需要检测蛇头是否撞墙,因为蛇身的每一个节点是重复执行蛇头的操作。蛇头不撞墙那么蛇身就没有机会撞墙,如果蛇头撞墙,那么就说明这个蛇撞墙了。
//检测是否撞墙
void KillByWall(pSnake ps)
{
//只要判断蛇头是否碰到墙就可以了
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56
|| ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
{
ps->_state = KILL_BY_WALL;//改变蛇的状态即可
}
}
检测蛇是否撞到自身就只需要检测蛇头和蛇身的某一个节点是否重合就行了。
//检测是否撞到自己
void KillBySelf(pSnake ps)
{
//只要判断蛇头是否碰到自己的蛇身
pSnakeNode pcur = ps->_pSnake->next;
while (pcur)
{
if (pcur->x == ps->_pSnake->x && pcur->y == ps->_pSnake->y)
{
ps->_state = KILL_BY_SELF;//改变蛇的状态即可
break;
}
pcur = pcur->next;
}
}
GameOver()——游戏的结束(善后工作)
善后也就是把蛇身的节点释放掉,并告诉玩家游戏结束的原因。
//结束游戏(善后工作)
void GameOver(pSnake ps)
{
SetPos(20, 13);
switch (ps->_state)
{
case KILL_BY_WALL:
printf("很遗憾!撞墙死亡!");
break;
case KILL_BY_SELF:
printf("很遗憾!撞到自己死亡!");
break;
case END_NORMAL:
printf("玩家主动结束游戏!");
break;
}
//释放蛇身链表
pSnakeNode prev = ps->_pSnake;
pSnakeNode pcur = ps->_pSnake;
while (pcur)
{
prev = pcur->next;
free(pcur);
pcur = prev;
}
}
贪吃蛇源码
Snake.h
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <locale.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 HEAD L'◆'
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
//游戏的状态
typedef enum GAME_STATE
{
OK,//正常运行
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//撞到自己
END_NORMAL//正常退出
}GAME_STATE;
//蛇的方向
typedef enum DIRECTION
{
UP = 1,//上
DOWN,//下
LEFT,//左
RIGHT//右
}DIRECTION;
//蛇身的节点类型
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
//贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物的指针
DIRECTION _dir;//蛇的方向
GAME_STATE _state;//游戏的运行状态
int _FoodWeight;//一个食物的分数
int _score;//总分数
int _SleepTime;//休眠时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
//定位光标
void SetPos(short x, short y);
//初始化游戏
void GameStart(pSnake ps);
//打印欢迎界面
void WelcomToGame();
//绘制地图
void CreatMap();
//初始化蛇
void InitSnake(pSnake ps);
//创建食物
void CreatFood(pSnake ps);
//游戏运行
void GameRun(pSnake ps);
//蛇走一步的过程
void SnakeMove(pSnake ps);
//判断蛇要走的下一个节点是否为食物
int NextIsFood(pSnakeNode next, pSnake ps);
//下一个位置是食物,就可以吃掉
void EatFood(pSnakeNode next, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakeNode next, pSnake ps);
//检测是否撞墙
void KillByWall(pSnake ps);
//检测是否撞到自己
void KillBySelf(pSnake ps);
//结束游戏(善后工作)
void GameOver(pSnake ps);
Snake.c
#include "Snake.h"
//定位光标
void SetPos(short x, short y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);//设置光标的位置
}
//打印欢迎界面
void WelcomToGame()
{
//首先得定位光标
SetPos(39, 10);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(40, 15);
system("pause");//暂停
system("cls");//清理屏幕
SetPos(28, 10);
printf("用↑.↓.←.→ 来控制蛇的移动!摁F3加速!摁F4减速!\n");
SetPos(38, 11);
printf("游戏即将开始,请做好准备!\n");
SetPos(40, 15);
system("pause");
system("cls");
}
//绘制地图
void CreatMap()
{
//打印上体墙
for (int i = 0; i <= 28; i++)
{
wprintf(L"%lc", WALL);
}
SetPos(0, 26);//定位到下体墙的位置
//打印下体墙
for (int i = 0; i <= 28; i++)
{
wprintf(L"%lc", WALL);
}
//打印左体墙
for (int i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//打印右体墙
for (int i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
//初始化蛇
void InitSnake(pSnake ps)
{
//创建一条蛇
pSnakeNode pcur = NULL;
pSnakeNode prev = NULL;
for (int i = 0; i < 5; i++)
{
pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));
if (tmp == NULL)
{
perror("InitSnake():malloc:");
return;
}
pcur = tmp;
pcur->x = POS_X + 2 * i;
pcur->y = POS_Y;
pcur->next = NULL;
if (ps->_pSnake == NULL)
{
//直接插入即可
ps->_pSnake = pcur;
}
else
{
//尾插
if (prev != NULL)
prev->next = pcur;
}
prev = pcur;
}
pcur = ps->_pSnake;
//开始在控制台上打印蛇
int count = 0;
while (pcur)
{
SetPos(pcur->x, pcur->y);//定位
if (count == 0)
{
count++;
wprintf(L"%lc", HEAD);//打印蛇头
}
else
{
wprintf(L"%lc", BODY);//打印蛇身
}
pcur = pcur->next;
}
//设置贪吃蛇的属性
ps->_dir = LEFT;//初始时蛇的方向向左
ps->_FoodWeight = 50;//一个食物50分
ps->_score = 0;//总分为0
ps->_SleepTime = 200;//单位是毫秒
ps->_state = OK;//正常运行
}
//创建食物
void CreatFood(pSnake ps)
{
int x = 0;
int y = 0;
//随机创建食物(食物的x坐标必须是2的倍数,因此要判断)
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2);
//食物的坐标不能和蛇身冲突
pSnakeNode pcur = ps->_pSnake;
//开始寻找看看是否与蛇身冲突
while (pcur)
{
if (pcur->x == x && pcur->y == y)
{
goto again;
}
pcur = pcur->next;
}
//开始创建食物的节点
pSnakeNode tmp = (pSnakeNode)malloc(sizeof(SnakeNode));
if (tmp == NULL)
{
perror("CreatFood():malloc:");
return;
}
tmp->x = x;
tmp->y = y;
tmp->next = NULL;
ps->_pFood = tmp;
//打印食物
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
//初始化游戏
void GameStart(pSnake ps)
{
//设置窗口大小以及名字
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏光标信息,为了后续打印
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);//获取标准输出的句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(houtput, &CursorInfo);//获取光标信息
CursorInfo.bVisible = false;//隐藏光标
SetConsoleCursorInfo(houtput, &CursorInfo);//设置光标状态
//打印欢迎界面和功能介绍
WelcomToGame();
//绘制地图
CreatMap();
//初始化蛇
InitSnake(ps);
//创建食物
CreatFood(ps);
}
//打印帮助信息
void PrintHelpInfo()
{
SetPos(68, 10);
printf("小提示:");
SetPos(68, 13);
printf("不能穿墙,不能咬到自己!");
SetPos(68, 14);
printf("用↑.↓.←.→ 来控制蛇的移动!");
SetPos(68, 15);
printf("摁F3加速!摁F4减速!");
SetPos(68, 16);
printf("加速将增加单个食物的分数!");
SetPos(68, 17);
printf("减速将减少单个食物的分数!");
SetPos(68, 18);
printf("摁Esc退出游戏!摁空格暂停游戏!");
}
void Pause()
{
while (1)
{
//休眠200毫秒
Sleep(200);
//这个只能放到休眠的后面
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//判断蛇要走的下一个节点是否为食物
int NextIsFood(pSnakeNode next, pSnake ps)
{
return ((next->x == ps->_pFood->x) && (next->y == ps->_pFood->y));
}
//下一个位置是食物,就可以吃掉
void EatFood(pSnakeNode next, pSnake ps)
{
//把这个节点(就是食物节点)头插到蛇身就行
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放掉这个节点(因为有两个节点)
free(next);
next = NULL;
//打印蛇身
pSnakeNode pcur = ps->_pSnake;
int count = 0;
while (pcur)
{
SetPos(pcur->x, pcur->y);//定位
if (count == 0)
{
count++;
wprintf(L"%lc", HEAD);//打印蛇头
}
else
{
wprintf(L"%lc", BODY);//打印蛇身
}
pcur = pcur->next;
}
ps->_score += ps->_FoodWeight;
//重新创建食物
CreatFood(ps);
}
//下一个位置不是食物
void NoFood(pSnakeNode next, pSnake ps)
{
//把下一个位置的节点头插到蛇身
next->next = ps->_pSnake;
ps->_pSnake = next;
//把蛇身最后一个节点的空间释放掉,顺便打印蛇身
pSnakeNode pcur = ps->_pSnake;
int count = 0;
while (pcur->next->next != NULL)
{
//遍历时,可以直接打印
SetPos(pcur->x, pcur->y);
if (count == 0)
{
count++;
wprintf(L"%lc", HEAD);
}
else
{
wprintf(L"%lc", BODY);
}
pcur = pcur->next;
}
//把最后一个节点的位置打印成空格
SetPos(pcur->next->x, pcur->next->y);
printf(" ");
free(pcur->next);
pcur->next = NULL;
}
//蛇走一步的过程
void SnakeMove(pSnake ps)
{
//创建一个节点来存放蛇要走的下一个节点
pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));
if (next == NULL)
{
perror("SnakeMove():malloc:");
return;
}
//根据方向来判断蛇是怎么走的
switch (ps->_dir)
{
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 (NextIsFood(next, ps))
{
EatFood(next, ps);
}
else
{
NoFood(next, ps);
}
}
//检测是否撞墙
void KillByWall(pSnake ps)
{
//只要判断蛇头是否碰到墙就可以了
if (ps->_pSnake->x == 0 || ps->_pSnake->x == 56
|| ps->_pSnake->y == 0 || ps->_pSnake->y == 26)
{
ps->_state = KILL_BY_WALL;
}
}
//检测是否撞到自己
void KillBySelf(pSnake ps)
{
//只要判断蛇头是否碰到自己的蛇身
pSnakeNode pcur = ps->_pSnake->next;
while (pcur)
{
if (pcur->x == ps->_pSnake->x && pcur->y == ps->_pSnake->y)
{
ps->_state = KILL_BY_SELF;
break;
}
pcur = pcur->next;
}
}
//游戏运行
void GameRun(pSnake ps)
{
//先打印帮助信息
PrintHelpInfo();
//接下来就是通过按键来判断贪吃蛇的运行状态
do
{
//打印分数显示
SetPos(68, 7);
printf("当前总分数:%05d", ps->_score);
SetPos(68, 8);
printf("当前食物分数:%02d", 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->_state = END_NORMAL;
}
else if (KEY_PRESS(VK_F3))
{
//加速(减少休眠时间)
if (ps->_SleepTime > 80)//设置为4档速度
{
ps->_SleepTime -= 30;
ps->_FoodWeight += 10;
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_FoodWeight > 10)
{
ps->_SleepTime += 20;
ps->_FoodWeight -= 10;
}
}
//蛇开始走
//走一步,就休息一下
SnakeMove(ps);//蛇走一步的过程
Sleep(ps->_SleepTime);
//检测是否撞墙
KillByWall(ps);
//检测是否撞到自己
KillBySelf(ps);
if (ps->_score > 30000)
{
SetPos(20, 13);
printf("恭喜你!成功通关!");
break;
}
} while (ps->_state == OK);
}
//结束游戏(善后工作)
void GameOver(pSnake ps)
{
SetPos(20, 13);
switch (ps->_state)
{
case KILL_BY_WALL:
printf("很遗憾!撞墙死亡!");
break;
case KILL_BY_SELF:
printf("很遗憾!撞到自己死亡!");
break;
case END_NORMAL:
printf("玩家主动结束游戏!");
break;
}
//释放蛇身链表
pSnakeNode prev = ps->_pSnake;
pSnakeNode pcur = ps->_pSnake;
while (pcur)
{
prev = pcur->next;
free(pcur);
pcur = prev;
}
}
test.c
#include "Snake.h"
void test()
{
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1.游戏相关界面的打印
//2.贪吃蛇与食物的打印
GameStart(&snake);
//游戏运行
GameRun(&snake);
//结束游戏(善后工作)
GameOver(&snake);
}
int main()
{
//适配本地化环境
setlocale(LC_ALL, "");
//生成随机种子
srand((unsigned int)time(NULL));
//测试贪吃蛇
test();
//为了格式更好看
SetPos(0, 27);
return 0;
}
好啦!本期贪吃蛇游戏的学习之旅到此结束了!我们下一期再一起学习吧!