目录

  • 1. 前言
  • 2. Win32 API 介绍
    • 2.1 Win32 API
    • 2.2 控制台程序
    • 2.3 控制台屏幕上的坐标COORD
    • 2.4 GetStdHandle
    • 2.5 GetConsoleCursorInfo
      • 2.5.1 CONSOLE_CURSOR_INFO
    • 2.6 SetConsoleCursorInfo
    • 2.7 SetConsoleCursorPosition
    • 2.8 GetAsyncKeyState
  • 3. 贪吃蛇游戏设计与分析
    • 3.1 地图
      • 3.1.1 “本地化
      • 3.1.2 类项
      • 3.1.3 宽字符的打印
      • 3.1.4 地图坐标
    • 3.2 蛇身和食物
    • 3.3 数据结构设计
      • 3.3.1 蛇的方向
      • 3.3.2 游戏状态
    • 3.4 游戏流程设计
  • 4.核心逻辑实现分析
    • 4.1 游戏主逻辑
    • 4.2 游戏开始(GameStart)
      • 4.2.1 打印欢迎界面
      • 4.2.2 创建地图
      • 4.2.3 初始化身
      • 4.2.4 创建第一个食物
    • 4.3 游戏运行(GameRun)
      • 4.3.1 蛇身移动
        • 4.3.1.1 判断蛇头到达的坐标处是否是食物
        • 4.3.1.2 判断蛇头的坐标是否和墙的坐标冲突
        • 4.3.1.3 判断蛇头的坐标是否和蛇身体的坐标冲突
    • 4.4 游戏结束
  • 5. 附代码
    • 5.1 snake.h
    • 5.2 snake.c
    • 5.3 test.c

1. 前言


我们要用C语言来实现贪吃蛇游戏之前,得了解C语言函数、枚举、结构体、动态内存管理、预处理指令、链表和Win32 API等等的一些相关知识。
关于链表和函数,在之前的文章中都有写过了,友友们可以自行查看。剩下知识会在之后的博客中所提及,请大家多多关注。
这里主要介绍Win32 API,及如何一步一步实现贪吃蛇游戏。

2. Win32 API 介绍

下面介绍的结构体和函数在Win32 API 都是现成的,我们就了解一下如何使用就行。

2.1 Win32 API

Windows这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它同时也是⼀个很大的服务中心,调⽤这个服务中心的各种服务(每⼀种服务就是⼀个函数),可以帮应用程序达到开启视窗、描绘图形、使⽤周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之称ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows32位平台的应用程序编程接口。

2.2 控制台程序

不知道大家知不知道cmd的程序。
平常我们运行起来的黑框程序其实就是控制台程序
来看看它是什么样的。

我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小。像30行,100列,这些都是可以的。
也可以调用C语言函数system来执行不过在使用时要包含#include

 system("mode con cols=20 lines=20");

我们也可以通过命令设置控制台窗⼝的名字:

system("title 贪吃蛇");

看看效果

2.3 控制台屏幕上的坐标COORD

COORD是WindowsAPI中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0) 的原点位于缓冲区的顶部左侧单元格。
就是所示这样

2.4 GetStdHandle

GetStdHandle是一个WindowsAPI函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
就是GetStdHandle来获得某一种设备的控制权限。

HANDLE GetStdHandle(DWORD nStdHandle);

举个例子:如果我们想获得输出程序的句柄
那我们得调用GetStdHandle这个函数。

HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE)

2.5 GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息。

BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo ); 

PCONSOLE_CURSOR_INFO 是指向CONSOLE_CURSOR_INFO结构的指针,该结构接收有关主机游标(光标)的信息。

2.5.1 CONSOLE_CURSOR_INFO

这个结构体,包含有关控制台光标的信息。

typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOLbVisible; } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  1. dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
  2. bVisible,游标的可见性。如果光标可见,则此成员为TRUE。

bVisible = false就是为了隐藏控制台光标。

CursorInfo.bVisible = false; 
int main(){HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO cursor_info;GetConsoleCursorInfo(handle, &cursor_info);cursor_info.dwSize = 100;cursor_info.bVisible = true;SetConsoleCursorInfo(handle, &cursor_info);return 0;}

把dwSize试着设置为dwSize = 100,把bVisible设置我bVisible = true。我们来看看效果:

2.6 SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。

就是在程序运行起来的时候,这个光标是有长度和宽度的。而这些属性是可以设置的,就是放在
CONSOLE_CURSOR_INFO结构体变量中的。

int main(){HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO cursor_info;GetConsoleCursorInfo(handle, &cursor_info);return 0;}

就是把控制台里面的光标信息放在 cursor_info放在里面,所以GetConsoleCursorInfo(handle, &cursor_info),里就传了地址。

2.7 SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
我们来看一个示例:
假设把光标定位到pos = { 5, 20 },输入1后,打印是在下一行打印。

int main(){HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { 5, 20 };SetConsoleCursorPosition(handle, pos);int ch = getchar();putchar(ch);return 0;}

用上面这种方式设置一个坐标比较麻烦,那我们直接封装一个函数来确定一个坐标。
代码来实现一下

void SetPos(int x, int y){HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x, y };SetConsoleCursorPosition(handle, pos);}

那以后需要设置坐标直接调用就行。

我们来试一下在(10,10)处打印一个hi

#include #include #include #include void SetPos(int x, int y){HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x, y };SetConsoleCursorPosition(handle, pos);}int main(){SetPos(10, 10);printf("hi\n");return 0;}

显然是可以的。

2.8 GetAsyncKeyState

因为我们需要知道玩家在键盘上按键,那我们怎么知道呢?

就是用GetAsyncKeyState获取按键情况,GetAsyncKeyState的函数原型如下:

SHORT GetAsyncKeyState( int vKey )

这个函数就一个参数,这个参数指的是键盘上每一个键的虚拟值。
在Win32 API中给键盘上的每一个键都编了号。
我们来看看一部分:

这个函数将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

GetAsyncKeyState的返回值是short类型,在上一次调用GetAsyncKeyState函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

如果我们要判断一个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1。
那我们如何检测呢?

不管这个数字是几,只要按位与1,结果得到1的话,说明被按过,返回的是0,说明没有被按过。

为了方便,我们封装一个PRESS_KET 来检测vk这虚拟键值对应的按键是否被按过
如果按过返回1,未按过返回0

#define KEY_PRESS(vk) ((GetAsyncKeyState(vk)&0x1) " />1:0)

3. 贪吃蛇游戏设计与分析

3.1 地图

我们最终的贪吃蛇大概要是这个样子,那我们的地图如何布置呢?



这里不得不讲一下控制台窗口的一些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍⼀下控制台窗口的坐标知识。
控制台窗口的坐标如图所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
这里再简单的讲一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。

后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型wchar_t 和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

3.1.1 本地化

提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。

就像货币的格式:

在标准中,依赖地区的部分有以下几项:

  1. 数字量的格式
  2. 货币量的格式
  3. 字符集
  4. 日期和时间的表示形式

3.1.2 类项

通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个个宏,指定一个类项:

  1. LC_COLLATE:影响字符串比较较函数strcoll()strxfrm()

  2. LC_CTYPE:影响字符处理函数的⾏为。

  3. LC_MONETARY:影响货币格式。

  4. LC_NUMERIC:影响printf() 的数字格式。

  5. LC_TIME:影响时间格式strftime()wcsftime()

  6. LC_ALL-针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

3.1.3 宽字符的打印

那如果想在屏幕上打印宽字符,怎么打印呢?
宽字符的字面量必须加上前缀“L”,否则C语言会把字面量当作窄字符类型处理。前缀“L”在单引号前面,表示宽字符,对应wprintf() 的占位符为 %lc;在双引号前面,表示宽字符串,wprintf() 的占位符为%ls
我们用代码来看看:

#include #include #include 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;}


3.1.4 地图坐标

而我们想得到这样的:
我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙,
如下:

3.2 蛇身和食物

初始化状态,假设蛇的长度是5,蛇身的每个节点是●,在固定的一个坐标处,比如(24,5)处开始出现蛇,连续5个节点。

注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点有一半出现在墙体中,另外⼀半在墙外的现象,坐标不好对齐。
关于食物,就是在墙体内随机生成一个坐标(x坐标必须是2的倍数),坐标不能和蛇的身体重合,然后打印★。

3.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 DIRECTION _Dir;//描述蛇的方向enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己}Snake, * pSnake;

3.3.1 蛇的方向

蛇就只有四个方向,向上,向下,向左和向右,,可以一一列举出来,所以使用枚举来描述蛇前进方向。
代码实现:

enum DIRECTION{UP = 1,DOWN,LEFT,RIGHT};

3.3.2 游戏状态

就像我们玩的一些游戏一样,要能够知道游戏运行的状态,像正常运行、撞到墙、蛇撞到自己和正常游戏结束,我们同样可以一一例举出来,也使用枚举来描述。
来看看代码实现:

enum GAME_STATUS{OK,//正常运行END_NORMAL,//按ESC退出KILL_BY_WALL,//撞到自己KILL_BY_SELF//正常结束};

3.4 游戏流程设计

同样设置三个文件,一个test.c用来测试代码,一个snake.h用来放函数声明,最后一个snake.c用来放相关函数的实现。

4.核心逻辑实现分析

4.1 游戏主逻辑

主逻辑分为3个过程:

  1. 游戏开始(GameStart)完成游戏的初始化
  2. 游戏运行(GameRun)完成游戏运行逻辑的实现
  3. 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

4.2 游戏开始(GameStart)

在snake.c中来开始对游戏初始化:

  1. 控制台窗口大小的设置
  2. 控制台窗口名字的设置
  3. 鼠标光标的隐藏
  4. 打印欢迎界⾯
  5. 创建地图
  6. 初始化第蛇
  7. 创建第⼀个食物
void GameStart(pSnake ps){//控制台窗口的设置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);//设置控制台光标状态//打印欢迎界面WelComeToGame();//创建地图CreateMap();//初始化贪食蛇InitSnake(ps);//创建食物CreateFood(ps);}

4.2.1 打印欢迎界面

在游戏开始之前,我们需要对玩家有一些提示:
就是像这样的

void WelComeToGame(){SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25);system("pause");//pause是暂停system("cls");SetPos(20, 14);printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");SetPos(40, 25);system("pause");system("cls");}

当做好这些后,就要创建地图了。

4.2.2 创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所有使用wprintf函数,打印格式串前使用L"%c"打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
先在屏幕上打印上和下,它们相差的是两个字符。
而左和右的墙,在循环时,就只相差1就行。

void CreateMap(){//上SetPos(0, 0);int i = 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);}}

最终实现的时候是这样的。

4.2.3 初始化身

蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每一个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身体后,将蛇的每一节打印在屏幕上。

在创建蛇身时,我们使用头插,将蛇的身体节点一个一个插入,但最终还要返回蛇头节点,方便之后进行操作。

void InitSnake(pSnake ps){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;//头插法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->_Status = OK;ps->_Score = 0;ps->_pFood = NULL;ps->_SleepTime = 200;ps->_FoodWeight = 10;ps->_Dir = RIGHT;}

4.2.4 创建第一个食物

要生成食物,先随机生成食物的坐标,而x坐标必须是2的倍数,而且食物物的坐标不能和蛇身体每个节点的坐标重复,也不能和墙的坐标重复。

void CreateFood(pSnake ps){int x = 0;int y = 0;again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x坐标必须是2的倍数//坐标不能和蛇的身体冲突pSnakeNode cur = ps->_pSnake;while (cur){//比较坐标if (cur->x == x && cur->y == 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(x, y);wprintf(L"%lc", FOOD);}

4.3 游戏运行(GameRun)

游戏运行期间,右侧打印帮助信息,提示玩家:
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

void GameRun(pSnake ps){PrintHelpInfo();do{SetPos(64, 10);printf("得分:%05d", ps->_Score);SetPos(64, 11);printf("每个食物的分数:%2d", ps->_FoodWeight);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 = END_NORMAL;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->_SleepTime < 320){ps->_SleepTime += 30;ps->_FoodWeight -= 2;}}Sleep(ps->_SleepTime);SnakeMove(ps);} while (ps->_Status == OK);}

4.3.1 蛇身移动

先创建下一个节点,根据移动方向和蛇头的坐标,蛇移动到下一个位置的坐标。
确定了下一个位置后,需要看下一个位置是否是食物(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;}//判断蛇头到达的坐标处是否是食物if (NextIsFood(ps, pNext)){//吃掉食物EatFood(ps, pNext);}else{//不吃食物NoFood(ps, pNext);}//蛇是否撞墙KillByWall(ps);//蛇是否自杀KillBySelf(ps);}
4.3.1.1 判断蛇头到达的坐标处是否是食物

不管是不是食物,节点都直接插入,而后再做下一步的处理。

判断蛇头到达的坐标处是否是食物,如果是那就吃掉,并创建新的食物。
代码实现

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;}free(ps->_pFood);ps->_Score += ps->_FoodWeight;CreateFood(ps);//新创建食物}

如果不是那就前进,同样是把下一个节点插入,在判断不是食物之后,遍历蛇身,释放最后一个节点。
代码实现:

void NoFood(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;}
4.3.1.2 判断蛇头的坐标是否和墙的坐标冲突

需要判断蛇头的坐标是否和墙的坐标冲突,如果冲突,那游戏就结束,不冲突,就继续前进。
就只需要判断蛇头节点的x是不是0或者56,或者是蛇头节点的y是不是0或26,就行。
相关代码实现:

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.1.3 判断蛇头的坐标是否和蛇身体的坐标冲突

需要判断蛇头节点的坐标是不是与身体的坐标重合,重合则返回的状态为KILL_BY_SEL。

void KillBySelf(pSnake ps){pSnakeNode cur = ps->_pSnake->next;while (cur){if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y){ps->_Status = KILL_BY_SELF;}cur = cur->next;}}

4.4 游戏结束

游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,是主动退出,还是撞到墙了,还是撞到蛇自己了,最后要释放蛇身节点。
释放节点时,采用头删的方式,不要忘记把头节点置为NULL。

void GameEnd(pSnake ps){SetPos(20, 12);switch (ps->_Status){case END_NORMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("自杀了,游戏结束\n");break;case KILL_BY_WALL:printf("撞墙了,游戏结束\n");break;}//释放蛇身的结点pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}ps->_pSnake = NULL;}

5. 附代码

5.1 snake.h

#include #include #include #include #include #include #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 DIRECTION{UP = 1,DOWN,LEFT,RIGHT};enum GAME_STATUS{OK,//正常运行END_NORMAL,//按ESC退出KILL_BY_WALL,KILL_BY_SELF};//贪吃蛇结点的描述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 DIRECTION _Dir;//描述蛇的方向enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己}Snake, * pSnake;//游戏开始 - 完成游戏的初始化动作void GameStart(pSnake ps);//定位坐标void SetPos(short x, short y);//游戏开始的欢迎界面void WelComeToGame();//打印地图void CreateMap();//初始化贪吃蛇void InitSnake(pSnake ps);//创建食物void CreateFood(pSnake ps);//游戏的正常运行void GameRun(pSnake ps);//打印帮助信息void PrintHelpInfo();//游戏暂定和恢复void Pause();//蛇的移动void SnakeMove(pSnake ps);//判断蛇头到达的坐标处是否是食物int NextIsFood(pSnake ps, pSnakeNode pnext);//吃掉食物void EatFood(pSnake ps, pSnakeNode pnext);//不吃食物void NoFood(pSnake ps, pSnakeNode pnext);//蛇是否撞墙void KillByWall(pSnake ps);//蛇是否自杀void KillBySelf(pSnake ps);//游戏结束后的善后处理void GameEnd(pSnake ps);

5.2 snake.c

#include "snake.h"//设置光标的坐标void SetPos(short x, short y){COORD pos = { x, y };HANDLE hOutput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);}void WelComeToGame(){//定位光标SetPos(40, 14);printf("欢迎来到贪吃蛇小游戏");SetPos(40, 25);system("pause");//pause是暂停system("cls");SetPos(20, 14);printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, F3是加速,F4是减速");SetPos(40, 25);system("pause");system("cls");}//void CreateMap()//{////上//SetPos(0, 0);//int i = 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 CreateMap(){//上SetPos(0, 0);int i = 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){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;//头插法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->_Status = OK;ps->_Score = 0;ps->_pFood = NULL;ps->_SleepTime = 200;ps->_FoodWeight = 10;ps->_Dir = RIGHT;}void CreateFood(pSnake ps){int x = 0;int y = 0;again:do{x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x坐标必须是2的倍数//坐标不能和蛇的身体冲突pSnakeNode cur = ps->_pSnake;while (cur){//比较坐标if (cur->x == x && cur->y == 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(x, y);wprintf(L"%lc", FOOD);}void GameStart(pSnake ps){//控制台窗口的设置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);//设置控制台光标状态//打印欢迎界面WelComeToGame();//创建地图CreateMap();//初始化贪食蛇InitSnake(ps);//创建食物CreateFood(ps);}void PrintHelpInfo(){SetPos(64, 15);printf("1.不能撞墙,不能咬到自己");SetPos(64, 16);printf("2.使用 ↑.↓.←.→ 分别控制蛇的移动");SetPos(64, 17);printf("3.F3加速,F4减速");SetPos(64, 18);printf("4.ESC-退出, 空格-暂停游戏");SetPos(64, 20);printf("zxctsclrjjjcph@版权");}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;}free(ps->_pFood);ps->_Score += ps->_FoodWeight;CreateFood(ps);//新创建食物}//不吃食物void NoFood(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 == 26)ps->_Status = KILL_BY_WALL;}//蛇是否自杀void KillBySelf(pSnake ps){pSnakeNode cur = ps->_pSnake->next;while (cur){if (ps->_pSnake->x == cur->x && ps->_pSnake->y == cur->y){ps->_Status = KILL_BY_SELF;}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;}//判断蛇头到达的坐标处是否是食物if (NextIsFood(ps, pNext)){//吃掉食物EatFood(ps, pNext);}else{//不吃食物NoFood(ps, pNext);}//蛇是否撞墙KillByWall(ps);//蛇是否自杀KillBySelf(ps);}void GameRun(pSnake ps){PrintHelpInfo();do{SetPos(64, 10);printf("得分:%05d", ps->_Score);SetPos(64, 11);printf("每个食物的分数:%2d", ps->_FoodWeight);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 = END_NORMAL;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->_SleepTime < 320){ps->_SleepTime += 30;ps->_FoodWeight -= 2;}}Sleep(ps->_SleepTime);SnakeMove(ps);} while (ps->_Status == OK);}void GameEnd(pSnake ps){SetPos(20, 12);switch (ps->_Status){case END_NORMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("自杀了,游戏结束\n");break;case KILL_BY_WALL:printf("撞墙了,游戏结束\n");break;}//释放蛇身的结点pSnakeNode cur = ps->_pSnake;while (cur){pSnakeNode del = cur;cur = cur->next;free(del);}ps->_pSnake = NULL;}

5.3 test.c

#define _CRT_SECURE_NO_WARNINGS 1#include "snake.h"void test(){Snake snake = { 0 };//创建了贪吃蛇//1. 游戏开始 - 初始化游戏GameStart(&snake);//2. 游戏运行 - 游戏的正常运行过程//GameRun(&snake);//3. 游戏结束 - 游戏善后(释放资源)//GameEnd(&snake);int ch = 0;do{Snake snake = { 0 };//创建了贪吃蛇//1. 游戏开始 - 初始化游戏GameStart(&snake);//2. 游戏运行 - 游戏的正常运行过程GameRun(&snake);//3. 游戏结束 - 游戏善后(释放资源)GameEnd(&snake);SetPos(20, 18);printf("再来一局吗?(Y/N):");ch = getchar();getchar();// 清理掉\n} while (ch == 'Y' || ch == 'y');SetPos(0, 27);} int main(){//设置程序适应本地环境setlocale(LC_ALL, "");srand((unsigned int)time(NULL));test();return 0;}

有错误欢迎指出,大家一起进步。
如有转载请标注。