在我们学习完C语言 和单链表知识点后 我们开始写个贪吃蛇的代码
目标:使用C语言在Windows环境的控制台模拟实现经典小游戏贪吃蛇
贪吃蛇代码实现的基本功能:
地图的绘制
蛇、食物的创建
蛇的状态(正常 撞墙 撞到自己 正常退出)
蛇移动的方向(上 下 左 右)
蛇的加速 减速
游戏暂停
.......
该代码会运用到函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等
我们会在学习前 会引出一些必要的知识点
目录
Win32 API介绍
控制台程序
控制台屏幕上的坐标COORD
GetStdHandle
GetConsoleCursorInfo
CONSOLE_CURSOR_INFO
SetConsoleCursorInfo
SetConsoleCursorPosition
GetAsyncKeyState
贪吃蛇游戏设计与分析
地图的绘制
本地化
类项
setlocale函数
宽字符的打印
游戏流程设计
核⼼逻辑实现分析
游戏开始(GameStart)
1.设置控制台窗口大小 名称 隐藏光标
2.欢迎页面及功能介绍(WelcomeToGame)
3.绘制地图(CreatMap)
4.创建蛇(初始化蛇身)(InitSnake)
5.创建食物(CreateFood)
游戏运行(GameRun)
1.游戏运⾏期间,右侧打印帮助信息(PrintHelpInfo)
2.根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
游戏有两种状态:i.蛇撞墙而亡 ii.蛇撞到自己
3.如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
蛇身移动(SnakeMove)
游戏结束(GameEnd)
完整代码如下
Snake.h
Snake.c
Test.c
Win32 API介绍
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application) ,所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。
控制台程序
平常我们运⾏起来的⿊框程序其实就是控制台程序 我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列(按住windows+R输入cmd 我们可以使用cmd命令来设置控制台窗口的长度:如 设置控制台窗口的大小 30行 100列 也可以通过命令设置控制台窗口的名字: 这些能在控制台窗口执行的命令 也可以调用C语言函数system来执行 代码实现:控制台屏幕上的坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。给坐标赋值:
COORD pos = { 2, 10 };
GetStdHandle
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊--键盘、标准输出--屏幕或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。(我们可以把句柄想象成一个钥匙 只有拿到钥匙才能打开锁 ---只有得到句柄后才能进行一些操作) HANDLE GetStdHandle(DWORD nStdHandle);
在我们贪吃蛇代码中 函数的参数是STD_OUTPUT_HANDLE 标准输出
实例:
HANDLE houtput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息 在该函数中 有一参数是 PCONSOLE_CURSOR_INFO 其是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标 (光标)的信息实例:
//获得标准输出设备的句柄HANDLE houtput = NULL;houtput=GetStdHandle(STD_OUTPUT_HANDLE);//houput获取标准输出的句柄 使得可以操作屏幕//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息
CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息 dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完 全填充单元格到单元底部的⽔平线条。 bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE 在前一个代码中我们声明了一个结构体cursor_infocursor_info.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性//获得标准输出设备的句柄HANDLE houtput = NULL;houtput=GetStdHandle(STD_OUTPUT_HANDLE);//houput获取标准输出的句柄 使得可以操作屏幕//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info = { 0 };//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息//修改光标占比cursor_info.dwSize = 50;cursor_info.bVisible = false;//设置和houtput句柄相关的控制台上的光标信息SetConsoleCursorInfo(houtput, &cursor_info);
SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置 实例://获得标准输出设备的句柄HANDLE houtput = NULL;houtput = GetStdHandle(STD_OUTPUT_HANDLE);//句柄houput获取标准输出的句柄 使得可以操作屏幕//定位光标的位置COORD pos = { 10,20 };SetConsoleCursorPosition(houtput, pos);
SetPos:封装一个设置光标位置的函数 ----贪吃蛇地图 蛇 食物的绘制都需要移动光标
void SetPos(short x, short y)//封装一个函数SetPos 光标位置{HANDLE houtput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标位置COORD pos = { x, y };SetConsoleCursorPosition(houtput, pos);}
GetAsyncKeyState
获取按键情况 将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态 GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果 返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。 如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1. 在贪吃蛇中 如:方向键......都需要该函数进行判断是否被按过 这里我们可以写一个宏:#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//这里的0x1也就是 1
Win32 API我们就讲到这里
除了Win32 API外 我们还有其他知识需要学习
我这里是用VS2022写的代码 这里还需要对运行窗口做些改变
贪吃蛇游戏设计与分析
绘制地图(CreatMap)
我们假设实现一个27行 58列的棋盘 再围绕地图画出墙 这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。 控制台窗⼝的坐标如下所⽰,横向的是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>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。<locale.h>本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。 在标准中,依赖地区的部分有以下⼏项: • 数字量的格式 • 货币量的格式 • 字符集 • ⽇期和时间的表⽰形式类项
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,指定⼀个类项: • LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。 • LC_CTYPE:影响字符处理函数的⾏为。 • LC_MONETARY:影响货币格式。 • LC_NUMERIC:影响 printf() 的数字格式。 • LC_TIME:影响时间格式 strftime() 和 wcsftime() 。 • LC_ALL :针对所有类项修改,将以上所有类别设置为给定的语⾔环境。setlocale函数
char* setlocale (int category, const char* locale);
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。 setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。 C标准给第⼆个参数仅定义了2种可能取值: "C" (正常模式)和 " " (本地模式) 在任意程序执行开始 都会隐藏式执行调用正常模式 当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。 ⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。 setlocale(LC_ALL, " ");//切换到本地环境
宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢? 宽字符的字⾯量必须加上前缀“L”,否则 C 语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引 号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %lc ;在双引号前⾯,表⽰宽字符串,对应 wprintf() 的占位符为 %ls 。 代码实例: 从输出结果就可以看出 打印一个普通字符 占一个字符的位置 但是打印一个宽字符 占用2个字符的位置 所以我们在贪吃蛇中使用宽字符时 得处理好地图上坐标的计算2.蛇身和食物(InitSnake) (CreateFood)
初始化状态 ,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现 蛇,连续5个节点 注意:蛇的每个节点的x坐标必须是2的倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。 关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,也不能超出墙体,然后打印★。 如下图:3.数据结构设计(蛇的结点 贪吃蛇的方向 状态 食物分数 总得分 加速 减速....)
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信 息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:typedef struct Snakenode{//坐标int x;int y;//指向下一个节点的指针struct Snakenode* next;}Snakenode,*pSnakeNode;//结构体指针pSnakeNode
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇: typedef struct Snake{pSnakeNode _pSnake;//指向蛇头的指针pSnakeNode _pFood;//指向食物节点的指针enum DIRECTION _dir;//蛇的方向enum GAME_STATUS _status;//游戏的状态int _food_weight;//食物分数int _score;//总分数int _sleep_time;//休息时间 时间越短 速度越快 时间越长 速度越慢}Snake,*pSnake;
蛇的⽅向,可以⼀⼀列举,使⽤枚举 enum DIRECTION{UP = 1,DOWN,LEFT,RIGHT};
游戏状态,可以⼀⼀列举,使⽤枚举 //蛇的状态//正常 撞墙 撞到自己 正常退出enum GAME_STATUS {OK,KILL_BY_WALL,KILL_BY_SELF,END_NORMAL};
游戏流程设计
核⼼逻辑实现分析
程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑
主逻辑分为3个过程: • 游戏开始(GameStart)完成游戏的初始化 • 游戏运⾏(GameRun)完成游戏运⾏逻辑的实现 • 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
//Test.c#include<locale.h>#include"snake.h"//完成的是游戏的测试逻辑void test(){int ch = 0;do{//创建贪吃蛇Snake snake = { 0 };//初始化游戏 //1.打印环境页面//2.功能介绍//3.绘制地图//4.创建蛇//5.创建食物//6.设置游戏的相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏--善后工作GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();//按下回车后 其实接收到的是\n 所以需要清理掉\n//getchar();//清理掉\nwhile (getchar() != '\n');} while (ch=='Y' || ch == 'y');SetPos(0, 26);//让代码结束的信息放在地图外 }int main(){//设置适配本地环境setlocale(LC_ALL, "");//中文 宽字符的打印srand((unsigned int)time(NULL));test();return 0;}
游戏开始(GameStart)
1.设置控制台窗口大小 名称 隐藏光标
void GameStart(pSnake ps){//0.先设置窗口的大小 然后把光标隐藏system("mode con cols=100 lines=30");//30行 100列system("title 贪吃蛇");HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);//隐藏光标操作//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info;//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息 cursor_info.bVisible = false;//设置和houtput句柄相关的控制台上的光标信息SetConsoleCursorInfo(houtput, &cursor_info);//1.打印欢迎页面 //2.功能介绍WelcomeToGame();//3.绘制地图CreatMap();//4.创建蛇InitSnake(ps);//5.创建食物CreateFood(ps);}
2.欢迎页面及功能介绍(WelcomeToGame)
打印欢迎页面
在我们使用光标位置 之前 我们封装一个函数SetPos
void SetPos(short x, short y)//封装一个函数SetPos 光标位置{HANDLE houtput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标位置COORD pos = { x, y };SetConsoleCursorPosition(houtput, pos);}
void WelcomeToGame(){SetPos(45, 14);wprintf(L"欢迎来到贪吃蛇小游戏\n");SetPos(46, 20);system("pause");system("cls");//清除控制台信息SetPos(30, 14);wprintf(L"用↑ ↓ ← → 来控制蛇的移动 按F3加速 F4减速\n");SetPos(34, 15);wprintf(L"加速能够得到更高的分数");SetPos(46, 20);system("pause");system("cls");//紧接着下面应该是地图绘制}
代码实现结果:
3.绘制地图(CreatMap)
绘制地图就是将墙体打印出来 因为这里是宽字符打印 所有打印使用wprintf函数 打印格式串前使用L 绘制地图的关键是算好光标的坐标 才能在想要的位置打印墙体
为墙体打印的宽字符写个宏:
#define WALL L'□'
这里的易错点也就是坐标的计算
代码如下:
void CreatMap(){//27行 58列的棋盘//上int i = 0;for (i = 0; i < 29; i++){//□wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (i = 0; i < 29; i++)wprintf(L"%lc", WALL);//左for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}//getchar();}
4.创建蛇(初始化蛇身)(InitSnake)
蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。 创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。 蛇的初始位置从 (24,5) 开始。 再设置贪吃蛇的属性 蛇的默认⽅向:RIGHT 初始成绩:0 每个⻝物的分数:10 蛇的移动速度:200毫秒 游戏状态是:OK
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 +i*2;//显示 + 未使用的表达式结果cur->y = POS_Y;//头插法插入链表if (ps->_pSnake == NULL)//空链表 //蛇头{ps->_pSnake = cur;}else//非空{cur->next = ps->_pSnake;ps->_pSnake = cur;}}cur = ps->_pSnake;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;/*getchar();*/为什么我的代码只打印一个●}
蛇⾝打印的宽字符: #define BODY L'●'
5.创建食物(CreateFood)
先随机生成食物的坐标(rand和srand函数)
1.x坐标必须是2的倍数
2.食物的坐标不能和蛇身每个结点的坐标重复 不能越过墙体
创建食物结点 打印食物
食物打印的宽字符:
#define FOOD L'★'
CreateFood函数:
void CreateFood(pSnake ps){int x = 0;int y = 0;//x = rand()%53+2;//x:2~54---0~52 //还要保证x是偶数//y = rand()%25+1;//y:1~25---0~24again:do{x = rand() % 53 + 2;//x:2~54---0~52 //还要保证x是偶数y = rand()%25+1;//y:1~25---0~24} while (x % 2 != 0);//x和y的坐标不能跟蛇的身体相冲突pSnakeNode cur = ps->_pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//创捷食物结点pSnakeNode pFood=(pSnakeNode)malloc(sizeof(Snakenode));if (pFood == NULL){perror("CreateFood()::malloc()");}pFood->x = x;pFood->y = y;pFood->next = NULL;SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = pFood;}
游戏运行(GameRun)
1.游戏运⾏期间,右侧打印帮助信息(PrintHelpInfo)
void PrintHelpInfo(){SetPos(64, 10);wprintf(L"%ls", L"不能穿墙,不能咬到自己");SetPos(64, 11);wprintf(L"%ls", L"用↑ ↓ ← → 来控制蛇的移动 \n");SetPos(64, 12);wprintf(L"%ls", L"按F3加速 F4减速");SetPos(64, 13);wprintf(L"%ls", L"按ESC退出游戏 空格暂停游戏");SetPos(64, 14);wprintf(L"%ls", L"made in LiFeiFei");}
2.根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
游戏有两种状态:i.蛇撞墙而亡 ii.蛇撞到自己
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;//cur指向蛇身while (cur){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){ps->_status = KILL_BY_SELF;break;}cur = cur->next;}}
3.如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
//获取按键情况 如果按键最小位为1 说明已按过#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
需要的虚拟按键的罗列: • 上:VK_UP • 下:VK_DOWN • 左:VK_LEFT • 右:VK_RIGHT • 空格:VK_SPACE • ESC:VK_ESCAPE • F3:VK_F3 • F4:VK_F4 蛇身移动(SnakeMove)
蛇下一步移动方向会有几种情况:
1.下一步不是食物---游戏正常 在打印蛇时 我们还要将最后一个结点原先所在位置打印两个空格 并释放掉原先最后一个结点 2.下一步是食物---吃掉食物---蛇的结点数+1 这里我们使用头插---新增结点位于蛇头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);//将倒数第二个结点的地址域置为NULLcur->next = NULL;}int NextIsFood(pSnakeNode pn, pSnake ps){return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);}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 pause()//暂停{while (1){Sleep(200);if (KEY_PRESS(VK_SPACE)){break;}}}
在NoFood中 有一个易错点:在打印下一步的蛇身时 也就是将下一个结点当作蛇头 并将之前蛇身最后一个结点打印为空格 释放掉原本蛇身的最后一个结点 在释放掉最后一个结点后 还得将指向(原先蛇身的倒数第二个结点)最后一个结点的指针改为NULL 保证蛇尾打印可以正常结束 不会越界访问
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);}
确定了蛇的⽅向和速度,蛇就可以移动了。 void GameRun(pSnake ps){//打印帮助信息PrintHelpInfo();do{//打印总分数 和食物的分值SetPos(64, 8);printf("总分数:%d\n", ps->_score);SetPos(64, 9);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))//ESC{//正常退出游戏ps->_status = END_NORMAL;//退出游戏break;}else if (KEY_PRESS(VK_F3))//F3加速---休眠时间减少{if (ps->_sleep_time >= 80)//初始休眠时间为200-80=120 减4次{//最小休眠时间为50ps->_sleep_time -= 30;ps->_food_weight += 2;//食物初始分值为10 ⼀个⻝物分数最⾼是20分}}else if (KEY_PRESS(VK_F4))//F4减速{if (ps->_sleep_time < 320)//200 230 260 290 320{ //10 8 6 4 2ps->_sleep_time += 30;ps->_food_weight -= 2;//⼀个⻝物分数最低是2分}}SnakeMove(ps);//蛇走一步的过程Sleep(ps->_sleep_time);} while (ps->_status == OK);}
游戏结束(GameEnd)
游戏状态不再是OK的时候 要告知游戏结束的原因 并且释放蛇身结点
void GameEnd(pSnake ps){SetPos(24, 12);switch (ps->_status){case END_NORMAL:printf("有事 不玩了\n");break;case KILL_BY_WALL:printf("撞个墙玩玩\n");break;case KILL_BY_SELF:printf("吃个自己吧\n");break;}//释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}}
完整代码如下
Snake.h
//Snake.h#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'★'//类型的声明// //蛇身的结点类型typedef struct Snakenode{//坐标int x;int y;//指向下一个节点的指针struct Snakenode* next;}Snakenode,*pSnakeNode;//结构体指针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; //枚举方向enum DIRECTION{UP = 1,DOWN,LEFT,RIGHT}; //蛇的状态//正常 撞墙 撞到自己 正常退出enum GAME_STATUS {OK,KILL_BY_WALL,KILL_BY_SELF,END_NORMAL}; //函数的声明 //定位光标位置void SetPos(short x, short y); //游戏的初始化void GameStart(pSnake ps); //欢迎界面的打印void WelcomeToGame(); //创建地图void CreatMap(); //4.创建蛇 --初始化蛇身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
//snake.c#include"snake.h" void SetPos(short x, short y)//封装一个函数SetPos 光标位置{HANDLE houtput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)houtput = GetStdHandle(STD_OUTPUT_HANDLE);//定位光标位置COORD pos = { x, y };SetConsoleCursorPosition(houtput, pos);}void WelcomeToGame(){SetPos(45, 14);wprintf(L"欢迎来到贪吃蛇小游戏\n");SetPos(46, 20);system("pause"); system("cls");//清除控制台信息SetPos(30, 14);wprintf(L"用↑ ↓ ← → 来控制蛇的移动 按F3加速 F4减速\n");SetPos(34, 15);wprintf(L"加速能够得到更高的分数"); SetPos(46, 20);system("pause"); system("cls");//紧接着下面应该是地图绘制} void CreatMap(){//27行 58列的棋盘//上int i = 0;for (i = 0; i < 29; i++){//□wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (i = 0; i < 29; i++)wprintf(L"%lc", WALL);//左for (i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);} //右for (i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}//getchar();} //4.创建蛇 --初始化蛇身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 +i*2;//显示 + 未使用的表达式结果cur->y = POS_Y; //头插法插入链表if (ps->_pSnake == NULL)//空链表 //蛇头{ps->_pSnake = cur;}else//非空{cur->next = ps->_pSnake;ps->_pSnake = cur;}}cur = ps->_pSnake;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;/*getchar();*/为什么我的代码只打印一个●} void CreateFood(pSnake ps){int x = 0;int y = 0;//x = rand()%53+2;//x:2~54---0~52 //还要保证x是偶数//y = rand()%25+1;//y:1~25---0~24again:do{x = rand() % 53 + 2;//x:2~54---0~52 //还要保证x是偶数y = rand()%25+1;//y:1~25---0~24} while (x % 2 != 0);//x和y的坐标不能跟蛇的身体相冲突pSnakeNode cur = ps->_pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//创捷食物结点pSnakeNode pFood=(pSnakeNode)malloc(sizeof(Snakenode));if (pFood == NULL){perror("CreateFood()::malloc()");}pFood->x = x;pFood->y = y;pFood->next = NULL; SetPos(x, y);wprintf(L"%lc", FOOD);ps->_pFood = pFood;} void GameStart(pSnake ps){//0.先设置窗口的大小 然后把光标隐藏system("mode con cols=100 lines=30");//30行 100列system("title 贪吃蛇");HANDLE houtput=GetStdHandle(STD_OUTPUT_HANDLE);//隐藏光标操作//定义一个光标信息的结构体CONSOLE_CURSOR_INFO cursor_info;//获取和houtput句柄相关的控制台上的光标信息 并存放在cursor_info中GetConsoleCursorInfo(houtput, &cursor_info);//获取控制台光标信息 cursor_info.bVisible = false;//设置和houtput句柄相关的控制台上的光标信息SetConsoleCursorInfo(houtput, &cursor_info);//1.打印欢迎页面 //2.功能介绍WelcomeToGame();//3.绘制地图CreatMap();//4.创建蛇InitSnake(ps);//5.创建食物CreateFood(ps);} void PrintHelpInfo(){SetPos(64, 10);wprintf(L"%ls", L"不能穿墙,不能咬到自己"); SetPos(64, 11);wprintf(L"%ls", L"用↑ ↓ ← → 来控制蛇的移动 \n"); SetPos(64, 12);wprintf(L"%ls", L"按F3加速 F4减速"); SetPos(64, 13);wprintf(L"%ls", L"按ESC退出游戏 空格暂停游戏"); SetPos(64, 14);wprintf(L"%ls", L"made in LiFeiFei");} //获取按键情况 如果按键最小位为1 说明已按过#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 ) void pause()//暂停{while (1){Sleep(200);if (KEY_PRESS(VK_SPACE)){break;}}} int NextIsFood(pSnakeNode pn, pSnake ps){return (ps->_pFood->x == pn->x && ps->_pFood->y == pn->y);} 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); //将倒数第二个结点的地址域置为NULLcur->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;//cur指向蛇身while (cur){if (cur->x == ps->_pSnake->x && cur->y == ps->_pSnake->y){ps->_status = KILL_BY_SELF;break;}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{NoFood(pNextNode, ps);} //检测蛇是否撞墙KillByWall(ps);//检测蛇是否撞到自己KillBySelf(ps);} void GameRun(pSnake ps){//打印帮助信息PrintHelpInfo();do{//打印总分数 和食物的分值SetPos(64, 8);printf("总分数:%d\n", ps->_score);SetPos(64, 9);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))//ESC{//正常退出游戏ps->_status = END_NORMAL;//退出游戏break;}else if (KEY_PRESS(VK_F3))//F3加速---休眠时间减少{if (ps->_sleep_time >= 80)//初始休眠时间为200-80=120 减4次{//最小休眠时间为50ps->_sleep_time -= 30;ps->_food_weight += 2;//食物初始分值为10 ⼀个⻝物分数最⾼是20分}}else if (KEY_PRESS(VK_F4))//F4减速{if (ps->_sleep_time < 320)//200 230 260 290 320{ //10 8 6 4 2ps->_sleep_time += 30;ps->_food_weight -= 2;//⼀个⻝物分数最低是2分}} SnakeMove(ps);//蛇走一步的过程Sleep(ps->_sleep_time);} while (ps->_status == OK);} void GameEnd(pSnake ps){SetPos(24, 12);switch (ps->_status){case END_NORMAL:printf("有事 不玩了\n");break;case KILL_BY_WALL:printf("撞个墙玩玩\n");break;case KILL_BY_SELF:printf("吃个自己吧\n");break;} //释放蛇身的链表pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}}
Test.c
//test.c#include<locale.h>#include"snake.h"//完成的是游戏的测试逻辑void test(){int ch = 0;do{//创建贪吃蛇Snake snake = { 0 };//初始化游戏 //1.打印环境页面//2.功能介绍//3.绘制地图//4.创建蛇//5.创建食物//6.设置游戏的相关信息GameStart(&snake);//运行游戏GameRun(&snake);//结束游戏--善后工作 GameEnd(&snake);SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();//按下回车后 其实接收到的是\n 所以需要清理掉\n//getchar();//清理掉\nwhile (getchar() != '\n'); } while (ch=='Y' || ch == 'y');SetPos(0, 26);//让代码结束的信息放在地图外 }int main(){//设置适配本地环境setlocale(LC_ALL, "");//中文 宽字符的打印srand((unsigned int)time(NULL));test(); return 0;}