🕹️前言
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。在编程语⾔的教学中,我们经常以贪吃蛇为例,从设计到代码实现来检验我们的编程能⼒和逻辑能⼒。
💡一,技术要点
C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
✏️二, 控制台设置
如果你们的Win11系统的控制台窗是这样显⽰,一定要重新设置控制台,不然无法进行贪吃蛇:
调整⽅式:
保存后,重新打开cmd就⾏。
🌳三,Win32 API 介绍
本次实现贪吃蛇会使⽤到的⼀些Win32 API知识,接下来我进行介绍。
🦊🦊3.1 Win32 API
Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接。
🐯🐯3.2 控制台程序
平常我们运⾏起来的⿊框程序其实就是控制台程序,我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列。
mode con cols=100 lines=30
也可以通过命令设置控制台窗⼝的名字:
title 贪吃蛇
这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。例如:
#include <stdio.h> #include <Windows.h> //设置控制台的窗口大小,窗口名称 int main() { system("mode con cols=100 lines=30"); system("title 贪吃蛇"); getchar(); //system("pause");//暂停 return 0; }
🎮🎮3.3 控制台屏幕上的坐标 COORD
COORD是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)的原点位于缓冲区的顶部左侧单元格。
给坐标赋值:
COORD pos = { 10, 15 }; • 1
✈️✈️3.4 获取设备句柄 GetStdHandle
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle); • 1
实例:
HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
🎄🎄3.5 获取控制台光标信息 GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息。
HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; //获取控制台光标信息 GetConsoleCursorInfo(hOutput, &CursorInfo);
其中 CONSOLE_CURSOR_INFO是一个结构体,它包含了有关控制台光标的信息
CursorInfo.bVisible = false; //隐藏控制台光标
🐳🐳3.6 设置光标可见性 etConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可⻅性。
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; //获取控制台光标信息 GetConsoleCursorInfo(hOutput, &CursorInfo); //隐藏控制台光标 CursorInfo.bVisible = false; //设置控制台光标状态 SetConsoleCursorInfo(hOutput, &CursorInfo);
🐉🐉3.7 设置光标位置 SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
SetPos:封装⼀个设置光标位置的函数:
void SetPos(int x, int y) { //获取设备句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //定位光标需要移动到的位置 COORD pos = { x ,y }; //将光标位置设置在到指定位置 SetConsoleCursorPosition(handle, pos); }
🦋🦋3.8 获取按键情况 GetAsyncKeyState
获取按键情况,将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调⽤ GetAsyncKeyState 函数后,如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
#define KEY_PRESS(vk) (GetAsyncKeyState(vk) & 0x1 ? 1:0) 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; }
🐍四,贪吃蛇游戏设计与分析
🔮🔮4.1 地图
我们最终的贪吃蛇⼤纲要是这个样⼦,那我们的地图如何布置呢?
这⾥不得不讲⼀下控制台窗⼝的⼀些知识,如果想在控制台的窗⼝中指定位置输出信息,我们得知道该位置的坐标,所以⾸先介绍⼀下控制台窗⼝的坐标知识。
控制台窗⼝的横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。
在游戏地图上,我们打印墙体使⽤宽字符:□,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★。普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
🚀🚀🚀4.1.1 <locale.h>本地化
#include <locale.h> //切换本地信息模式 int main() { char* loc; loc = setlocale(LC_ALL, NULL); printf("默认的本地信息:%s\n", loc); loc = setlocale(LC_ALL, ""); printf("设置后的本地信息:%s\n", loc); return 0; }
✈️✈️✈️4.1.2 宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字⾯量必须加上前缀“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'●'; wprintf(L"%lc\n", ch1); wprintf(L"%lc\n", ch2); wprintf(L"%lc\n", ch3); wprintf(L"%lc\n", ch4); printf("ab\n"); return 0; }
普通字符和宽字符打印出宽度的展⽰如下:
🎄🎄🎄4.1.3 地图坐标
我们假设实现⼀个27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙,如下:
🌭🌭4.2 蛇⾝和⻝物
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24,5)处开始出现蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。
🐥🐥4.3 数据结构设计
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:
//维护一个蛇身的节点 //定义蛇身的节点: 蛇身坐标+下一个节点的指针 typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode;
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
//贪吃蛇 -- 整个游戏中的维护: // 蛇头+食物+方向+当前总分数+一个食物的分数+蛇的状态+蛇的速度 typedef struct Snake { pSnakeNode pSnake;//维护整条蛇的指针 pSnakeNode pFood;//指向食物的指针,蛇会吃掉食物,所以类似蛇的节点类型,增长蛇身 int Score;//当前累计的分数 int FoodWeight;//一个食物的分数 int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢 enum GAME_STATUS status;//游戏当前的状态 enum DIRECTION dir;//蛇当前走的方向 }Snake ,* pSnake;
蛇的⽅向,可以⼀⼀列举,使⽤枚举:
/蛇的方向 enum DIRECTION { UP=1, DOWN, LEFT, RIGHT };
游戏状态,可以⼀⼀列举,使⽤枚举:
//游戏的状态 enum GAME_STATUS { OK = 1,//游戏正常进行 ESC,//按ESC键退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//自己撞自己 };
⛱️⛱️4.4 游戏流程设计
💎5. 核⼼逻辑实现分析
🎋🎋5.1 游戏主逻辑
程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑。
主逻辑分为3个过程:
• 游戏开始(GameStart)完成游戏的初始化
• 游戏运⾏(GameRun)完成游戏运⾏逻辑的实现
• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放
#define _CRT_SECURE_NO_WARNINGS #include "Snake.h" void test() { int ch = 0; do { //创建贪吃蛇 Snake snake = { 0 }; GameStart(&snake);//游戏开始前的初始化 GameRun(&snake);//玩游戏的过程 GameEnd(&snake);//游戏的善后工作 SetPos(20, 15); printf("是否再来一局?(Y/N):"); getchar(); ch = getchar(); } while (ch == 'Y' || ch == 'y'); } int main() { //修改适配本地中文环境 setlocale(LC_ALL, ""); test();//贪吃蛇游戏的测试 SetPos(0, 26); return 0; }
🌵🌵5.2 游戏开始(GameStart)
这个模块完成游戏的初始化任务:
• 控制台窗⼝⼤⼩的设置
• 控制台窗⼝名字的设置
• ⿏标光标的隐藏
• 打印欢迎界⾯
• 创建地图
• 初始化第蛇
• 创建第⼀个⻝物
void GameStart(pSnake ps) { //设置控制台的信息:窗口大小,窗口名 system("mode con cols=100 lines=30"); system("title 贪吃蛇"); //隐藏光标 //获取句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台光标信息 CONSOLE_CURSOR_INFO cursor_info = { 0 }; GetConsoleCursorInfo(handle, &cursor_info); //隐藏光标 cursor_info.bVisible = false; //将改变后的光标进行设置 SetConsoleCursorInfo(handle, &cursor_info); //打印欢迎信息 WelcomeToGame(); //绘制地图 CreateMap(); //初始化蛇 InitSnake(ps); //创建食物 CreateFood(ps); }
🌹🌹🌹5.2.1 打印欢迎界⾯
在游戏正式开始之前,做⼀些功能提醒:
void WelcomeToGame() { //欢迎信息 SetPos(35, 10); printf("欢迎来到贪吃蛇小游戏!\n"); SetPos(38, 20); system("pause"); system("cls");//清屏 //功能介绍信息 SetPos(18, 10); printf("用↑.↓ . ← . →来控制蛇的移动,F3是加速,F4是减速。"); SetPos(18, 11); printf("加速能够得到更高的分数!"); SetPos(38, 20); system("pause"); system("cls"); }
🌳🌳🌳5.2.2 创建地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使⽤wprintf函数,打印格式串前使⽤L,打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
墙体打印的宽字符:
#define WALL L'□'
易错点:就是坐标的计算
上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)
创建地图函数CreateMap:
void CreateMap() { int i = 0; //上 SetPos(0, 0); for (i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //下 SetPos(0, 26); for (i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //左 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); } }
🌽🌽🌽5.2.3 初始化蛇⾝
蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
• 蛇的初始位置从(24,5)开始。
再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,每个⻝物的分数。
• 游戏状态是:OK
• 蛇的移动速度:200毫秒
• 蛇的默认⽅向:RIGHT
• 初始成绩:0
• 每个⻝物的分数:10
蛇⾝打印的宽字符:
#define BODY L'●'
初始化蛇⾝函数:InitSnake
void InitSnake(pSnake ps) { //初始化5个蛇身节点 pSnakeNode cur = NULL; int i = 0; for (i = 0; i < 5; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("InitSnake:malloc():"); return; } cur->x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; //用头插,把5个节点链在一起 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->FoodWeight = 10; ps->pFood = NULL; ps->Score = 0; ps->SleepTime = 180;//毫秒,就是2秒 ps->status = OK; }
🍒🍒🍒5.2.4 创建第⼀个⻝物
• 先随机⽣成⻝物的坐标
◦ x坐标必须是2的倍数
◦ ⻝物的坐标不能和蛇⾝每个节点的坐标重复
• 创建⻝物节点,打印⻝物
⻝物打印的宽字符:
#define FOOD L'★'
创建⻝物的函数:CreateFood
void CreateFood(pSnake ps) { //1.食物是随机出现的,坐标就是随机的 //2.坐标必须在墙内 //3.坐标不能在蛇身上 int x = 0; int y = 0; again: do { x = rand() % 53 + 2; y = rand() % 24 + 1; } while (x % 2 != 0);//生成的x坐标必须为偶数,蛇头的坐标是2的倍数,方便蛇头吃 //坐标和蛇的身体的每个节点做比较,遍历比较 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()\n"); return; } pFood->x = x; pFood->y = y; ps->pFood = pFood; SetPos(x, y);//定位在坐标的位置,产生食物 wprintf(L"%lc", FOOD); }
🍓🍓5.3 游戏运⾏(GameRun)
游戏运⾏期间,右侧打印帮助信息,提⽰玩家,坐标开始位置(64,15)
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游
戏。
需要的虚拟按键的罗列:
• 上:VK_UP
• 下:VK_DOWN
• 左:VK_LEFT
• 右:VK_RIGHT
• 空格:VK_SPACE
• ESC:VK_ESCAPE
• F3:VK_F3
• F4:VK_F4
确定了蛇的⽅向和速度,蛇就可以移动了。
void GameRun(pSnake ps) { //打印帮助信息 PrintHelpInfo(); do { //当前的分数情况 SetPos(62, 10); printf("总分:%5d\n", ps->Score); SetPos(62, 11); printf("每个食物的分值:%02d\n", ps->FoodWeight); //检测按键情况 //上,下,左,右,ESC,空格,F3,F4 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_ESCAPE)) { ps->status = ESC; break; } else if (KEY_PRESS(VK_SPACE)) { //游戏要暂停 Pause();//暂停和恢复 } else if (KEY_PRESS(VK_F3)) { //加速:休眠时间要变短 if (ps->SleepTime >= 80) { ps->SleepTime -= 30;//每加速一次,休眠时间减少30毫秒 ps->FoodWeight += 2;//每加速一次,食物分数加2分 } } else if (KEY_PRESS(VK_F4)) { //减速:休眠时间要变长 if (ps->FoodWeight>2) { ps->SleepTime += 30;//每减速一次,休眠时间增加30毫秒 ps->FoodWeight -= 2;//每减速一次,食物分数减2分 } } //睡眠一下 Sleep(ps->SleepTime); //蛇走的过程 SnakeMove(ps); } while (ps->status == OK); }
🧬🧬🧬5.3.1 检测按键状态 KEY_PRESS
检测按键状态,我们封装了⼀个宏:
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
💰💰💰5.3.2 打印帮助信息 PrintHelpInfo
void PrintHelpInfo() { SetPos(62, 15); printf("1.不能穿墙,不能咬到自己!"); SetPos(62, 16); printf("2.用↑.↓ . ← . →来控制蛇的移动!"); SetPos(62, 17); printf("3.F3是加速,F4是减速."); SetPos(62, 18); printf("4.ESC:退出游戏.space:暂停游戏."); }
🎲🎲🎲5.3.3 蛇⾝移动(SnakeMove)
先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标。
确定了下⼀个位置后,看下⼀个位置是否是⻝物(NextIsFood),是⻝物就做吃⻝物处理
(EatFood),如果不是⻝物则做前进⼀步的处理(NoFood)。
蛇⾝移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上⾃⼰蛇⾝(KillBySelf),从⽽影
响游戏的状态。
void SnakeMove(pSnake ps) { //可以专门创建一个节点,判断它是否与食物在同一位置,是就直接吃掉 pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNext == NULL) { perror("SnakeMove::malloc()"); return; } pNext ->next = NULL; //根据按键按的方向进行移动 switch(ps->dir) { case UP: pNext->x = ps->pSnake->x; pNext->y = ps->pSnake->y - 1; break; case DOWN: pNext->x = ps->pSnake->x; pNext->y = ps->pSnake->y + 1; break; case LEFT: pNext->x = ps->pSnake->x - 2; pNext->y = ps->pSnake->y; break; case RIGHT: pNext->x = ps->pSnake->x + 2; pNext->y = ps->pSnake->y; break; } //下一个坐标处是否是食物,是,返回1,不是,返回0 if (NextIsFood(ps, pNext)) { //是食物就吃掉,蛇身变长 EatFood(ps, pNext); } else { //不是食物就正常走一步 NotEatFood(ps, pNext); } //检测是否撞墙 KillByWall(ps); //检测是否撞到自己 KillBySeif(ps); }
🍭🍭🍭🍭5.3.3.1 判断下一个位置是否是食物 NextIsFood
int NextIsFood(pSnake ps, pSnakeNode pNext) { if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个位置是食物 else return 0; }
🌭🌭🌭🌭5.3.3.2 吃食物 EatFood
void EatFood(pSnake ps, pSnakeNode pNext) { //是食物,就把那个食物节点链到蛇头 pNext->next = ps->pSnake; ps->pSnake = pNext; //打印出此时的蛇 pSnakeNode cur = ps->pSnake;//指向蛇头 while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } ps->Score += ps->FoodWeight; //吃掉食物后,食物节点就消失了 free(ps->pFood); //再生成新的食物 CreateFood(ps); }
⏰⏰⏰⏰5.3.3.3 不是食物 NoFood
将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,释放掉蛇⾝的最后⼀个节点。
易错点:这⾥最容易错误的是,释放最后⼀个结点后,还得将指向在最后⼀个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。
void NotEatFood(pSnake ps, pSnakeNode pNext) { //不是食物 //用头插法,先把pNext节点挂上去 pNext->next = ps->pSnake; ps->pSnake = pNext; //再把尾结点删除,保持蛇的原长 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); cur->next = NULL;//易错 }
💰💰💰💰5.3.3.4 检测是否撞墙 KillByWall
判断蛇头的坐标是否和墙的坐标冲突:
//检测是否撞墙 void KillByWall(pSnake ps) { if (ps->pSnake->x == 0 || //左 ps->pSnake->x == 56 || //右 ps->pSnake->y == 0 || //上 ps->pSnake->y == 26) //下 { ps->status = KILL_BY_WALL; } }
🐳🐳🐳🐳5.3.3.5 检测是否撞到自己 KillBySelf
判断蛇头的坐标是否和蛇⾝体的坐标冲突:
//检测是否撞到自己 void KillBySeif(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; return; //由于有循环,撞到自己时结束循环 } cur = cur->next; } }
🚀🚀5.4 游戏结束
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。
void GameEnd(pSnake ps) { SetPos(18, 12); //说明清楚游戏结束的状态 switch(ps->status) { case ESC: 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->next; free(cur); cur = del; } free(ps->pFood); ps = NULL; }
🎮6. 完整源代码
完整代码实现,分3个⽂件实现。
test.c
#define _CRT_SECURE_NO_WARNINGS #include "Snake.h" void test() { int ch = 0; do { //创建贪吃蛇 Snake snake = { 0 }; GameStart(&snake);//游戏开始前的初始化 GameRun(&snake);//玩游戏的过程 GameEnd(&snake);//游戏的善后工作 SetPos(20, 15); printf("是否再来一局?(Y/N):"); getchar(); ch = getchar(); } while (ch == 'Y' || ch == 'y'); } int main() { //修改适配本地中文环境 setlocale(LC_ALL, ""); test();//贪吃蛇游戏的测试 SetPos(0, 26); return 0; }
snake.h
#pragma once #include <stdio.h> #include <stdlib.h> #include <Windows.h> #include <stdbool.h> #include <locale.h> #define WALL L'□' //墙体 #define BODY L'●' //蛇身 #define FOOD L'★' //食物 //蛇默认的起始位置 #define POS_X 24 #define POS_Y 5 //游戏的状态 enum GAME_STATUS { OK = 1,//游戏正常进行 ESC,//按ESC键退出 KILL_BY_WALL,//撞墙 KILL_BY_SELF//自己撞自己 }; //蛇的方向 enum DIRECTION { UP=1, DOWN, LEFT, RIGHT }; //维护一个蛇身的节点 //定义蛇身的节点: 蛇身坐标+下一个节点的指针 typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode; //贪吃蛇 -- 整个游戏中的维护: // 蛇头+食物+方向+当前总分数+一个食物的分数+蛇的状态+蛇的速度 typedef struct Snake { pSnakeNode pSnake;//维护整条蛇的指针 pSnakeNode pFood;//指向食物的指针,蛇会吃掉食物,所以类似蛇的节点类型,增长蛇身 int Score;//当前累计的分数 int FoodWeight;//一个食物的分数 int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢 enum GAME_STATUS status;//游戏当前的状态 enum DIRECTION dir;//蛇当前走的方向 }Snake ,* pSnake; //定位函数 void SetPos(int x, int y); //获取按键情况,检测按键是否按过,1为按过,0为没2按过 #define KEY_PRESS(vk) (GetAsyncKeyState(vk) & 0x1 ? 1:0) //游戏开始的准备环节 void GameStart(pSnake ps); //打印欢迎信息 void WelcomeToGame(); //绘制地图 void CreateMap(); //初始化蛇 void InitSnake(pSnake ps); //创建食物 void CreateFood(pSnake ps); //游戏运行的整个逻辑 void GameRun(pSnake ps); //打印帮助信息 void PrintHelpInfo(); //蛇移动的函数 -- 每一次走一步 void SnakeMove(pSnake ps); //判断蛇头的下一步要走的节点是不是食物 int NextIsFood(pSnake ps, pSnakeNode pNext); //下一步要走的位置是食物,就吃掉 void EatFood(pSnake ps, pSnakeNode pNext); //下一步要走的位置不是食物,就继续走 void NotEatFood(pSnake ps, pSnakeNode pNext); //检测是否撞墙 void KillByWall(pSnake ps); //检测是否撞到自己 void KillBySeif(pSnake ps); //游戏结束释放资源 void GameEnd(pSnake ps);
snake.c
#define _CRT_SECURE_NO_WARNINGS #include "Snake.h" void SetPos(int x, int y) { //获取设备句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //定位光标需要移动到的位置 COORD pos = { x ,y }; //将光标位置设置在到指定位置 SetConsoleCursorPosition(handle, pos); } void WelcomeToGame() { //欢迎信息 SetPos(35, 10); printf("欢迎来到贪吃蛇小游戏!\n"); SetPos(38, 20); system("pause"); system("cls");//清屏 //功能介绍信息 SetPos(18, 10); printf("用↑.↓ . ← . →来控制蛇的移动,F3是加速,F4是减速。"); SetPos(18, 11); printf("加速能够得到更高的分数!"); SetPos(38, 20); system("pause"); system("cls"); } void CreateMap() { int i = 0; //上 SetPos(0, 0); for (i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //下 SetPos(0, 26); for (i = 0; i <= 56; i += 2) { wprintf(L"%lc", WALL); } //左 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); } } void InitSnake(pSnake ps) { //初始化5个蛇身节点 pSnakeNode cur = NULL; int i = 0; for (i = 0; i < 5; i++) { cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("InitSnake:malloc():"); return; } cur->x = POS_X + 2 * i; cur->y = POS_Y; cur->next = NULL; //用头插,把5个节点链在一起 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->FoodWeight = 10; ps->pFood = NULL; ps->Score = 0; ps->SleepTime = 180;//毫秒,就是2秒 ps->status = OK; } void CreateFood(pSnake ps) { //1.食物是随机出现的,坐标就是随机的 //2.坐标必须在墙内 //3.坐标不能在蛇身上 int x = 0; int y = 0; again: do { x = rand() % 53 + 2; y = rand() % 24 + 1; } while (x % 2 != 0);//生成的x坐标必须为偶数,蛇头的坐标是2的倍数,方便蛇头吃 //坐标和蛇的身体的每个节点做比较,遍历比较 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()\n"); return; } pFood->x = x; pFood->y = y; ps->pFood = pFood; SetPos(x, y);//定位在坐标的位置,产生食物 wprintf(L"%lc", FOOD); } void GameStart(pSnake ps) { //设置控制台的信息:窗口大小,窗口名 system("mode con cols=100 lines=30"); system("title 贪吃蛇"); //隐藏光标 //获取句柄 HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); //获取控制台光标信息 CONSOLE_CURSOR_INFO cursor_info = { 0 }; GetConsoleCursorInfo(handle, &cursor_info); //隐藏光标 cursor_info.bVisible = false; //将改变后的光标进行设置 SetConsoleCursorInfo(handle, &cursor_info); //打印欢迎信息 WelcomeToGame(); //绘制地图 CreateMap(); //初始化蛇 InitSnake(ps); //创建食物 CreateFood(ps); } void PrintHelpInfo() { SetPos(62, 15); printf("1.不能穿墙,不能咬到自己!"); SetPos(62, 16); printf("2.用↑.↓ . ← . →来控制蛇的移动!"); SetPos(62, 17); printf("3.F3是加速,F4是减速."); SetPos(62, 18); printf("4.ESC:退出游戏.space:暂停游戏."); } void Pause() { while (1) { Sleep(100); if (KEY_PRESS(VK_SPACE)) { break; } } } int NextIsFood(pSnake ps, pSnakeNode pNext) { if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) return 1;//下一个位置是食物 else return 0; } void EatFood(pSnake ps, pSnakeNode pNext) { //是食物,就把那个食物节点链到蛇头 pNext->next = ps->pSnake; ps->pSnake = pNext; //打印出此时的蛇 pSnakeNode cur = ps->pSnake;//指向蛇头 while (cur) { SetPos(cur->x, cur->y); wprintf(L"%lc", BODY); cur = cur->next; } ps->Score += ps->FoodWeight; //吃掉食物后,食物节点就消失了 free(ps->pFood); //再生成新的食物 CreateFood(ps); } void NotEatFood(pSnake ps, pSnakeNode pNext) { //不是食物 //用头插法,先把pNext节点挂上去 pNext->next = ps->pSnake; ps->pSnake = pNext; //再把尾结点删除,保持蛇的原长 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); 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 KillBySeif(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; return; //由于有循环,撞到自己时结束循环 } cur = cur->next; } } void SnakeMove(pSnake ps) { //可以专门创建一个节点,判断它是否与食物在同一位置,是就直接吃掉 pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNext == NULL) { perror("SnakeMove::malloc()"); return; } pNext ->next = NULL; //根据按键按的方向进行移动 switch(ps->dir) { case UP: pNext->x = ps->pSnake->x; pNext->y = ps->pSnake->y - 1; break; case DOWN: pNext->x = ps->pSnake->x; pNext->y = ps->pSnake->y + 1; break; case LEFT: pNext->x = ps->pSnake->x - 2; pNext->y = ps->pSnake->y; break; case RIGHT: pNext->x = ps->pSnake->x + 2; pNext->y = ps->pSnake->y; break; } //下一个坐标处是否是食物,是,返回1,不是,返回0 if (NextIsFood(ps, pNext)) { //是食物就吃掉,蛇身变长 EatFood(ps, pNext); } else { //不是食物就正常走一步 NotEatFood(ps, pNext); } //检测是否撞墙 KillByWall(ps); //检测是否撞到自己 KillBySeif(ps); } void GameRun(pSnake ps) { //打印帮助信息 PrintHelpInfo(); do { //当前的分数情况 SetPos(62, 10); printf("总分:%5d\n", ps->Score); SetPos(62, 11); printf("每个食物的分值:%02d\n", ps->FoodWeight); //检测按键情况 //上,下,左,右,ESC,空格,F3,F4 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_ESCAPE)) { ps->status = ESC; break; } else if (KEY_PRESS(VK_SPACE)) { //游戏要暂停 Pause();//暂停和恢复 } else if (KEY_PRESS(VK_F3)) { //加速:休眠时间要变短 if (ps->SleepTime >= 80) { ps->SleepTime -= 30;//每加速一次,休眠时间减少30毫秒 ps->FoodWeight += 2;//每加速一次,食物分数加2分 } } else if (KEY_PRESS(VK_F4)) { //减速:休眠时间要变长 if (ps->FoodWeight>2) { ps->SleepTime += 30;//每减速一次,休眠时间增加30毫秒 ps->FoodWeight -= 2;//每减速一次,食物分数减2分 } } //睡眠一下 Sleep(ps->SleepTime); //蛇走的过程 SnakeMove(ps); } while (ps->status == OK); } void GameEnd(pSnake ps) { SetPos(18, 12); //说明清楚游戏结束的状态 switch(ps->status) { case ESC: 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->next; free(cur); cur = del; } free(ps->pFood); ps = NULL; }