目录
前言
1. Win32 API介绍
1.1 Win32 API
1.2 控制台程序
1.3 控制台屏幕上的坐标COORD
1.4 GetStdHandle
1.5 GetConsoleCursorInfo
1.6 CONSOLE_CURSOR_INFO
1.7 SetConsoleCursorInfo
1.8 SetConsoleCursorPosition
1.8 GetAsyncKeyState
2.贪吃蛇游戏设计
2.1地图
2.1.1 本地化
2.2宽字符的打印
2.3 蛇身和食物
2.4 数据结构的设计
3.核心逻辑的实现
3.1 游戏主逻辑
3.2 游戏开始
3.2.1 隐藏光标
3.2.2 打印欢迎信息
编辑
编辑
3.2.3 绘制地图
编辑
3.2.4 初始化蛇身
3.2.5 创建食物
3.3 游戏运行
3.3.1 KEY_PRESS
3.3.2 PrintHelpInfo
3.3.3 蛇身的移动
3.3.3.1 NextIsFood
3.3.3.2 EatFood
3.3.3.3 NotEatFood
3.3.3.4 KillByWall
3.3.3.5 KillBySelf
3.4 游戏结束
4.完整代码
4.1 test.c
4.2 snake.h
4.3 snake.c
总结
前言
今天这篇贪吃蛇游戏实现满满干货,可以边看边写,或者反复观看。其中需要运用到C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等知识,我相信你认真看一遍会学到不少知识。
如果你边看边写,在写一部分的时候需要停下来看效果可以使用getchar()或者system(“pause”)这两段代码其中之一即可。都需要你按键盘上的按钮才会继续。
1. Win32 API介绍
1.1 Win32 API
Windows 这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外, 它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每一种服务就是一个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程(Application), 所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应⽤程序编程接⼝。
1.2 控制台程序
平常我们运⾏起来的⿊框程序其实就是控制台程序
我们可以使⽤cmd命令来设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列
mode con cols=100 lines=30
也可以通过命令设置控制台窗⼝的名字:
title 贪吃蛇
但是,如果你打开cmd是下面这样子,那么你的命令窗口是个终端,需要调整成控制窗口。
按右键,点击设置。点击默认终端应用程序的下划线,选择windows控制台主机。
这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执行。例如:
#include <stdio.h>
int main()
{//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列system("mode con cols=100 lines=30");//设置cmd窗⼝名称system("title 贪吃蛇");return 0;
}
1.3 控制台屏幕上的坐标COORD
COORD 是Windows API中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标。
如上图,表明原点,X轴和Y轴。给坐标赋值:
COORD pos = { 10, 15 };
1.4 GetStdHandle
GetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作备。
HANDLE GetStdHandle(DWORD nStdHandle);
简单来说,就是你的命令窗口可以开启多个,为了让系统知道你在哪一个命令窗口执行命令,需要用到GetStdHandle获取该命令窗口的信息。Handle是一个关键字。
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
1.5 GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息。
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
第一个函数参数是Handle类型的句柄,第二个参数下面会介绍。
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
1.6 CONSOLE_CURSOR_INFO
这个结构体,包含有关控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
- dwSize,由光标填充的字符单元格的百分⽐。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的⽔平线条。
- bVisible,游标的可⻅性。 如果光标可⻅,则此成员为 TRUE。
1.7 SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的⼤⼩和可见性。
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
实例:
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
1.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);SetConsoleCursorPosition(hOutput, pos);
}
1.8 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 )
2.贪吃蛇游戏设计
2.1地图
贪吃蛇游戏初始界面大概如下:
在游戏地图上,我们打印墙体使⽤宽字符:■,打印蛇使⽤宽字符●,打印⻝物使⽤宽字符★。这些可以在搜狗输入法的符号大全中找到。普通的字符是占⼀个字节的,这类宽字符是占⽤2个字节。
过去C语⾔并不适合⾮英语国家(地区)使⽤。C语⾔最初假定字符都是⾃⼰的。但是这些假定并不是在世界的任何地⽅都适⽤。后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊和宽字符的类型wchar_t 和宽字符的输⼊和输出函数,加⼊和<locale.h>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
2.1.1 本地化
<locale.h>提供的函数⽤于控制C标准库中对于不同的地区会产⽣不⼀样⾏为的部分。
通过修改地区,程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中⼀部分可能是我们不希望修改的。所以C语⾔⽀持针对不同的类项进⾏修改,下⾯的⼀个宏,指定⼀个类项:
- LC_COLLATE
- LC_CTYPE
- LC_MONETARY
- LC_NUMERIC
- LC_TIME
- LC_ALL - 针对所有类项修改
setlocale 函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。C标准给第⼆个参数仅定义了2种可能取值:"C"和" "。
在任意程序执⾏开始,都会隐藏式执⾏调⽤:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。
setlocale(LC_ALL, " ");//切换到本地环境
2.2宽字符的打印
那如果想在屏幕上打印宽字符,怎么打印呢?
#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"%c\n", ch1);wprintf(L"%c\n", ch2);wprintf(L"%c\n", ch3);wprintf(L"%c\n", ch4);return 0;
}
输出结果:
从输出的结果来看,我们发现⼀个普通字符占⼀个字符的位置但是打印⼀个汉字字符,占⽤2个字符的位置,那么我们如果要在贪吃蛇中使⽤宽字符,就得处理好地图上坐标的计算。
一个坐标一个正常字符,两个坐标就是一个宽字符。
我们假设实现⼀个棋盘27行,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙。
2.3 蛇身和食物
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。
2.4 数据结构的设计
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:
typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode, * pSnakeNode;
注意:typedef加在结构体面前,第二个命名* pSnakeNode,是吧struct SnakeNode* 这个结构体指针类型重命名成pSnakeNode。
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
typedef struct Snake
{pSnakeNode _pSnake;//维护整条蛇的指针pSnakeNode _pFood;//维护⻝物的指针enum DIRECTION _Dir;//蛇头的⽅向默认是向右enum GAME_STATUS _Status;//游戏状态int _Socre;//当前获得分数int _foodWeight;//默认每个⻝物10分int _SleepTime;//每⾛⼀步休眠时间
}Snake, * pSnake;
蛇的⽅向,可以⼀⼀列举,使⽤枚举
//⽅向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};
游戏状态,可以⼀⼀列举,使⽤枚举
//游戏状态
enum GAME_STATUS
{OK,//正常运⾏KILL_BY_WALL,//撞墙KILL_BY_SELF,//咬到⾃⼰END_NOMAL//正常结束,按ESC按键
};
3.核心逻辑的实现
首先,需要创建三个文件,snake.h、snake.c和test.c,其中snake.h里面需要放入数据节后的设计和函数的声明,snake.c里面是贪吃蛇游戏实现的整个核心,test.c是测试代码。如果不熟悉函数文件分写操作,可以点击以下链接,在函数声明与定义中多个文件下有介绍。http://t.csdnimg.cn/E973phttp://t.csdnimg.cn/E973p
3.1 游戏主逻辑
在test.c中,先修改为本地模式,封装一个test函数,内部装游戏结束后再来一局的逻辑,ch接受键盘上的输入的字符。SetPos是改变输入文字的位置。
#include "snake.h"
#include <locale.h>void test()
{int ch = 0;do{Snake snake = { 0 };GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("再来⼀局吗?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y');SetPos(0, 27);
}int main()
{//修改当前地区为本地模式,为了⽀持中⽂宽字符的打印setlocale(LC_ALL, "");//测试逻辑test();return 0;
}
3.2 游戏开始
void GameStart(pSnake ps)
{//设置控制台的信息,窗口大小,窗口名system("mode con cols=100 lines=30");system("title 贪吃蛇");//隐藏光标HideCursor();//打印欢迎信息WelcomeToGame();//绘制地图CreateMap();//初始化蛇InitSnake(ps);//创建食物CreateFood(ps);
}
3.2.1 隐藏光标
void HideCursor()
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取光标信息CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(handle, &CursorInfo);//修改光标值CursorInfo.bVisible = false;SetConsoleCursorInfo(handle, &CursorInfo);
}
3.2.2 打印欢迎信息
在游戏正式开始之前,做⼀些功能提醒。
void WelcomeToGame()
{//欢迎信息SetPos(38, 12);printf("欢迎来到贪吃蛇小游戏\n");SetPos(39, 17);system("pause");system("cls");//功能介绍信息SetPos(28, 12);printf("用↑ ↓ ← → 来控制蛇的移动,F3加速,F4是减速");SetPos(37, 13);printf("加速能得到更高的分数");SetPos(39, 17);system("pause");system("cls");//清空屏幕
}
3.2.3 绘制地图
创建地图就是将墙打印出来,因为是宽字符打印,所有使⽤wprintf函数,打印格式串前使⽤L打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
墙体打印的宽字符:
#define WALL L'■'
创建地图函数CreateMap
void CreateMap()
{int i = 0;//上(0,0)-(56, 0)SetPos(0, 0);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//左//x是0,y从1开始增⻓for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}//x是56,y从1开始增⻓for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}
3.2.4 初始化蛇身
蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
再设置当前游戏的状态,蛇移动的速度,默认的⽅向,初始成绩,蛇的状态,每个⻝物的分数。其中蛇休眠的速度是通过Sleep函数实现,初始值200毫秒。
蛇⾝打印的宽字符:
#define BODY L'●'
初始化蛇⾝函数:InitSnake
void InitSnake(pSnake ps)
{//创建5个蛇身的节点pSnakeNode cur = NULL;for (int 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;//头插法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 = 200;ps->status = OK;}
3.2.5 创建食物
- 先随机⽣成⻝物的坐标
- x坐标必须是2的倍数
- ⻝物的坐标不能和蛇⾝每个节点的坐标重复
- 创建⻝物节点,打印⻝物
⻝物打印的宽字符:
#define FOOD L'★'
创建⻝物的函数:CreateFood
void CreateFood(pSnake ps)
{srand((unsigned int)time(NULL));int x = 0;int y = 0;again:do {x = rand() % 53 + 2;y = rand() % 24 + 1;} while(x % 2!= 0);//坐标和蛇的身体的每个节点的坐标比较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");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;SetPos(pFood->x, pFood->y);wprintf(L"%lc", FOOD);
}
3.3 游戏运行
游戏运⾏期间,右侧打印帮助信息,提⽰玩家
- 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
- 如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
- 确定了蛇的⽅向和速度,蛇就可以移动了。
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{ //打印分值信息SetPos(62, 10);printf("总分:%d", ps->Score);SetPos(62, 11);printf("食物的分值:%02d", ps->FoodWeight);//检测按键//上、下、左、右、ESC、空格、F3、F4if (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;ps->FoodWeight += 2;}} else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight >= 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//走一步SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->status == OK);}
3.3.1 KEY_PRESS
检测按键状态,我们封装了⼀个宏,其中VK为前缀的都是宏代表键盘按键的虚拟键值。
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
3.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.萧瑟其中版");
}
3.3.3 蛇身的移动
- 先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标。
- 确定了下⼀个位置后,看下⼀个位置是否是⻝物(NextIsFood),是⻝物就做吃⻝物处理
- (EatFood),如果不是⻝物则做前进⼀步的处理(NoFood)。
- 蛇⾝移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上⾃⼰蛇⾝(KillBySelf),从而影响游戏的状态。
void SnakeMove(pSnake ps)
{//创建下⼀个节点pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("SankeMove():malloc()");return 0;}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;}//下一个坐标是否是食物if (NextIsFood(ps, pNext)){EatFood(ps,pNext);}else{NotEatFood(ps, pNext);}//判断是否咬到自己KillBySelf(ps);//判断是都撞到墙KillByWall(ps);
}
3.3.3.1 NextIsFood
int NextIsFood(pSnake ps, pSnakeNode pNext)
{if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y)return 1;elsereturn 0;
}
3.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);
}
3.3.3.3 NotEatFood
将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,放弃掉蛇⾝的最后⼀个节点。
void NotEatFood(pSnake ps, pSnakeNode pNext)
{//头插法pNext->next = ps->pSnake;ps->pSnake = pNext;//释放尾结点pSnakeNode cur = ps->pSnake;while (cur->next->next){//重新打印我的蛇身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;//易错}
3.3.3.4 KillByWall
void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0|| ps->pSnake->x == 56|| ps->pSnake->y == 0|| ps->pSnake->y == 25){ps->status = KILL_BY_WALL;}
}
3.3.3.5 KillBySelf
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;}cur = cur->next;}
}
3.4 游戏结束
游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。
void GameEnd(pSnake ps)
{SetPos(15, 12);switch (ps->status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_SELF:printf("很抱歉,撞墙了,游戏结束\n");break;case KILL_BY_WALL:printf("很抱歉,咬到自己了,游戏结束\n");break;}//释放贪吃蛇的链表资源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->pFood);ps = NULL;
}
4.完整代码
完整代码实现,分3个⽂件实现.
4.1 test.c
#include "snake.h"void test()
{int ch = 0;do{ //创建贪吃蛇Snake sanke = { 0 };GameStart(&sanke);//游戏开始前的初始化GameRun(&sanke);//游戏过程GameEnd(&sanke);//善后工作SetPos(20, 15);printf("再来一局吗?(Y/N):");ch = getchar();getchar();//清理\n} while (ch == 'Y' || ch == 'y');
}int main()
{//修改适配本地中文环境setlocale(LC_ALL, "");test();//贪吃蛇游戏的测试SetPos(0, 26);return 0;
}
4.2 snake.h
#pragma once
#include <Windows.h>
#include <locale.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <time.h>#define WALL L'■'
#define BODY L'●'
#define FOOD L'★'
//默认蛇身坐标
#define POS_X 24
#define POS_Y 5#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1 ? 1:0)enum GAME_STATUS
{OK = 1,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 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 GameStart(pSnake ps);//打印欢迎的界面
void WelcomeToGame();//设置光标位置
void SetPos(short x, short y);//绘制地图
void CreateMap();//初始化贪吃蛇
void InitSnake(pSnake ps);//创建食物
void CreateFood(pSnake ps);//整个游戏运行逻辑
void GameRun(pSnake ps);//蛇的移动
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 KillBySelf(pSnake ps);//善后工作
void GameEnd(pSnake ps);
4.3 snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "snake.h"void SetPos(short x, short y)
{//获得设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//根据句柄设置光标信息COORD pos = { x, y };SetConsoleCursorPosition(handle, pos);
}void HideCursor()
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取光标信息CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(handle, &CursorInfo);//修改光标值CursorInfo.bVisible = false;SetConsoleCursorInfo(handle, &CursorInfo);
}void WelcomeToGame()
{//欢迎信息SetPos(38, 12);printf("欢迎来到贪吃蛇小游戏\n");SetPos(39, 17);system("pause");system("cls");//功能介绍信息SetPos(28, 12);printf("用↑ ↓ ← → 来控制蛇的移动,F3加速,F4是减速");SetPos(37, 13);printf("加速能得到更高的分数");SetPos(39, 17);system("pause");system("cls");//清空屏幕
}void CreateMap()
{int i = 0;//上(0,0)-(56, 0)SetPos(0, 0);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//下(0,26)-(56, 26)SetPos(0, 26);for (i = 0; i < 58; i += 2){wprintf(L"%c", WALL);}//左//x是0,y从1开始增⻓for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%c", WALL);}//x是56,y从1开始增⻓for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%c", WALL);}
}void InitSnake(pSnake ps)
{//创建5个蛇身的节点pSnakeNode cur = NULL;for (int 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;//头插法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 = 200;ps->status = OK;}void CreateFood(pSnake ps)
{srand((unsigned int)time(NULL));int x = 0;int y = 0;again:do {x = rand() % 53 + 2;y = rand() % 24 + 1;} while(x % 2!= 0);//坐标和蛇的身体的每个节点的坐标比较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");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;SetPos(pFood->x, pFood->y);wprintf(L"%lc", FOOD);
}void GameStart(pSnake ps)
{//设置控制台的信息,窗口大小,窗口名system("mode con cols=100 lines=30");system("title 贪吃蛇");//隐藏光标HideCursor();//打印欢迎信息WelcomeToGame();//绘制地图CreateMap();//初始化蛇InitSnake(ps);//创建食物CreateFood(ps);//system("pause");
}void PrintHelpInfo()
{SetPos(62, 15);printf("1.不能穿墙,不能咬到自己");SetPos(62, 16);printf("2.用↑↓←→ 来控制蛇的移动");SetPos(62, 17);printf("3.F3加速,F4是减速");SetPos(62, 18);printf("4.萧瑟其中版");}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;elsereturn 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->next = ps->pSnake;ps->pSnake = pNext;//释放尾结点pSnakeNode cur = ps->pSnake;while (cur->next->next){//重新打印我的蛇身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 == 25){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;}cur = cur->next;}
}void SnakeMove(pSnake ps)
{//创建下⼀个节点pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("SankeMove():malloc()");return 0;}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;}//下一个坐标是否是食物if (NextIsFood(ps, pNext)){EatFood(ps,pNext);}else{NotEatFood(ps, pNext);}//判断是否咬到自己KillBySelf(ps);//判断是都撞到墙KillByWall(ps);
}void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{ //打印分值信息SetPos(62, 10);printf("总分:%d", ps->Score);SetPos(62, 11);printf("食物的分值:%02d", ps->FoodWeight);//检测按键//上、下、左、右、ESC、空格、F3、F4if (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;ps->FoodWeight += 2;}} else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight >= 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//走一步SnakeMove(ps);//睡眠一下Sleep(ps->SleepTime);} while (ps->status == OK);}void GameEnd(pSnake ps)
{SetPos(15, 12);switch (ps->status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_SELF:printf("很抱歉,撞墙了,游戏结束\n");break;case KILL_BY_WALL:printf("很抱歉,咬到自己了,游戏结束\n");break;}//释放贪吃蛇的链表资源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->pFood);ps = NULL;
}
总结
如果你已经坚持看到末尾,我相信你已经了解了个大概。现在可以动手试试,写出属于你的贪吃蛇小游戏,你也可以扩展许多功能,如两个玩家操纵两条蛇,使用你之前学习到的编程知识,获得成就感和乐趣!
创作十分不易,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!