超级详细——手撕贪吃蛇小游戏!

目录

前言

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/E973picon-default.png?t=N7T8http://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 游戏运行

游戏运⾏期间,右侧打印帮助信息,提⽰玩家

  1. 根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
  2. 如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
  3. 确定了蛇的⽅向和速度,蛇就可以移动了。
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 蛇身的移动

  1. 先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标。
  2. 确定了下⼀个位置后,看下⼀个位置是否是⻝物(NextIsFood),是⻝物就做吃⻝物处理
  3. (EatFood),如果不是⻝物则做前进⼀步的处理(NoFood)。
  4. 蛇⾝移动后,判断此次移动是否会造成撞墙(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;
}


总结

如果你已经坚持看到末尾,我相信你已经了解了个大概。现在可以动手试试,写出属于你的贪吃蛇小游戏,你也可以扩展许多功能,如两个玩家操纵两条蛇,使用你之前学习到的编程知识,获得成就感和乐趣!

创作十分不易,如果喜欢这篇文章,请留下你的三连哦,你的支持的我最大的动力!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/661395.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

物联网浏览器(IoTBrowser)-Modbus协议集成和测试

Modbus协议在应用中一般用来与PLC或者其他硬件设备通讯&#xff0c;Modbus集成到IoTBrowser使用串口插件模式开发&#xff0c;不同的是采用命令函数&#xff0c;具体可以参考前面几篇文章。目前示例实现了Modbus-Rtu和Modbus-Tcp两种&#xff0c;通过js可以与Modbus进行通讯控制…

【EI会议征稿中|ACM出版】#先投稿,先送审#第三届网络安全、人工智能与数字经济国际学术会议(CSAIDE 2024)​

#先投稿&#xff0c;先送审#ACM出版#第三届网络安全、人工智能与数字经济国际学术会议&#xff08;CSAIDE 2024&#xff09; 2024 3rd International Conference on Cyber Security, Artificial Intelligence and Digital Economy 2024年3月8日-10日 | 中国济南 会议官网&…

oracle数据库慢查询SQL

目录 场景&#xff1a; 环境&#xff1a; 慢SQL查询一&#xff1a; 问题一&#xff1a;办件列表查询慢 分析&#xff1a; 解决方法&#xff1a; 问题二&#xff1a;系统性卡顿 分析&#xff1a; 解决方法&#xff1a; 慢SQL查询二 扩展&#xff1a; 场景&#xff1a; 线…

CXO清单:低代码平台必备的16个基本功能:从需求到实现的全面指南

对于 CIO、CTO 和 CDO&#xff08;在此统称为 CXO&#xff09;来说&#xff0c;认识到快速变化的技术和竞争格局以及他们在组织中的角色变化至关重要。处理持续不断的软件开发请求、考虑不断变化的业务流程、提高客户和法规的透明度、提高企业数据安全性以及在短时间内扩展基础…

精酿啤酒:麦芽汁的煮沸与沸腾时间的影响

在啤酒酿造过程中&#xff0c;麦芽汁的煮沸与沸腾时间是关键的工艺参数&#xff0c;对啤酒的品质和口感具有显著影响。对于Fendi Club啤酒来说&#xff0c;合理控制煮沸与沸腾时间更是重要。 首先&#xff0c;麦芽汁的煮沸时间对啤酒的口感和稳定性有重要影响。煮沸时间过短&am…

如何使用宝塔面板搭建MySQL 5.5数据库并实现公网远程连接

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar3.2 创建HTTP隧道 4.远程连接5.固定TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单几步,通过宝塔面板cp…

linux系统上C程序的编译、运行及调试-gcc

gcc -o timer timer.c &#xff1a;生成可执行文件main&#xff0c;依托main.c,也可依托多个文件./timer :运行代码

Skywalking的Trace Profiling 代码级性能剖析功能应用详解

代码级性能剖析 Skywalking 提供了Trace Profiling功能对具体出现问题的span进行代码级性能剖析。 代码级性能剖析就是利用方法栈快照&#xff0c;并对方法执行情况进行分析和汇总。并结合有限的分布式追踪 span 上下文&#xff0c;对代码执行速度进行估算。性能剖析激活时&a…

[C#][opencvsharp]winform实现自定义卷积核锐化和USM锐化

【锐化介绍】 图像锐化(image sharpening)是补偿图像的轮廓&#xff0c;增强图像的边缘及灰度跳变的部分&#xff0c;使图像变得清晰&#xff0c;分为空间域处理和频域处理两类。图像锐化是为了突出图像上地物的边缘、轮廓&#xff0c;或某些线性目标要素的特征。这种滤波方法…

详细分析SpringSecurity中的@PreAuthorize注解

目录 1. 基本知识2. 使用方式2.1 配置类2.2 直接使用 1. 基本知识 在Java中&#xff0c;PreAuthorize 是Spring Security框架中的一个注解&#xff0c;用于在方法调用之前对用户的权限进行验证。 允许在方法级别定义访问控制规则&#xff0c;确保只有满足指定条件的用户才能调…

Java基础 集合(二)List详解

目录 简介 数组与集合的区别如下&#xff1a; 介绍 AbstractList 和 AbstractSequentialList Vector 替代方案 Stack ArrayList LinkedList 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界…

nodejs+vue+ElementUi家庭美食菜谱分享网站_in9c2

&#xff08;设计制作有一定的安全性&#xff1b;数据库方面主要采用的是MySQL来进行开发&#xff0c;其特点是稳定性好&#xff0c;数据库存储容量大&#xff0c;处理能力快等优势&#xff1b;服务器采用的是Tomcat服务&#xff0c;能够提供稳固的运行平台&#xff0c;确保系统…

JavaSE-项目小结-IP归属地查询(本地IP地址库)

一、项目介绍 1. 背景 IP地址是网络通信中的重要标识&#xff0c;通过分析IP地址的归属地信息&#xff0c;可以帮助我们了解访问来源、用户行为和网络安全等关键信息。例如应用于网站访问日志分析&#xff1a;通过分析访问日志中的IP地址&#xff0c;了解网站访问者的地理位置分…

Duplicate entry ‘2020045-2-1‘ for key ‘index_uid‘ 解决方案

项目场景&#xff1a; 今天小编在工作中编写接口对数据库增加相同的非主键数据的时候&#xff0c;突然出现了这样的一个错误&#xff1a; 下面我来给大家解答这个错误的出现原因以及解决办法。 问题描述 Duplicate entry 2020045-2-1 for key index_uid 这个错误大概意思就是…

企业的多域名SSL证书

多域名SSL证书作为一种加密通信的方式&#xff0c;可以有效保护多个网站的用户数据在传输过程中的安全。不管个人或者企事业单位 都可以申请多域名SSL证书&#xff0c;提高网站的安全性&#xff0c;保护网站数据传输安全。今天就随SSL盾了解多域名SSL证书旗下的企业多域名SSL证…

深度解读NVMe计算存储协议-3

在NVMe计算存储架构中&#xff0c;Copy命令用于在不同类型的命名空间之间进行数据复制&#xff1a; Memory Copy命令&#xff1a;定义于SLM&#xff08;Subsystem Local Memory&#xff09;命令集&#xff0c;主要用于从非易失性存储命名空间&#xff08;NVM namespaces&#x…

ERP系统助力车间生产:班组、设备、工序一网打尽!实现生产全流程可视化!

​随着企业生产规模的扩大和业务复杂性的增加&#xff0c;车间管理在企业运营中的地位日益突出。ERP系统作为企业资源管理的核心平台&#xff0c;为车间管理提供了全面的解决方案。通过合理配置和使用ERP系统的功能模块&#xff0c;企业可以优化生产流程、提高生产效率、确保产…

SOME/IP SD 协议介绍(二) SOME/IP-SD消息格式

SOME/IP-SD消息格式 通用要求 服务发现消息应通过UDP进行支持。准备将服务发现消息传输到TCP中以供将来使用情况。服务发现消息应以SOME/IP头开始&#xff0c;如图1所示&#xff1a; • 服务发现消息应使用0xFFFF的Service-ID&#xff08;16位&#xff09;。 • 服务发现消息…

榜单!高阶智驾冲刺10%搭载率,哪些玩家占据自研感知「高地」

得「感知」者&#xff0c;是智能化尤其是智能驾驶技术变革快速演进期的受益者。尤其是对于车企来说&#xff0c;规控自研易&#xff0c;感知自研难。 尤其是过去几年时间&#xff0c;基于机器学习和深度学习&#xff0c;TransformerBEV技术进一步提高对异常行为的预测准确性&am…

CHS_06.2.3.4_2+用信号量实现进程互斥、同步、前驱关系

CHS_06.2.3.4_2用信号量实现进程互斥、同步、前驱关系 知识总览信号量机制实现进程互斥信号量机制实现进程同步信号量机制实现前驱关系 知识回顾 各位同学 大家好 在这个小节中 我们要学习怎么用信号量机制来实现进程的同步互制关系 知识总览 那么 我们之前学习了互斥的几种软…