目录1 · 前期准备2 · Win32 API2 - 1 · 控制台程序2 - 2 · 控制台屏幕上的坐标2 - 2 - 1 · COORD2 - 2 - 2 · SetConsoleCursorPosition2 - 3 · 隐藏光标2 - 3 - 1 · GetStdHandle2 - 3 - 2 · GetConsoleCursorInfo2 - 3 - 3 · SetConsoleCursorInfo2 - 4 · 获取按键情况3 · 本地化3 - 1 · 头文件 locale.h3 - 2 · setlocale函数3 - 3 · 打印宽字符4 · 贪吃蛇游戏实现5 · 结构设计6 · 游戏开始6 - 2 · 设置光标位置 SetPos6 - 2 · 欢迎界面6 - 3 · 地图绘制6 - 4 · 初始化打印蛇6 - 5 · 创建食物6 - 6 · 测试7 · 游戏运行7 - 1 · 打印帮助信息7 - 2 · 蛇移动7 - 2 - 1 · 判断下一步是否为食物7 - 2 - 2 · 吃食物7 - 2 - 3 · 下一步没有食物7 - 2 - 4 · 判断是否撞墙7 - 2 - 5 · 判断是否撞到自己8 · 游戏结束9 · 游戏测试总结1 · 前期准备我们实现贪吃蛇需要用到函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。那么我们先需要简单了解一下我们需要用到的 Win32 API 的功能2 ·Win32 APIWindows 这个多作业系统除了协调应⽤程序的执行、分配内存、管理资源之外 它同时也是⼀个很大的服务中心调用这个服务中心的各种服务每⼀种服务就是⼀个函数可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的。由于这些函数服务的对象是应⽤程序(Application) 所以便称之为 Application Programming Interface简称 API 函数。WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。下面简单介绍一下我们将会用到的功能2 - 1 · 控制台程序平时我们运行出来的那个框框就是控制台程序或者终端。本篇对贪吃蛇的模拟实现仅能在控制台程序上。我们可以使⽤cmd命令来设置控制台窗口的长宽设置控制台窗⼝的大小比如mode con cols115 lines35这样就设置了一个 35 行 115列大小大控制台窗口。也可以设置窗口名比如title 贪吃蛇这样就将窗口名设置成了 贪吃蛇。这些命令一般只能在控制台窗口那里输入并运行。在C语言中我们可以使用 system函数来执行。比如system(mode con cols115 lines35); system(title 贪吃蛇); system(cls); system(pause);其中system(cls) 的效果是清理屏幕system(pause) 的效果是暂停程序运行,我们也会用到。2 - 2 · 控制台屏幕上的坐标2 - 2 - 1 · COORDCOORD 是Windows API中定义的⼀个结构体表示⼀个字符在控制台屏幕上的坐标typedef struct _COORD { SHORT X; SHORT Y; } COORD, *PCOORD;可以给坐标赋值COORD pos { 10, 15 };2 - 2 - 2 ·SetConsoleCursorPosition函数原型如下BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD pos );第一个参数是句柄我们下面会介绍。设置指定控制台屏幕缓冲区中的光标位置我们将想要设置的坐标信息放在COORD类型的pos中调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。2 - 3 · 隐藏光标在我们控制台屏幕缓冲区那个一闪一闪的小方块用于提示位置的就是光标。在游戏过程中我们显然不希望有一个一闪一闪的光标来干扰我们为了隐藏光标我们需要用到以下功能2 - 3 - 1 ·GetStdHandleGetStdHandle是⼀个Windows API函数。它⽤于从⼀个特定的标准设备标准输⼊、标准输出或标准错误中取得⼀个句柄用来标识不同设备的数值使用这个句柄可以操作设备。这个函数的参数只能是以下三个中的一个值含义STD_INPUT_HANDLE标准输入设备。 最初这是输入缓冲区CONIN$的控制台。STD_OUTPUT_HANDLE标准输出设备。 最初这是活动控制台屏幕缓冲区CONOUT$。STD_ERROR_HANDLE标准错误设备。 最初这是活动控制台屏幕缓冲区CONOUT$。2 - 3 - 2 ·GetConsoleCursorInfo检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息函数原型如下BOOL WINAPI GetConsoleCursorInfo ( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo );其中第二个参数是一个结构体包含有关控制台光标的信息定义如下typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;其中dwSize由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化范围从完全填充单元格到单元底部的水平线条。bVisible游标的可见性。 如果光标可见则此成员为 TRUE。2 - 3 - 3 ·SetConsoleCursorInfo设置指定控制台屏幕缓冲区的光标的大小和可见性。函数原型如下BOOL WINAPI SetConsoleCursorInfo( HANDLE hConsoleOutput, const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo );2 - 4 · 获取按键情况获取按键情况要用到GetAsyncKeyState函数原型如下SHORT GetAsyncKeyState( int vKey );接收一个参数需要传虚拟键值将键盘上每个键的虚拟键值传递给函数函数通过返回值来分辨按键的状态。GetAsyncKeyState的返回值是short类型在上⼀次调用GetAsyncKeyState函数后如果返回的16位的short数据中最高位是1说明按键的状态是按下如果最高是0说明按键的状态是抬起如果最低位被置为1则说明该按键被按过否则为0。3 · 本地化在游戏地图的打印蛇身的打印食物的打印我们都需要用到宽字符。普通的字符占1字节宽字符占2字节。简单的介绍⼀下C语言的国际化特性相关的知识过去C语言并不适合⾮英语国家地区使用。C语言最初假定字符都是自己的。但是这些假定并不是在世界的任何地方都适用。C语言字符默认是采用ASCII编码的ASCII字符集采用的是单字节编码且只使用了单字节中的低7位最高位是没有使用的。因此ASCII字符集共包含128个字符在英语国家中128个字符是基本够用的但对其他国家每个国家都有自己的不同的需求于是一些国家使用单字节中的最高位。不同国家有不同需求但0--127表示的符号是⼀样的不⼀样的只是128--255的这⼀段。后来为了使C语言适应国际化C语言的标准中不断加入了国际化的支持。比如加入宽字符的类型wchar_t和宽字符的输入和输出函数加入和locale.h头文件其中提供了允许程序员针对特定地区通常是国家或者说某种特定语言的地理区域调整程序行为的函数。3 - 1 · 头文件 locale.hlocale.h提供的函数用于控制C标准库中对于不同的地区会产⽣不⼀样行为的部分。在标准中依赖地区的部分有以下几项数字量的格式货币量的格式字符集日期和时间的表示形式通过修改地区程序可以改变它的⾏为来适应世界的不同区域。但地区的改变可能会影响库的许多部分其中⼀部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改LC_COLLATELC_CTYPELC_MONETARYLC_NUMERICLC_TIMELC_ALL - 针对所有类项修改3 - 2 ·setlocale函数原型如下char* setlocale (int category, const char* locale);setlocale 函数用于修改当前地区可以针对⼀个类项修改也可以针对所有类项。setlocale 的第⼀个参数可以是前面说明的类项中的⼀个那么每次只会影响⼀个类项如果第⼀个参数是LC_ALL就会影响所有的类项。C标准给第⼆个参数仅定义了2种可能取值C和。在任意程序执行开始都会隐藏式执行调用:setlocale(LC_ALL, C);当地区设置为C时库函数按正常方式执行。当程序运行起来后想改变地区就只能显示调用setlocale函数。⽤ 作为第2个参数调用setlocale函数 就可以切换到本地模式这种模式下程序会适应本地环境。注意: 第二个双引号里面不能是空格我自己写的时候在 setlocale的第二个参数的中加了空格结果一直打印不出宽字符。3 - 3 · 打印宽字符需要用到 wprintf 如下#include stdio.h #includelocale.h int main() { setlocale(LC_ALL, ); wchar_t ch L★; printf(12\n); printf(ab\n); wprintf(L%c\n, ch); return 0; }运行一下这里的 ★ 就是宽字符4 · 贪吃蛇游戏实现在了解了上面的知识后我们就可以开始模拟实现贪吃蛇了。我们大致分为三个部分游戏开始(初始化)游戏运行游戏结束5 · 结构设计如下#include stdio.h #include stdlib.h #include locale.h #include Windows.h #include stdbool.h #include time.h typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode; enum DIRECTION { UP, DOWN, LEFT, RIGHT }; enum STATE { OK, KILL_BY_WALL, KILL_BY_SELF, NORMAL_END }; typedef struct SnakeBasic { SnakeNode* _psnake;//指向整条蛇 SnakeNode* _pfood;//食物 enum DIRECTION _dir;//方向 enum STATE _state;//状态 int _foodWeight;//食物得分权重 int _score;//总得分 int _sleeptime;//移动速度 }Snake;我们用链表来存储蛇身的每个结点每个结点只需要存储一个坐标即可方便我们进行定位。同时我们定义了一个结构体用来维护我们整个游戏的基本数据。方向可以一一列举所以用上了枚举。游戏运行的状态也可以一一列举也用上了枚举。6 · 游戏开始代码如下void GameStart(Snake* ps) { //运行前准备 //本地化 setlocale(LC_ALL, ); //设置窗口 system(mode con cols110 lines35); system(title 贪吃蛇); //设置光标 HANDLE HOutPut NULL; HOutPut GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(HOutPut, CursorInfo); CursorInfo.bVisible false; SetConsoleCursorInfo(HOutPut, CursorInfo); //欢迎界面 WelComeToGame(); //打印地图 CreateMap(); //初始化蛇 InitSnake(ps); //创建食物 CreateFood(ps); }我们先进行运行前准备本地化设置窗口大小与窗口名以及隐藏光标。这里我设置的窗口大小为 110列 * 35行窗口大小可以根据自己的喜好进行修改。6 - 2 · 设置光标位置 SetPos由于定位这个功能我们会很频繁的进行使用而定位实际上需要写的代码还挺长因此我们写一个函数方便后面进行定位这个功能如下//设置光标位置,x为列y为行 void SetPos(int x, int y) { COORD pos { x, y }; HANDLE HOutPut NULL; //拿句柄 HOutPut GetStdHandle(STD_OUTPUT_HANDLE); //设置光标位置 SetConsoleCursorPosition(HOutPut, pos); }6 - 2 · 欢迎界面在游戏开始之前我们先要对玩家进行游戏提醒。代码如下void WelComeToGame() { SetPos(42, 16); printf(欢迎来到贪吃蛇游戏); SetPos(40, 30); system(pause); system(cls); SetPos(30, 16); printf(使用 W,A,S,D 来控制蛇的移动方向); SetPos(30, 17); printf(短按F1加速短按F2减速加速可以得到更多分数); SetPos(40, 30); system(pause); system(cls); }我们运行一下看看:按任意键之后:6 - 3 · 地图绘制我们使用 ■ 这个宽字符来作为墙体。为了方便使用我们用 #define 定义#define WALL L■代码如下void CreateMap() { int i 0; // 66 * 28大小 for (i 0; i 66; i 2) { wprintf(L%c, WALL); } for (i 0; i 66; i 2) { SetPos(i, 28); wprintf(L%c, WALL); } for (i 0; i 28; i) { SetPos(0, i); wprintf(L%c, WALL); } for (i 0; i 28; i) { SetPos(66, i); wprintf(L%c, WALL); } }我们的地图占据偏左侧的一大块区域屏幕右侧我们留着打印帮助信息与得分。定位之后进行打印需要注意的是一个宽字符占 2列 * 1行的空间。6 - 4 · 初始化打印蛇我们用 ● 这个宽字符作为蛇身为了方便使用我们用了 #define#define BODY L●同时也用 #define 定义了蛇的初始位置:#define START_POS_X 26 #define START_POS_Y 6当然可按个人喜好修改。代码如下void InitSnake(Snake* ps) { int i 0; for(i0; i5; i) { SnakeNode* newnode (SnakeNode*)malloc(sizeof(SnakeNode)); if (newnode NULL) { perror(snake malloc); exit(1); } //确定初始位置 newnode-x START_POS_X i * 2; newnode-y START_POS_Y; newnode-next NULL; //头插 if (ps-_psnake NULL) { ps-_psnake newnode; } else { newnode-next ps-_psnake; ps-_psnake newnode; } } //打印蛇 SnakeNode* pcur ps-_psnake; while (pcur) { SetPos(pcur-x, pcur-y); wprintf(L%c, BODY); pcur pcur-next; } //初始化其他基础 ps-_dir RIGHT;//初始方向为右 ps-_foodWeight 10; ps-_score 0; ps-_sleeptime 200; ps-_state OK; }Snake 是我们用来维护整个游戏的基本数据的结构体。先进行对蛇的初始化创建五个结点并连接然后打印蛇顺手对其他基本数据进行赋值。6 - 5 · 创建食物我们使用 ★ 这个宽字符来作为食物。为了方便使用我们用 #define 定义#define FOOD L★代码如下void CreateFood(Snake* ps) { SnakeNode* food (SnakeNode*)malloc(sizeof(SnakeNode)); if (food NULL) { perror(food malloc); exit(1); } again: //随机生成坐标x应为2的倍数 // 2 x 64 food-x (rand() % 32 1) * 2; // 1 y 27 food-y rand() % 27 1; //并且不能与蛇身重叠 SnakeNode* pcur ps-_psnake; while (pcur) { if (food-x pcur-x food-y pcur-y) { goto again; } pcur pcur-next; } //打印食物 SetPos(food-x, food-y); wprintf(L%c, FOOD); ps-_pfood food; }需要注意的是我们蛇身是宽字符因此食物应出现在 列为2的倍数的地方并且食物需要在地图内且不能与蛇重叠。我们随机生成了食物的 x 和 y 坐标然后进行判断如果不符合就goto 回到 again 进行重新随机生成坐标直到坐标合法。最后打印食物并将食物结点保存方便后续判断。6 - 6 · 测试我们对GameStart 运行一下7 · 游戏运行为了判断按键我们写了一个宏//检测按键 #define KEY_PRESS(VK) ((GetAsyncKeyState(VK) 1) ? 1 : 0)用按位与上一个1来判断最低位是否为1由此判断某个按键有没有被按下。代码如下void GameRun(Snake* ps) { PrintHelp(ps); do { //打印分数与果实分数 SetPos(77, 12); printf(当前得分:%d, ps-_score); SetPos(77, 13); printf(当前单个果实分数:%2d, ps-_foodWeight); //判断按键 if(KEY_PRESS(W) ps-_dir ! DOWN) { ps-_dir UP; } else if(KEY_PRESS(A) ps-_dir ! RIGHT) { ps-_dir LEFT; } else if(KEY_PRESS(S) ps-_dir ! UP) { ps-_dir DOWN; } else if(KEY_PRESS(D) ps-_dir ! LEFT) { ps-_dir RIGHT; } else if (KEY_PRESS(VK_F1)) { if (ps-_sleeptime 80) { ps-_sleeptime - 30; ps-_foodWeight 2; } } else if (KEY_PRESS(VK_F2)) { if (ps-_sleeptime 320) { ps-_sleeptime 30; ps-_foodWeight - 2; } } else if (KEY_PRESS(VK_SPACE)) { SetPos(25, 17); system(pause); SetPos(25, 17); printf( ); } else if (KEY_PRESS(VK_ESCAPE)) { ps-_state NORMAL_END; } Sleep(ps-_sleeptime); SnakeMove(ps); } while (ps-_state OK); }先打印帮助信息随后需要实时打印分数与当前吃一个果实的得分。随后获取按键情况W,A,S,D 就调整蛇的方向F1就进行加速同时提高吃果实的得分这里设置了加速的上限也可以通过吃一个果实的得分来设置。F2就进行减速同时降低吃果实的得分这里设置了减速的下限也可以通过吃一个果实的得分来设置。加速和减速其实就是对 Sleep 的时间进行调整Sleep函数的功能是让程序休眠休眠时间为传的参数单位为毫秒。因此休眠时间越短蛇走的是更快的。ESC 就退出游戏即将游戏状态改为 NORMAL_END空格就暂停游戏光标定位到一个地图内偏下合适位置随后 system(pause)。游戏运行一直循环直到此时游戏状态不为 OK。7 - 1 · 打印帮助信息代码如下void PrintHelp(Snake* ps) { SetPos(77, 18); printf(不能撞墙不能咬到自己); SetPos(77, 19); printf(用 W,A,S,D 进行移动); SetPos(77, 20); printf(短按F1可加速短按F2可减速); SetPos(77, 21); printf(最高四档); SetPos(77, 22); printf(按ESC退出游戏按空格暂停); }在屏幕右侧提示玩家。7 - 2 · 蛇移动代码如下void SnakeMove(Snake* ps) { SnakeNode* next (SnakeNode*)malloc(sizeof(SnakeNode)); if (next NULL) { perror(Move malloc); exit(1); } //判断下一步的位置 switch (ps-_dir) { case UP: next-x ps-_psnake-x; next-y ps-_psnake-y - 1; break; case LEFT: next-x ps-_psnake-x - 2; next-y ps-_psnake-y; break; case DOWN: next-x ps-_psnake-x; next-y ps-_psnake-y 1; break; case RIGHT: next-x ps-_psnake-x 2; next-y ps-_psnake-y; break; } //判断下一步是否是食物 if (NextIsFood(ps, next)) { //吃食物 EatFood(ps, next); } else { NoFood(ps, next); } KillByWall(ps); KillBySelf(ps); }先确定下一步的位置然后判断是不是食物以及走了一步之后会不会导致蛇死亡。7 - 2 - 1 · 判断下一步是否为食物代码如下bool NextIsFood(Snake* ps, SnakeNode* pnext) { return (ps-_pfood-x pnext-x ps-_pfood-y pnext-y); }如果下一步的位置与食物重合说明下一步是食物。7 - 2 - 2 · 吃食物代码如下void EatFood(Snake* ps, SnakeNode* pnext) { //头插 pnext-next ps-_psnake; ps-_psnake pnext; //打印蛇 SnakeNode* pcur ps-_psnake; while (pcur) { SetPos(pcur-x, pcur-y); wprintf(L%c, BODY); pcur pcur-next; } ps-_score ps-_foodWeight; //释放旧的食物结点 free(ps-_pfood); //重新创建食物 CreateFood(ps); }蛇吃了食物身体是会变长一个结点的因此只需要将食物的结点头插进链表然后重新打印蛇再重新创建一个食物即可。防止内存泄漏先释放旧的食物结点。7 - 2 - 3 · 下一步没有食物代码如下void NoFood(Snake* ps, SnakeNode* pnext) { //头插 pnext-next ps-_psnake; ps-_psnake pnext; //尾删 SnakeNode* pcur ps-_psnake; while (pcur-next-next) { SetPos(pcur-x, pcur-y); wprintf(L%c, BODY); pcur pcur-next; } SnakeNode* ptail pcur-next; //用空格覆盖原蛇尾 SetPos(ptail-x, ptail-y); printf( ); free(ptail); ptail NULL; pcur-next NULL; }先将下一步的结点进行头插因为没吃到食物因此蛇身长度不变还要对链表进行尾删。由于我们之前已经在原表尾的坐标打印了蛇的一节身体因此要对原表尾的地方进行覆盖由于蛇身是宽字符所以需要用两个空格 来覆盖。7 - 2 - 4 · 判断是否撞墙代码如下bool KillByWall(Snake* ps) { if ((ps-_psnake-x 0) || (ps-_psnake-x 66) || (ps-_psnake-y 0) || (ps-_psnake-y 28)) { ps-_state KILL_BY_WALL; return true; } return false; }对蛇头的坐标进行判断与墙所在的 x 或 y 坐标有重合即为撞墙。需要修改 Snake 结构体中的 _state 即修改此时游戏状态以退出游戏运行的循环。7 - 2 - 5 · 判断是否撞到自己代码如下bool KillBySelf(Snake* ps) { SnakeNode* pcur ps-_psnake-next; while (pcur) { if (pcur-x ps-_psnake-x pcur-y ps-_psnake-y) { ps-_state KILL_BY_SELF; return true; } pcur pcur-next; } return false; }由于此时已经运行过 EatFood 或 NoFood所以此时我们已经更新了蛇身的链表所以只需判断此时的蛇头是否与蛇身有重叠。需要修改 Snake 结构体中的 _state 即修改此时游戏状态以退出游戏运行的循环。8 · 游戏结束代码如下void GameEnd(Snake* ps) { SetPos(33, 17); switch (ps-_state) { case KILL_BY_WALL: printf(你撞墙了游戏结束); break; case KILL_BY_SELF: printf(你咬到自己了游戏结束); break; case NORMAL_END: printf(你主动退出游戏结束); break; } //销毁蛇 SnakeNode* pcur ps-_psnake; SnakeNode* del NULL; while (pcur) { del pcur; pcur pcur-next; free(del); } }当游戏状态不为 OK 时此时游戏就结束了我们需要打印信息来告知玩家为何结束。并且需要对蛇的链表进行销毁以防止内存泄漏。9 · 游戏测试代码如下#include Snake.h int main() { int ch 0; srand((unsigned int)time(NULL)); do { Snake snake { 0 }; GameStart(snake); GameRun(snake); GameEnd(snake); SetPos(33, 18); printf(再来一局吗 Y / N); ch getchar(); while(getchar() ! \n); } while (ch y || ch Y); SetPos(0, 30); return 0; }srand 是为了确保 rand 出来的随机数是随机的。当游戏结束可以询问玩家是否再开一把。下面的 while(getchar() ! \n); 是读取掉回车键防止下次询问时系统读取到的是上一次询问的回车键。总结以上简单对贪吃蛇游戏进行了模拟实现后续更新仍为数据结构。以上内容如有错误或不准确之处欢迎指出或者你有更好的想法也欢迎交流。