C语言(万字讲解,帮你再现经典游戏) | 贪吃蛇 —— 附完整代码可以直接copy运行游玩
DjangoJason 2024-07-22 17:05:03 阅读 53
贪吃蛇
引言
在计算机科学的世界里,经典游戏是无法被忽视的一部分。它们不仅是我们对计算机编程技术进行探索和学习的重要工具,也是我们与计算机交互的一种有趣方式。其中,贪吃蛇游戏(Snake Game)无疑是这些经典游戏中的一颗明珠,它简单而充满挑战,一直以来都备受欢迎。
贪吃蛇游戏的核心玩法简单而经典:控制一条蛇在有限的空间内移动,吃掉食物,不断成长,但要避免撞到墙壁或者自己的身体。尽管规则简单,但这个游戏却蕴含了丰富的编程技术和算法挑战。从基本的用户输入处理,到数据结构的应用,再到图形界面的展示,贪吃蛇游戏是一个极好的项目,让我们有机会探索和实践各种计算机科学的基础概念。
在本文中,我们将深入探讨如何使用C语言来实现贪吃蛇游戏。C语言作为一种高效而强大的编程语言,是实现游戏的理想选择。通过这个项目,我们不仅可以加深对C语言的理解,还能够锻炼自己的逻辑思维和问题解决能力。
在接下来的内容中,我们将逐步介绍贪吃蛇游戏的实现过程,从所需要的基本知识到游戏逻辑再到用户界面的设计,带领读者一步步走进这个有趣而充满挑战的编程世界。让我们一起开始这段奇妙的编程之旅吧!
目录
贪吃蛇引言1. 游戏效果及基本功能2. 技术要点2.0 随机数的生成2.0.1 rand2.0.2 srand2.0.3 time2.0.4 设置随机数的范围
2.1 Win32 API2.2 控制台程序2.3 控制台屏幕上的坐标 `COORD`2.4 GetStdHandle2.5 GetConsoleCursorInfo2.6 CONSOLE_CURSOR_INFO2.7 SetConsoleCursorInfo2.8 SetConsoleCursorPosition2.9 GetAsyncKeyState
3. 贪吃蛇游戏设计和分析3.1 地图设计3.1.1 `<locale.h>`本地化3.1.2 类项3.1.3 setlocale函数3.1.4 宽字符的打印3.1.5 地图坐标
3.2 蛇身和食物3.3 数据结构设计3.4 游戏流程设计
4. 核心逻辑实现分析4.1 游戏主逻辑4.2 游戏开始(GameStart)4.2.1 打印欢迎界面4.2.2 创建地图4.2.3 初始化蛇身4.2.4 创建第一个食物
4.3 游戏运行(GameRun)4.3.1 蛇身移动`SnakeMove`4.3.2 `NextIsFood`4.3.3 `EatFood`4.3.4 `NoFood`4.3.5 `KillByWall`4.3.6`KillBySelf`
4.4 游戏结束(GameEnd)
5. 参考代码
1. 游戏效果及基本功能
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
游戏画面展示:
需要实现的基本功能:
贪吃蛇地图控制蛇吃食物的功能(上,下,左,右方向键控制蛇的动作)蛇撞墙死亡蛇撞自身死亡计算得分蛇身加速、减速暂停游戏
2. 技术要点
要想实现贪吃蛇游戏首先我们需要掌握:
C语言随机数的生成、函数、枚举、 结构体、动态内存管理、预处理指令、链表、Win32API 等。
这些知识点在我的前几期博客都有介绍。
2.0 随机数的生成
2.0.1 rand
C语言提供了一个函数叫 <code>rand,这函数是可以生成随机数,函数原型如下所示:
int rand (void);
rand
函数会返回一个伪随机数,这个随机数的范围是在0~RAND_MAX
之间,这个RAND_MAX
的大小是
依赖编译器上实现的,但是大部分编译器上是32767
。
rand函数的使用需要包含一个头文件是:stdlib.h
我们现在通过一个示例来了解一下什么是伪随机数。
示例
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
return 0;
}
运行两次之后,看到结果我们会发现,两次产生的随机数序列竟然是一样的,所以说伪随机数不是真正的随机数,是通过某种算法生成的随机数。真正的随机数的是无法预测下一个值是多少的。而rand函数是对一个叫“种子”的基准值进行运算生成的随机数。
之所以前面每次运行程序产生的随机数序列是一样的,那是因为rand函数⽣成随机数的默认种子是1。
如果要生成不同的随机数,就要让种子是变化的。
2.0.2 srand
C语言中又提供了一个函数叫 srand,用来初始化随机数的生成器的,srand的原型如下:
<code>void srand (unsigned int seed);
srand
函数是不需要频繁调用的,一次运行的程序中调用一次就够了。
程序中在调用rand
函数之前先调用 srand
函数,通过 srand 函数的参数seed
来设置rand
函数生成随机数的时候的种子,只要种子在变化,每次生成的随机数序列也就变化起来了。
因此,我们接下来介绍一个函数time
来帮助我们实现“种子随机”。
2.0.3 time
在程序中我们一般是使用程序运行的时间作为种子,因为时间时刻在发生变化的。
在C语言中有一个函数叫 time
,就可以获得这个时间,time
函数原型如下:
time_t time (time_t* timer);
time 函数会返回当前的日历时间,其实返回的是1970年1月1日0时0分0秒
到现在程序运行时间之间的差值,单位是秒。返回的类型是time_t
类型的,time_t
类型本质上其实就是32位或者64位的整型类型。
time
函数的参数 timer
如果是非NULL
指针的话,函数也会将这个返回的差值放在timer
指向的内存中带回去。
如果 timer
是NULL
,就只返回这个时间的差值。time
函数返回的这个时间差也被叫做:时间戳。
time
函数的时候需要包含头文件:time.h
2.0.4 设置随机数的范围
如果要生成a~b
的随机数,方法如下:
a + rand()%(b-a+1)
接下来介绍实现贪吃蛇会用到的一些Win32 API知识
2.1 Win32 API
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是⼀个很大的服务中心,调用这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows
32位平台的应用程序编程接口。
2.2 控制台程序
平常我们运行起来的黑框程序其实就是控制台程序。
如果大家显示的是终端,可以通过这样设置来打开控制台程序。
只有这样才能正常运行贪吃蛇游戏!
我们可以使用cmd命令来设置控制台窗口的长宽
示例 :设置控制台窗口的大小,30行,100列。
<code>mode con cols=100 lines=30
参考:mode指令
也可以通过命令设置控制台窗口的名字
示例 :命名为贪吃蛇
title 贪吃蛇
参考: title命令
这样能在控制台窗口执行的命令,也可以调用C语言函数system来执行。
示例
<code>#include<stdio.h>
int main()
{
//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
system("mode con cols=100 lines=30");
//设置cmd窗⼝名称
system("title 贪吃蛇");
return 0;
}
2.3 控制台屏幕上的坐标 COORD
COORD
是Windows API中定义的一个结构体,表示一个字符在控制台屏幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
COORD类型的声明
<code>typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
给坐标复制
COORD pos = { 10, 15 };
2.4 GetStdHandle
GetStdHandle
是一个Windows API
函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),用户使用这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);
示例
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
STD_OUTPUT_HANDLE
是一个预定义的常量,表示标准输出流。通过调用 GetStdHandle(STD_OUTPUT_HANDLE)
,就可以获取到标准输出流的句柄,并将其赋值给变量 hOutput
,以便后续在程序中使用。
2.5 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.6 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize
:由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。bVisible
:游标的可见性 如果光标可见,则此成员为TRUE
。
CursorInfo.bVisible = false; //隐藏控制台光标
2.7 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
示例
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);// 获得标准输出设备的句柄
//隐藏光标操作
CONSOLE_CURSOR_INFO CursorInfo; //定义了一个光标信息的结构体变量,名为CursorInfo
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取和hOutpot句柄相关的控制台上的光标信息,并存放在CursorInfo
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置和hOutpot句柄相关的控制台上的光标状态
2.8 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD
类型的pos
中,调用SetConsoleCursorPosition
函数将光标位置设置到指定的位置。
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
示例
COORD pos = {10 , 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
SetPos
:封装一个设置光标位置的函数。
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
2.9 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
我们可以参考键码表输入参数来使用GetAsyncKeyState
函数判断键是否被按下
示例:检测数字键
#include <stdio.h>
#include <windows.h>
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");
}
else if (KEY_PRESS(0x35))
{
printf("5\n");
}
else if (KEY_PRESS(0x36))
{
printf("6\n");
}
else if (KEY_PRESS(0x37))
{
printf("7\n");
}
else if (KEY_PRESS(0x38))
{
printf("8\n");
}
else if (KEY_PRESS(0x39))
{
printf("9\n");
}
}
return 0;
}
3. 贪吃蛇游戏设计和分析
3.1 地图设计
如果我们要将地图设计成这样,就需要讲一下控制台窗口的知识了,如果想在控制台窗口中的指定位置输出信息,我们得知道这个位置的坐标,所以首先我们要介绍一下控制台窗口的坐标知识。
控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★ 。
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单的讲⼀下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。
C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为<code>0xxxxxxxx;可以看到,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
:影响字符串比较函数 strcoll()
和 strxfrm()
。LC_CTYPE
:影响字符处理函数的行为。LC_MONETARY
:影响货币格式。LC_NUMERIC
:影响 printf()
的数字格式。LC_TIME
:影响时间格式 strftime()
和 wcsftime()
。LC_ALL
:针对所有类项修改,将以上所有类别设置为给定的语言环境。
参考: 每个类项的详细说明
3.1.3 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, " ");//切换到本地环境
3.1.4 宽字符的打印
打印宽字符时,宽字符的字面量必须加上前缀“L”
,否则 C 语言会把字面量当作窄字符类型处理。前缀“L”
在单引号前面,表示宽字符,对应wprintf()
的占位符为 %lc
;在双引号前面,表示宽字符串,对应wprintf()
的占位符为%ls
。
#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列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙。
如下:
3.2 蛇身和食物
初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24, 5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半儿出现在墙体中,另外一般在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标 (x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。
3.3 数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
<code>//snake.h文件
//蛇身的节点类型
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode; //对于这个结构体变量定义一个指针变量pSnakeNode
//typedef struct SnakeNode* pSnakeNode; //这样写也可以
要管理整条贪吃蛇,我们再封装一下Snake的结构来维护整条贪吃蛇。
//snake.h文件
//贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物节点的指针
enum DIRECTION _dir;//蛇的方向,默认向右
enum GAME_STATUS _status;//游戏的状态
int _food_weight;//一个食物的分数
int _score; //总成绩
int _sleep_time; //每一步的休眠时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
蛇的方向可以使用枚举来一一列举。
//snake.h文件
//蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
游戏状态也是通过枚举来一一列举。
//snake.h文件
//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
OK, //正常
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞到自己
END_NORMAL //正常退出
};
3.4 游戏流程设计
4. 核心逻辑实现分析
4.1 游戏主逻辑
程序开始就设置程序支持本模式,然后进入游戏的主逻辑。
主逻辑一共分为3个过程:
游戏开始(GameStart)完成游戏的初始化游戏运行(GameRun)完成游戏运行逻辑的实现游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
<code>//test.c 文件
#include<locale.h> //本地化头文件
void test()
{
int ch = 0;
do
{
system("cls"); //清空窗口
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1. 打印环境界面
//2. 功能介绍
//3. 绘制地图
//4. 创建蛇
//5. 创建食物
//6. 设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//检测是否有按键被按下—— 因为控制台的本身机制,如果在游玩时按上键,控制台会把历史命令显示出来。因此使用系统调用函数来拿走按键
while (_kbhit())
{
// 使用 _getch() 获取按下的键,不阻塞程序
_getch();
// 处理按键事件,可以根据需要进行相应的操作
}
//结束游戏 - 善后工作
GameEnd(&snake);
//打印是否进入下一局的引导语
SetPos(20, 15);//定位
printf("再来一局吗?(Y/N):");
ch = getchar(); //获取玩家输入
while (getchar() != '\n'); //防止输入过多无用字符导致程序崩溃
} while (ch=='Y' || ch=='y');
SetPos(0, 27);//将进程结束提示至于窗口最下方
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL)); //只需初始化一次的随机种子用于随机数
test();
return 0;
}
4.2 游戏开始(GameStart)
这个模块完成游戏的初始化任务:
控制台窗口大小的设置控制台窗口名字的设置鼠标光标的隐藏打印欢迎界面创建地图初始化蛇身创建第一个食物
//snake.c 文件
void GameStart(pSnake ps)
{
//0. 先设置窗口的大小,再光标隐藏
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);//设置控制台光标状态
//1. 打印环境界面和功能介绍
WelcomeToGame();
//2. 绘制地图
CreateMap();
//3. 创建蛇
InitSnake(ps);
//4. 创建食物
CreateFood(ps);
}
4.2.1 打印欢迎界面
在游戏正式开始之前,做一些功能提醒。
首先为我们的界面创建做些准备——创建一个位置函数
使用这个函数,我们就可以在控制台窗口的任意位置放入我们想要插入的信息。
//snake.c文件
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
然后在指定位置插入我们的提示信息
//snake.c文件
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(42, 20);
system("pause"); //按任意键继续...... 按下后会切换页面
system("cls"); //清空上一页面的窗口信息
SetPos(25, 14);
wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause"); //按任意键继续...... 按下后会切换页面
system("cls");//清空上一页面的窗口信息
}
4.2.2 创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用<code>wprintf函数,打印格式串前使用L
。打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
墙体打印宽字符:
//snake.h文件
//在这里我们直接将常用的几个宽字符常量进行定义
#define WALL L'□'
坐标的计算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
创建地图函数CreateMap
//snake.c 文件
void CreateMap()
{
//上
int i = 0;
SetPos(0, 0);//上(0,0)-(56, 0)
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);//下(0,26)-(56, 26)
for (i = 0; i < 29; i++)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);//x是0,y从1开始增⻓
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);//x是56,y从1开始增⻓
wprintf(L"%lc", WALL);
}
}
4.2.3 初始化蛇身
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每⼀节打印在屏幕上。
蛇的初始位置从 (24,5) 开始。
再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。游戏状态是:OK蛇的移动速度:200毫秒蛇的默认方向:RIGHT初始成绩:0每个食物的分数:10
蛇身打印的宽字符:
<code>//snake.h文件
//在这里我们直接将常用的几个宽字符常量进行定义
#define BODY L'●'
初始化蛇身函数:InitSnake
//snake.c 文件
void InitSnake(pSnake ps) //初始化蛇身需要将蛇的结构指针传给函数
{
int i = 0;
pSnakeNode cur = NULL;
//创建蛇⾝节点,并初始化坐标
//头插法
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //为蛇身节点申请空间
if (cur == NULL)//防止空间申请失败
{
perror("InitSnake()::malloc()");
return;
}
//给节点进行坐标定位
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插法插入链表
if (ps->_pSnake == NULL) //空链表
{
ps->_pSnake = cur;
}
else //非空
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
//打印贪吃蛇
cur = ps->_pSnake; //将蛇头地址给cur
while (cur)
{
SetPos(cur->x, cur->y);//给定位置
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置贪吃蛇的属性
ps->_dir = RIGHT;//默认向右
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位是毫秒,休眠时间是蛇每走一步中间间隔的时间
ps->_status = OK;
}
4.2.4 创建第一个食物
先随机生成食物的坐标
◦ x坐标必须是2的倍数
◦ 食物的坐标不能和蛇身每个节点的坐标重复创建食物节点,打印食物
食物打印的宽字符:
//snake.h文件
//在这里我们直接将常用的几个宽字符常量进行定义
#define FOOD L'★'
创建食物的函数:CreateFood
//snake.c 文件
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//生成x坐标需是2的倍数
//x:2~54
//y: 1~25
again:
do
{
x = rand() % 53 + 2; //根据合适的坐标范围随机生成食物坐标
y = rand() % 25 + 1;
} while (x % 2 != 0);//判断x坐标是否是2的倍数,不是就进入循环重新生成坐标
//x和y的坐标不能和蛇的身体坐标冲突
pSnakeNode cur = ps->_pSnake;//将指向蛇头的指针赋给cur
while (cur)
{
if (x == cur->x && y == cur->y)//判断是否和身体坐标发生冲突
{
goto again;//如果冲突,就返回到again标点重新生成坐标
}
cur = cur->next;
}//到这里随机生成的食物坐标就通过检查了
//创建食物的节点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //为食物节点申请空间
if (pFood == NULL)//防止空间申请失败
{
perror("CreateFood()::malloc()");
return;
}
//将生成的坐标赋给食物节点
pFood->x = x;
pFood->y = y;
pFood->next = NULL; //让食物节点的next指向空,达到食物节点独立。
SetPos(x, y);//定位位置
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;//将食物信息赋给食物指针
}
4.3 游戏运行(GameRun)
游戏运行期间,右侧打印帮助信息提示玩家,坐标(64, 15)。
PrintfHelpInfo
//snake.c 文件
void PrintHelpInfo()
{
SetPos(64, 5);
wprintf(L"%ls", L"按空格开始游戏!");
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
SetPos(64, 18);
wprintf(L"%ls", L"版权归Jason所有");
}
根据游戏状态检查游戏是否继续,如果状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
需要的虚拟按键的罗列:
上:<code>VK_UP下:VK_DOWN
左:VK_LEFT
右:VK_RIGHT
空格:VK_SPACE
ESC:VK_ESCAPE
F3:VK_F3
F4:VK_F4
确定了蛇的方向和速度,蛇就可以移动了。
//snake.c文件
void GameRun(pSnake ps)
{
//在窗口右侧打印帮助信息
PrintHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 11);
printf("当前食物的分数:%2d\n", ps->_food_weight);
//检测按键是否被按下,以及防止按键冲突
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
{
ps->_dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
{
ps->_dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
{
ps->_dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
{
ps->_dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
Pause(); //暂停函数
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL; //将游戏状态设为正常退出
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80) //当休眠时间被缩短到80ms时,速度已经足够快了
{
ps->_sleep_time -= 30; //减少休眠时间
ps->_food_weight += 2; //增加食物的分数,最高分是20分
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_food_weight > 2) //食物分数已经降到了最低标准
{
ps->_sleep_time += 30; //增加休眠时间
ps->_food_weight -= 2; //减少食物的分数
}
}
SnakeMove(ps);//蛇走一步的过程的函数
Sleep(ps->_sleep_time); //蛇每次到达一定状态,就要进行休眠
} while (ps->_status==OK); //只有游戏状态是ok,才会再次进入循环重复进行检测,使游戏进行下去。
}
封装宏来检测按键状态——低位为1为按下,0为未按下
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
暂停函数Pause
//snake.c文件
void Pause()
{
while (1)
{
Sleep(200);//暂停200毫秒
if (KEY_PRESS(VK_SPACE))//如果没有按下空格,则会反复暂停200毫秒
{
break;
}
}
}
4.3.1 蛇身移动SnakeMove
先创建下一个节点,根据移动方向和蛇头的坐标,确定了下一个位置后,看下一个位置是否是食物(NextIsFood),是食物就做吃食物处理(EatFood),如果不是食物则做前进一步的处理(NoFood)。
蛇身移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上自己蛇身(KillBySelf),从而影响游戏的状态。
//snake.c文件
void SnakeMove(pSnake ps)
{
//创建一个结点,表示蛇即将到的下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
//确定下⼀个节点的坐标,下⼀个节点的坐标根据蛇头的坐标和方向确定
switch (ps->_dir)//选择方向
{
case UP:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
break;
case DOWN:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
break;
case LEFT:
pNextNode->x = ps->_pSnake->x-2;
pNextNode->y = ps->_pSnake->y;
break;
case RIGHT:
pNextNode->x = ps->_pSnake->x+2;
pNextNode->y = ps->_pSnake->y;
break;
}
//检测下一个坐标处是否是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
4.3.2 NextIsFood
//snake.c 文件
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 贪吃蛇的指针
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
4.3.3 EatFood
//snake.c 文件
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 贪吃蛇的指针
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight; //增加食物获得总分
//重新创建食物
CreateFood(ps);
}
4.3.4 NoFood
将下一个节点头插入蛇的身体,并将之前蛇身最后一个节点打印为空格(遮盖之前的蛇尾,要不然蛇走过的会形成轨迹),释放掉蛇身的最后一个节点。
易错点:这里最容易错误的是,释放最后一个结点后,还得将指向在最后一个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。
//snake.c 文件
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->_pSnake;
ps->_pSnake = pn;
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//把最后一个结点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个结点
free(cur->next);
//把倒数第二个节点的地址置为NULL
cur->next = NULL;
}
4.3.5 KillByWall
判断蛇头的坐标是否和墙的坐标发生冲突。
//snake.c 文件
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; //将游戏状态改为:撞墙死
}
}
4.3.6<code>KillBySelf
//snake.c 文件
void KillBySelf(pSnake ps) //此函数和上边的撞墙函数同理
{
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next; //通过函数遍历蛇身所有节点来检测蛇头是否撞到蛇身
}
}
4.4 游戏结束(GameEnd)
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
<code>//snake.c 文件
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->_status) //根据最后游戏结束的不同状态打印信息
{
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
wprintf(L"您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
5. 参考代码
snake.h 文件
#pragma once
#include <windows.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//类型的声明
//蛇的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
//蛇的状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATUS
{
OK, //正常
KILL_BY_WALL, //撞墙
KILL_BY_SELF, //撞到自己
END_NORMAL //正常退出
};
//蛇身的节点类型
typedef struct SnakeNode
{
//坐标
int x;
int y;
//指向下一个节点的指针
struct SnakeNode* next;
}SnakeNode, * pSnakeNode; //对于这个结构体变量定义一个指针变量pSnakeNode
//typedef struct SnakeNode* pSnakeNode; //这样写也可以
//贪吃蛇
typedef struct Snake
{
pSnakeNode _pSnake;//指向蛇头的指针
pSnakeNode _pFood;//指向食物节点的指针
enum DIRECTION _dir;//蛇的方向,默认向右
enum GAME_STATUS _status;//游戏的状态
int _food_weight;//一个食物的分数
int _score; //总成绩
int _sleep_time; //每一步的休眠时间,时间越短,速度越快,时间越长,速度越慢
}Snake, * pSnake;
//函数的声明
//定位光标位置
void SetPos(short x, short y);
//游戏的初始化
void GameStart(pSnake ps);
//欢迎界面的打印
void WelcomeToGame();
//创建地图
void CreateMap();
void CreateMap();
//初始化蛇身
void InitSnake(pSnake ps);
//创建食物
void CreateFood(pSnake ps);
//游戏运行的逻辑
void GameRun(pSnake ps);
//蛇的移动-走一步
void SnakeMove(pSnake ps);
//判断下一个坐标是否是食物
int NextIsFood(pSnakeNode pn, pSnake ps);
//下一个位置是食物,就吃掉食物
void EatFood(pSnakeNode pn, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps);
//检测蛇是否撞墙
void KillByWall(pSnake ps);
//检测蛇是否撞到自己
void KillBySelf(pSnake ps);
//游戏善后的工作
void GameEnd(pSnake ps);
snake.c 文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"
//GAMESTART
//控制台窗口大小的设置
//控制台窗口名字的设置
//鼠标光标的隐藏
//打印欢迎界面
//创建地图
//初始化蛇身
//创建第一个食物
void GameStart(pSnake ps)
{
//0. 先设置窗口的大小,再光标隐藏
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);//设置控制台光标状态
//1. 打印环境界面和功能介绍
WelcomeToGame();
//2. 绘制地图
CreateMap();
//3. 创建蛇
InitSnake(ps);
//4. 创建食物
CreateFood(ps);
}
//位置函数
void SetPos(short x, short y)
{
//获得标准输出设备的句柄
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
//定位光标的位置
COORD pos = { x, y };
SetConsoleCursorPosition(houtput, pos);
}
//1. 打印环境界面和功能介绍
void WelcomeToGame()
{
SetPos(40, 14);
wprintf(L"欢迎来到贪吃蛇小游戏\n");
SetPos(42, 20);
system("pause"); //按任意键继续...... 按下后会切换页面
system("cls"); //清空上一页面的窗口信息
SetPos(25, 14);
wprintf(L"用 ↑. ↓ . ← . → 来控制蛇的移动,按F3加速,F4减速\n");
SetPos(25, 15);
wprintf(L"加速能够得到更高的分数\n");
SetPos(42, 20);
system("pause"); //按任意键继续...... 按下后会切换页面
system("cls");//清空上一页面的窗口信息
}
//2. 绘制地图
void CreateMap()
{
//上
int i = 0;
SetPos(0, 0);//上(0,0)-(56, 0)
for (i = 0; i < 58; i+= 2)
{
wprintf(L"%lc", WALL);
}
//下
SetPos(0, 26);//下(0,26)-(56, 26)
for (i = 0; i < 58; i+=2)
{
wprintf(L"%lc", WALL);
}
//左
for (i = 1; i <= 25; i++)
{
SetPos(0, i);//x是0,y从1开始增⻓
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i <= 25; i++)
{
SetPos(56, i);//x是56,y从1开始增⻓
wprintf(L"%lc", WALL);
}
}
//3. 创建蛇
void InitSnake(pSnake ps) //初始化蛇身需要将蛇的结构指针传给函数
{
int i = 0;
pSnakeNode cur = NULL;
//创建蛇⾝节点,并初始化坐标
//头插法
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode)); //为蛇身节点申请空间
if (cur == NULL)//防止空间申请失败
{
perror("InitSnake()::malloc()");
return;
}
//给节点进行坐标定位
cur->next = NULL;
cur->x = POS_X + 2 * i;
cur->y = POS_Y;
//头插法插入链表
if (ps->_pSnake == NULL) //空链表
{
ps->_pSnake = cur;
}
else //非空
{
cur->next = ps->_pSnake;
ps->_pSnake = cur;
}
}
//打印贪吃蛇
cur = ps->_pSnake; //将蛇头地址给cur
while (cur)
{
SetPos(cur->x, cur->y);//给定位置
wprintf(L"%lc", BODY);
cur = cur->next;
}
//设置贪吃蛇的属性
ps->_dir = RIGHT;//默认向右
ps->_score = 0;
ps->_food_weight = 10;
ps->_sleep_time = 200;//单位是毫秒,休眠时间是蛇每走一步中间间隔的时间
ps->_status = OK;
}
//4. 创建食物
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
//生成x坐标需是2的倍数
//x:2~54
//y: 1~25
again:
do
{
x = rand() % 53 + 2; //根据合适的坐标范围随机生成食物坐标
y = rand() % 25 + 1;
} while (x % 2 != 0);//判断x坐标是否是2的倍数,不是就进入循环重新生成坐标
//x和y的坐标不能和蛇的身体坐标冲突
pSnakeNode cur = ps->_pSnake;//将指向蛇头的指针赋给cur
while (cur)
{
if (x == cur->x && y == cur->y)//判断是否和身体坐标发生冲突
{
goto again;//如果冲突,就返回到again标点重新生成坐标
}
cur = cur->next;
}//到这里随机生成的食物坐标就通过检查了
//创建食物的节点
pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //为食物节点申请空间
if (pFood == NULL)//防止空间申请失败
{
perror("CreateFood()::malloc()");
return;
}
//将生成的坐标赋给食物节点
pFood->x = x;
pFood->y = y;
pFood->next = NULL; //让食物节点的next指向空,达到食物节点独立。
SetPos(x, y);//定位位置
wprintf(L"%lc", FOOD);
ps->_pFood = pFood;//将食物信息赋给食物指针
}
//GAMERUN
//打印帮助信息
void PrintHelpInfo()
{
SetPos(64, 5);
wprintf(L"%ls", L"按空格开始游戏!");
SetPos(64, 14);
wprintf(L"%ls", L"不能穿墙,不能咬到自己");
SetPos(64, 15);
wprintf(L"%ls", L"用 ↑. ↓ . ← . → 来控制蛇的移动");
SetPos(64, 16);
wprintf(L"%ls", L"按F3加速,F4减速");
SetPos(64, 17);
wprintf(L"%ls", L"按ESC退出游戏,按空格暂停游戏");
SetPos(64, 18);
wprintf(L"%ls", L"版权归Jason所有");
}
//封装宏来检测按键状态——低位为1为按下,0为未按下
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
//暂停函数
void Pause()
{
while (1)
{
Sleep(200);//暂停200毫秒
if (KEY_PRESS(VK_SPACE))//如果没有按下空格,则会反复暂停200毫秒
{
break;
}
}
}
//游戏运行
void GameRun(pSnake ps)
{
//在窗口右侧打印帮助信息
PrintHelpInfo();
do
{
//打印总分数和食物的分值
SetPos(64, 10);
printf("总分数:%d\n", ps->_score);
SetPos(64, 11);
printf("当前食物的分数:%2d\n", ps->_food_weight);
//检测按键是否被按下,以及防止按键冲突
if (KEY_PRESS(VK_UP) && ps->_dir != DOWN)
{
ps->_dir = UP;
}
else if (KEY_PRESS(VK_DOWN) && ps->_dir != UP)
{
ps->_dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) && ps->_dir != RIGHT)
{
ps->_dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) && ps->_dir != LEFT)
{
ps->_dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE))
{
Pause(); //暂停函数
}
else if (KEY_PRESS(VK_ESCAPE))
{
//正常退出游戏
ps->_status = END_NORMAL; //将游戏状态设为正常退出
}
else if (KEY_PRESS(VK_F3))
{
//加速
if (ps->_sleep_time > 80) //当休眠时间被缩短到80ms时,速度已经足够快了
{
ps->_sleep_time -= 30; //减少休眠时间
ps->_food_weight += 2; //增加食物的分数,最高分是20分
}
}
else if (KEY_PRESS(VK_F4))
{
//减速
if (ps->_food_weight > 2) //食物分数已经降到了最低标准
{
ps->_sleep_time += 30; //增加休眠时间
ps->_food_weight -= 2; //减少食物的分数
}
}
SnakeMove(ps);//蛇走一步的过程的函数
Sleep(ps->_sleep_time); //蛇每次到达一定状态,就要进行休眠
} while (ps->_status == OK); //只有游戏状态是ok,才会再次进入循环重复进行检测,使游戏进行下去。
}
//蛇身运动检测
void SnakeMove(pSnake ps)
{
//创建一个结点,表示蛇即将到的下一个节点
pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNextNode == NULL)
{
perror("SnakeMove()::malloc()");
return;
}
//确定下⼀个节点的坐标,下⼀个节点的坐标根据蛇头的坐标和方向确定
switch (ps->_dir)//选择方向
{
case UP:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y - 1;
break;
case DOWN:
pNextNode->x = ps->_pSnake->x;
pNextNode->y = ps->_pSnake->y + 1;
break;
case LEFT:
pNextNode->x = ps->_pSnake->x - 2;
pNextNode->y = ps->_pSnake->y;
break;
case RIGHT:
pNextNode->x = ps->_pSnake->x + 2;
pNextNode->y = ps->_pSnake->y;
break;
}
//检测下一个坐标处是否是食物
if (NextIsFood(pNextNode, ps))
{
EatFood(pNextNode, ps);
}
else
{
NoFood(pNextNode, ps);
}
//检测蛇是否撞墙
KillByWall(ps);
//检测蛇是否撞到自己
KillBySelf(ps);
}
//判断下一个坐标是否是食物
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 贪吃蛇的指针
int NextIsFood(pSnakeNode pn, pSnake ps)
{
return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);
}
//下一个位置是食物,就吃掉食物
//pSnakeNode pn 是下⼀个节点的地址
//pSnake ps 贪吃蛇的指针
void EatFood(pSnakeNode pn, pSnake ps)
{
//头插法
ps->_pFood->next = ps->_pSnake;
ps->_pSnake = ps->_pFood;
//释放下一个位置的节点
free(pn);
pn = NULL;
pSnakeNode cur = ps->_pSnake;
//打印蛇
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->_score += ps->_food_weight; //增加食物获得总分
//重新创建食物
CreateFood(ps);
}
//下一个位置不是食物
void NoFood(pSnakeNode pn, pSnake ps)
{
//头插法
pn->next = ps->_pSnake;
ps->_pSnake = pn;
pSnakeNode cur = ps->_pSnake;
while (cur->next->next != NULL)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
//把最后一个结点打印成空格
SetPos(cur->next->x, cur->next->y);
printf(" ");
//释放最后一个结点
free(cur->next);
//把倒数第二个节点的地址置为NULL
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 (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y)
{
ps->_status = KILL_BY_SELF;
break;
}
cur = cur->next; //通过函数遍历蛇身所有节点来检测蛇头是否撞到蛇身
}
}
//游戏善后的工作
void GameEnd(pSnake ps)
{
SetPos(24, 12);
switch (ps->_status) //根据最后游戏结束的不同状态打印信息
{
case END_NORMAL:
wprintf(L"您主动结束游戏\n");
break;
case KILL_BY_WALL:
wprintf(L"您撞到墙上,游戏结束\n");
break;
case KILL_BY_SELF:
wprintf(L"您撞到了自己,游戏结束\n");
break;
}
//释放蛇身的链表
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode del = cur;
cur = cur->next;
free(del);
}
}
test.c 文件
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include <locale.h>
#include "snake.h"
#include<locale.h> //本地化头文件
void test()
{
int ch = 0;
do
{
system("cls"); //清空窗口
//创建贪吃蛇
Snake snake = { 0 };
//初始化游戏
//1. 打印环境界面
//2. 功能介绍
//3. 绘制地图
//4. 创建蛇
//5. 创建食物
//6. 设置游戏的相关信息
GameStart(&snake);
//运行游戏
GameRun(&snake);
//检测是否有按键被按下—— 因为控制台的本身机制,如果在游玩时按上键,控制台会把历史命令显示出来。因此使用系统调用函数来拿走按键
while (_kbhit())
{
// 使用 _getch() 获取按下的键,不阻塞程序
_getch();
// 处理按键事件,可以根据需要进行相应的操作
}
//结束游戏 - 善后工作
GameEnd(&snake);
//打印是否进入下一局的引导语
SetPos(20, 15);//定位
printf("再来一局吗?(Y/N):");
ch = getchar();
while (getchar() != '\n');
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);//将进程结束提示至于窗口最下方
}
int main()
{
//设置适配本地环境
setlocale(LC_ALL, "");
srand((unsigned int)time(NULL)); //只需初始化一次的随机种子用于随机数
test();
return 0;
}
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。