贪吃蛇
引言
在计算机科学的世界里,经典游戏是无法被忽视的一部分。它们不仅是我们对计算机编程技术进行探索和学习的重要工具,也是我们与计算机交互的一种有趣方式。其中,贪吃蛇游戏(Snake Game)无疑是这些经典游戏中的一颗明珠,它简单而充满挑战,一直以来都备受欢迎。
贪吃蛇游戏的核心玩法简单而经典:控制一条蛇在有限的空间内移动,吃掉食物,不断成长,但要避免撞到墙壁或者自己的身体。尽管规则简单,但这个游戏却蕴含了丰富的编程技术和算法挑战。从基本的用户输入处理,到数据结构的应用,再到图形界面的展示,贪吃蛇游戏是一个极好的项目,让我们有机会探索和实践各种计算机科学的基础概念。
在本文中,我们将深入探讨如何使用C语言来实现贪吃蛇游戏。C语言作为一种高效而强大的编程语言,是实现游戏的理想选择。通过这个项目,我们不仅可以加深对C语言的理解,还能够锻炼自己的逻辑思维和问题解决能力。
在接下来的内容中,我们将逐步介绍贪吃蛇游戏的实现过程,从所需要的基本知识到游戏逻辑再到用户界面的设计,带领读者一步步走进这个有趣而充满挑战的编程世界。让我们一起开始这段奇妙的编程之旅吧!
目录
1. 游戏效果及基本功能
使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇。
游戏画面展示:
需要实现的基本功能:
- 贪吃蛇地图控制
- 蛇吃食物的功能(上,下,左,右方向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
2. 技术要点
要想实现贪吃蛇游戏首先我们需要掌握:
C语言随机数的生成、函数、枚举、 结构体、动态内存管理、预处理指令、链表、Win32API 等。
这些知识点在我的前几期博客都有介绍。
2.0 随机数的生成
2.0.1 rand
C语言提供了一个函数叫 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的原型如下:
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列。
mode con cols=100 lines=30
参考:mode指令
也可以通过命令设置控制台窗口的名字
示例 :命名为贪吃蛇
title 贪吃蛇
参考: title命令
这样能在控制台窗口执行的命令,也可以调用C语言函数system来执行。
示例
#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类型的声明
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位,最高位是没有使用的,可表示为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 数据结构设计
在游戏运行的过程中,蛇每次吃一个食物,蛇的身体就会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,所以蛇节点结构如下:
//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)完成游戏结束的说明,实现资源释放
//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 创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使用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
蛇身打印的宽字符:
//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,游戏继续,否则游戏结束。
如果游戏继续,就检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。
需要的虚拟按键的罗列:
- 上:
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.6KillBySelf
//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(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。
//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; }