C语言实战项目--贪吃蛇

        贪吃蛇是久负盛名的游戏之一,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的行列。在编程语言的教学中,我们以贪吃蛇为例,从设计到代码实现来提升大家的编程能⼒和逻辑能⼒。

        在本篇讲解中,我们会看到很多陌生的知识,但是不必惊慌,你只需要了解如何使用就可以了,不必在这里大费周章,我们学习的关键是游戏编码中的编写能力和底层逻辑思维。

游戏开始前的准备工作

        为了让代码的逻辑清晰可见,我们主要将代码分成三个文件(snake.h、snake.c、test.c)。

snake.h用于包含其他文件需要用到的头文件、常量、宏以及结构体和函数声明。

snake.c用于各种函数的实现。

test.c用于测试游戏。

        下面我来说明一下C语言的文字环境问题。

<locale.h>

        在游戏中,我们需要用到一些字符(方块、五角星、圆圈)来作为游戏的墙体、蛇的身体、和食物,这些在C语言的标准环境下是不被支持的,这⾥再简单的讲⼀下C语⾔的国际化特性相关的知识,过去C语⾔并不适合⾮英语国家(地区)使⽤。C语⾔最初假定字符都是单字节的。但是这些假定并不是在世界的任何地⽅都适⽤。后来为了使C语⾔适应国际化,C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型:wchar_t 和宽字符的输⼊和输出函数,加入了<locale.h>头⽂件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。

拓展:

        C语⾔字符默认是采⽤ASCII编码的,ASCII字符集采⽤的是单字节编码,且只使⽤了单字节中的低7位,最⾼位是没有使⽤的,可表⽰为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够⽤的,但是,在其他国家语⾔中,⽐如,在法语中,字⺟上⽅有注⾳符号,它就⽆法⽤ASCII码表⽰。于是,⼀些欧洲国家就决定,利⽤字节中闲置的最⾼位编⼊新的符号。⽐如,法语中的é的编码为130(⼆进制0000010)。这样⼀来,这些欧洲国家使⽤的编码体系,可以表⽰最多256个符号。但是,这⾥⼜出现了新的问题。不同的国家有不同的字⺟,因此,哪怕它们都使⽤256个符号的编码⽅式,代表的字⺟却不⼀样。⽐如,130在法语编码中代表了é,在希伯来语编码中却代表了字⺟Gimel(),在俄语编码中⼜会代表另⼀个符号。但是不管怎样,所有这些编码⽅式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。⾄于亚洲国家的⽂字,使⽤的符号就更多了,汉字就多达10万左右。⼀个字节只能表⽰256种符号,肯定是不够的,就必须使⽤多个字节表达⼀个符号。⽐如,简体中⽂常⻅的编码⽅式是GB2312,使⽤两个字节表⽰⼀个汉字,所以理论上最多可以表⽰256x256=65536个符号。

        了解了C语言在不同国家的文字环境差异后我们就来了解一下如何修改C语言的环境。

locale.h的本地化

        <locale.h>提供的函数用于控制c标准库中对于同地区会产生不一样的行为的部分。在标准中依赖地区的部分有以下几项:

•   数字的格式

•   货币量的格式

•   字符集
•   日期和时间的表示形式

 类项

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

• LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的⾏为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语⾔环境。

setlocale函数

      上面的这些类项都在本函数中有说明。

 char* setlocale (int category, const char* locale);

        setlocale函数⽤于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
        setlocale的第⼀个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀数是LC_ALL,就会影响所有的类项。

        C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和 " "(本地模式)。
        在任意程序执行开始,都会隐藏式执行调用:  setlocale(LC_ALL, "C");
        当地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤""作为第2个参数,调⽤setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。

⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等。
setlocale(LC_ALL, " ");        //切换到本地环境即可

我们对贪吃蛇封装一个测试函数,代码如下:

int main()
{//设置C语言为本地环境,为了支持中文字符的打印。setlocale(LC_ALL, "");//游戏测试test();return 0;
}

        封装好函数之后,我们进入到函数中的游戏环境搭建模块。

游戏环境搭建

        Win32API

        介绍

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

        控制台程序

        平常我们运行起来的黑框程序其实就是程序控制台

        我们可以使用cmd命令来设置控制台窗口的长宽(行列):30行,100列

mode con cols=100 lines=30

         同时我们还可以使用命令设置控制台窗口的名字:

title 贪吃蛇

        想要使用控制台窗口执行命令,我们需要调用system函数,该函数包含在windows.h的头文件中,代码如下:

void test()
{//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列system("mode con cols=100 lines=30");//设置cmd窗⼝名称system("title 贪吃蛇");
}

        学会了设置控制台窗口和cmd窗口后,我们再来解决一个光标显示问题,我们知道在输入是屏幕上总有一个光标在闪,如果我们在运行游戏中总是有这个光标就会影响我们。那下面我们就来看看如何隐藏光标。 

 GetStdHandle函数

        GetStdHandle是⼀个WindowsAPI函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。

函数原型:

HANDLE GetStdHandle(DWORD nStdHandle);

使用方法:

HANDLE hOutput = NULL;        //创建HANDLE类型的变量

//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);        //参数的用法见下图

 GetConsoleCursorInfo

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

结构体定义原型:

BOOL WINAPI GetConsoleCursorInfo(
        HANDLE                                         hConsoleOutput,
        PCONSOLE_CURSOR_INFO         lpConsoleCursorInfo
);

PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关控制台游标的信息。

使用方式:

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO {
        DWORD         dwSize;
        BOOL             bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

• dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
• bVisible,游标的可见性。如果光标可见,则此成员为TRUE。(这里需要包含stdbool.h的头文件,因为true和false都是bool类型的变量)
        如果想要隐藏光标,使光标不可见,操作如下:

CursorInfo.bVisible = false; //隐藏控制台光标

 SetConsoleCursorInfo

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

BOOL WINAPI SetConsoleCursorInfo(
        HANDLE                                             hConsoleOutput,
        const CONSOLE_CURSOR_INFO    *lpConsoleCursorInfo
);

综合上述讲解, 我们就可以用代码实现对光标隐藏的操作:(包含在windows.h头文件下)

void test()
{HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//隐藏光标操作CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
}

 游戏的核心逻辑实现分析 

         数据结构设计

        在游戏运行的过程中,蛇每次吃一个食物,蛇的身体都会变长一个节点,如果我们使用链表存储蛇的信息,那么蛇的每一节其实就是链表的每个节点。每个节点只要记录号蛇身节点在地图上的坐标就好了,所以蛇身的结构体如下:

//蛇身的结构体
typedef int slDataType;
typedef struct SnakeList
{slDataType x;slDataType y;struct SnakeList* next;
}SnakeList;

 因为我们要管理整个贪吃蛇游戏的对象,所以我们再封装⼀个Snake的结构来维护整条贪吃蛇:

//贪吃蛇游戏的运行结构体
typedef struct Snake
{SnakeList* psnake;SnakeList* pFood;enum DIRECTION dir;enum STATUS status;int score;int SingleFoodScore;int SleepTime;
}Snake;

在Snake结构体中,我还创建了方向和游戏状态两种枚举类型:这样使用起来更方便

//⽅向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT
};//游戏状态
enum GAME_STATUS
{
OK,//正常运⾏
KILL_BY_WALL,//撞墙
KILL_BY_SELF,//咬到⾃⼰
END_NOMAL//正常结束
};

         游戏主逻辑

        程序开始就设置程序⽀持本地模式,然后进⼊游戏的主逻辑。

我们将主逻辑分成三个过程:

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

        游戏循环框架

//游戏测试
void test()
{//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列system("mode con cols=100 lines=30");//设置cmd窗⼝名称system("title 贪吃蛇");//获取句柄HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//隐藏光标操作CONSOLE_CURSOR_INFO CursorInfo;//获取控制台光标信息GetConsoleCursorInfo(hOutput, &CursorInfo);//隐藏控制台光标CursorInfo.bVisible = false; //设置控制台光标状态SetConsoleCursorInfo(hOutput, &CursorInfo);char ch;do{Snake snake = { 0 };//游戏开始(游戏初始化)GameStart(&snake);//游戏运行(游戏逻辑)GameRun(&snake);//游戏结束(资源释放)GameEnd(&snake);SetPos(25, 15);printf("再来一局吗(Y/N):");ch = getchar();getchar();system("cls");} while (ch == 'Y' || ch == 'y');

         在do-while循环中,我们使用ch接收getchar()获取到的字符是为了我们在接收到y时能够继续下一次循环,而我们在后面又加了一个getchar(),这是因为我们在输入字符后会跟一个回车,如果这个回车没有被清理,下一次循环中的getchar()会先接收回车,而不是我们们第二次输入的字符。最后清理屏幕。

游戏开始(GameStart)

        在游戏的开始,我们要做四件事来完成游戏的初始化任务:

1、打印欢迎界面-Welcome

2、创建地图-CreateMap

3、初始化蛇身-InitSnake

4、创建食物

测试函数代码如下:

//游戏开始
void GameStart(Snake* ps)
{//打印欢迎界面Welcome();//创建地图CreateMap();//初始化蛇身InitSnake(ps);//创建食物CreateFood(ps);
}

下面我来一 一 分析:

打印欢迎界面

//打印欢迎界面
void Welcome()
{SetPos(40, 13);printf("欢迎来到贪吃蛇小游戏…");SetPos(42, 16);system("pause");    //使函数运行暂停,按任意位置继续。system("cls");      //清理屏幕信息SetPos(24, 13);printf("用↑ . ↓ . ← . →分别控制蛇的移动方向,F3为加速,F4为减速\n");SetPos(42, 16);system("pause");system("cls");}

SetPos自定义函数

  COORD        

为了使每一个封装的函数显示更简介更有可读性,我将设置坐标的函数封装成了SetPos(),下面我们先来讲解一下SetPos函数的用法和结构:

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

COORD类型的声明:

typedef struct _COORD {
        SHORT         X;
        SHORT         Y;
} COORD, *PCOORD;

给坐标赋值方法:

COORD pos = { 10, 15 };

SetConsoleCursorPosition


        该函数的作用是设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。
        使用案例如下:

COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);

这样我们就可以封装成一个设置光标位置的函数了:

//设置光标的位置
void SetPos(slDataType x, slDataType y)
{COORD pos = { x, y };HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
}

这样我们就完成了打印欢迎界面:

 创建地图

        创建地图就是将墙打印出来,因为我们要使用到宽字符,所以我们先来讲一下宽字符的打印。

宽字符的打印


        宽字符的字面量必须加上前缀“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'★';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;
}

        从输出结果上我们可以看出,正常字符是宽字符的一半。那么我们在贪吃蛇中使用宽字符就得处理好地图上的坐标位置计算。

普通字符和宽字符打印出宽度的展示如下:

这也是为什么我们设置行和列的时候需要设置不同大小的原因。

地图坐标

        我们假设实现一个27行,58列的棋盘,在围绕地图画出墙体,如下:

墙体打印的宽字符:

#define  WALL   L'□'                //使用#define定义的常量

坐标的计算极易出错,所以我们要谨慎一些:

上:(0,0)到(56,0)
下:(0,26)到(56,26)
左:(0,1)到(0,25)
右:(56,1)到(56,25)

创建地图函数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);}
}

初始化蛇身

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

完成上述操作后,对贪吃蛇游戏的结构体进行初始化:

• 蛇的初始位置从(24,5)开始。
再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。
• 游戏状态是:OK
• 蛇的移动速度(睡眠时间):200ms
• 蛇的默认方向:RIGHT
• 初始总分:0
• 每个食物的分数:10

 蛇身打印的宽字符:

#define  BODY  L'●'

//打印蛇身
void PrintBody(SnakeList* ps)
{SnakeList* pcur = ps;while (pcur){SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}
}//初始化蛇身
void InitSnake(Snake* ps)
{//创建蛇的身体节点SnakeList* pcur = NULL;for (int i = 0; i < 5; i++){//动态申请空间pcur = (SnakeList*)malloc(sizeof(SnakeList));if (pcur == NULL){perror("Init:malloc");exit(1);}//设置初值pcur->next = NULL;pcur->x = 24 + i * 2;pcur->y = 5;//头插法连接蛇的身体节点if (ps->psnake == NULL)//空表直接赋值{ps->psnake = pcur;}else//非空链表直接头插{pcur->next = ps->psnake;ps->psnake = pcur;}}//打印蛇身PrintBody(ps->psnake);//初始化贪吃蛇游戏的数据ps->dir = RIGHT;ps->score = 0;ps->SingleFoodScore = 10;ps->status = OK;ps->SleepTime = 200;
}

         这样我们就成功创建了蛇身。 

创建⼀个食物

        创建食物的位置要求是随机的,所以我们在这里又使用到之前讲过的随机种子。首先随机生成食物的坐标(x坐标必须是二的倍数,因为宽字符占两个单位的x坐标,并且食物的坐标不能和蛇身的任一节点重合),然后创建食物节点,打印食物,将食物节点维护到Snake结构体中。

//打印食物坐标
void PrintFood(SnakeList* ps)
{SetPos(ps->x,ps->y);wprintf(L"%lc", FOOD);
}//创建食物
void CreateFood(Snake* ps)
{//使用随机种子生成随机数建立坐标slDataType x;slDataType y;//保证坐标位置的有效性again:do{x = rand() % 53 + 2;y = rand() % 26 + 1;} while (x%2 != 0);//确保食物坐标不与蛇身节点重合SnakeList* cur = ps->psnake;//获取指向蛇头的指针while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//申请食物节点SnakeList* pfood = (SnakeList*)malloc(sizeof(SnakeList));if (pfood == NULL){perror("Create:malloc");exit(1);}//赋值坐标值pfood->next = NULL;pfood->x = x;pfood->y = y;//打印坐标PrintFood(pfood);//维护食物节点ps->pFood = pfood;
}

 游戏运行(GameRun)

        在游戏运行中,我们需要通过键盘输入指令操作游戏,这就需要我们设置虚拟按键来操作蛇的移动。

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 )

 在游戏期间我们需要用到的虚拟按键有:

需要的虚拟按键的罗列:
• 上:VK_UP
• 下:VK_DOWN
• 左:VK_LEFT
• 右:VK_RIGHT
• 空格:VK_SPACE
• ESC:VK_ESCAPE
• F3:VK_F3
• F4:VK_F4         

        当然在游戏运行期间,还要在右侧打印帮助信息,提示玩家如何操作,坐标开始位置设为(64,10)。游戏中根据游戏状态检查游戏是否继续,如果状态是OK,游戏继续,否则游戏结束。如果游戏继续,就是检测按键情况,确定蛇的下一步方向,或者是否加速减速,是否暂停或者退出游戏。

//打印帮助信息
void PrintHelpInfo(Snake* ps)
{SetPos(64, 10);printf("得分:%d  ", ps->score);printf("每个食物得分:%d\n", ps->SingleFoodScore);SetPos(64, 12);printf("用↑ . ↓ . ← . →分别控制蛇的移动\n");SetPos(64, 13);printf("F3为加速,F4为减速\n");SetPos(64, 15);printf("ESC :退出游戏.space:暂停游戏.");
}
//游戏运行(游戏逻辑)
void GameRun(Snake* ps)
{//打印帮助信息PrintHelpInfo(ps);//设置虚拟按键do{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_SPACE)){SetPos(0, 28);system("pause");}else if (KEY_PRESS(VK_ESCAPE)){ps->status = END_NORMAL;break;}else if (KEY_PRESS(VK_F3)){//休眠最短50ms,单个食物最高20分if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->SingleFoodScore += 2;}}else if (KEY_PRESS(VK_F4)){//休眠最高320ms,单个食物最低2分if (ps->SleepTime < 320){ps->SleepTime += 30;ps->SingleFoodScore -= 2;}}//休眠Sleep(ps->SleepTime);//蛇身移动SnakeMove(ps);} while (ps->status == OK);
}

        这里不方便演示,我就不贴图了,各位可以自行尝试。

        我们已经设置好了蛇的移动方向问题,下面最关键的是如何让蛇动起来,我们封装成一个蛇身移动函数(SnakeMove)。

蛇⾝移动(SnakeMove)

        1、先创建下一个节点,根据移动方向和蛇头坐标将蛇移动到下一个位置的坐标。2、确定了下一个位置后,判断下一个位置是否有食物(NextIsFood),有食物就进入函数(EatFood),没有食物就进入函数(NoFood)。3、蛇身移动后,判断此次移动是否会造成撞墙(KILLBYWALL)或者撞上自己的身体节点(KILLBYSELF),从而影响游戏的状态。

//蛇身移动
void SnakeMove(Snake* ps)
{//蛇向前移动的原理是创建一个新节点,并头插后删除尾节点,空格覆盖之前的尾节点打印SnakeList* nextnode = (SnakeList*)malloc(sizeof(SnakeList));if (nextnode == NULL){perror("SnakeMove:malloc");exit(1);}//确定下一个节点的坐标,下一个节点的坐标根据蛇头的坐标和方向确定switch(ps->dir){case UP:{nextnode->x = ps->psnake->x;nextnode->y = ps->psnake->y - 1;}break;case DOWN:{nextnode->x = ps->psnake->x;nextnode->y = ps->psnake->y + 1;}break;case LEFT:{nextnode->x = ps->psnake->x - 2;nextnode->y = ps->psnake->y;}break;case RIGHT:{nextnode->x = ps->psnake->x + 2;nextnode->y = ps->psnake->y;}break;}//下一个位置是食物的情况if (NextIsFood(nextnode, ps)){EatFood(nextnode,ps);}else//下一个位置没有食物的情况{NoFood(nextnode,ps);}//撞墙KILLBYWALL(ps);//撞自己KILLBYSELF(ps);
}

        我们再来看一下再SnakeMove中调用的五个函数:

//下一个位置有食物
int NextIsFood(SnakeList* psn,Snake* ps)
{//判断蛇头下一个位置的坐标是否和食物坐标一致,若是则返回非零数,否则返回0。return (psn->x == ps->pFood->x) && (psn->y == ps->pFood->y);
}
//吃食物
void EatFood(SnakeList* psn, Snake* ps)
{//头插法psn->next = ps->psnake;ps->psnake = psn;//打印蛇身PrintBody(ps->psnake);//加分ps->score += ps->SingleFoodScore;//释放食物节点free(ps->pFood);//创建新的食物节点CreateFood(ps);
}//下一个节点没有食物
void NoFood(SnakeList* psn, Snake* ps)
{//头插法psn->next = ps->psnake;ps->psnake = psn;//打印蛇身,头插一个节点,最后一个节点覆盖为空格SnakeList* pcur = ps->psnake;while (pcur->next->next){SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//覆盖尾打印节点SetPos(pcur->next->x, pcur->next->y);printf("  ");//释放尾节点free(pcur->next);pcur->next = NULL;
}//撞墙
int KILLBYWALL(Snake* ps)
{if ((ps->psnake->x == 0)|| (ps->psnake->x == 56)|| (ps->psnake->y == 0)|| (ps->psnake->y == 27)){ps->status = KILL_BY_WALL;return 1;}return 0;
}
//撞自己
int KILLBYSELF(Snake* ps)
{SnakeList* pcur = ps->psnake->next;while (pcur){if ((pcur->x == ps->psnake->x)&& (ps->psnake->y == pcur->y)){ps->status = KILL_BY_SELF;return 1;}pcur = pcur->next;}return 0;
}

游戏结束(GameEnd)

        这里我们只需要判断游戏的状态就可以了,如果游戏的状态不是OK,那就会在测试函数里判断是否跳出循环,我们在本函数中说明结束原因并释放蛇身节点就可以了。

//游戏结束(资源释放)
void GameEnd(Snake* ps)
{SetPos(24, 12);switch(ps->status){case END_NORMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("您撞上自己了 ,游戏结束!\n");break;case KILL_BY_WALL:printf("您撞墙了,游戏结束!\n");break;}//释放蛇的节点SnakeList* pcur = ps->psnake;while (pcur){SnakeList* del = pcur;pcur = pcur->next;free(del);}
}

        这样我们就完成了贪吃蛇游戏的代码。

完整代码演示:

Snake.h#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<time.h>//定义常量字符
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'//定义虚拟按键的宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)//枚举类型
//方向
enum DIRECTION
{UP = 1,DOWN,LEFT,RIGHT
};
//游戏状态
enum STATUS
{OK,//正常KILL_BY_WALL,//撞墙KILL_BY_SELF,//撞自己END_NORMAL//正常结束
};//蛇身的结构体
typedef int slDataType;
typedef struct SnakeList
{slDataType x;slDataType y;struct SnakeList* next;
}SnakeList;//贪吃蛇游戏的运行结构体
typedef struct Snake
{SnakeList* psnake;SnakeList* pFood;enum DIRECTION dir;enum STATUS status;int score;int SingleFoodScore;int SleepTime;
}Snake;//设置光标的位置
void SetPos(slDataType x, slDataType y);//游戏开始
void GameStart(Snake* ps);//游戏运行(游戏逻辑)
void GameRun(Snake* ps);//游戏结束(资源释放)
void GameEnd(Snake* ps);
Snake.c#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"//设置光标的位置
void SetPos(slDataType x, slDataType y)
{COORD pos = { x, y };HANDLE hOutput = NULL;//获取标准输出的句柄(⽤来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
}//打印欢迎界面
void Welcome()
{SetPos(40, 13);printf("欢迎来到贪吃蛇小游戏…");SetPos(42, 16);system("pause");system("cls");SetPos(24, 13);printf("用↑ . ↓ . ← . →分别控制蛇的移动方向,F3为加速,F4为减速\n");SetPos(42, 16);system("pause");system("cls");}//创建地图
CreateMap()
{//上SetPos(0,0);for (int i = 0; i < 29;i++){wprintf(L"%lc", WALL);}//下SetPos(0, 27);for (int i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//左for (int j = 1; j <= 26; j++){SetPos(0, j);wprintf(L"%lc", WALL);}//右for (int j = 1; j <= 26; j++){SetPos(56, j);wprintf(L"%lc", WALL);}
}//打印蛇身
void PrintBody(SnakeList* ps)
{SnakeList* pcur = ps;while (pcur){SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}
}//初始化蛇身
void InitSnake(Snake* ps)
{//创建蛇的身体节点SnakeList* pcur = NULL;for (int i = 0; i < 5; i++){//动态申请空间pcur = (SnakeList*)malloc(sizeof(SnakeList));if (pcur == NULL){perror("Init:malloc");exit(1);}//设置初值pcur->next = NULL;pcur->x = 24 + i * 2;pcur->y = 5;//头插法连接蛇的身体节点if (ps->psnake == NULL)//空表直接赋值{ps->psnake = pcur;}else//非空链表直接头插{pcur->next = ps->psnake;ps->psnake = pcur;}}//打印蛇身PrintBody(ps->psnake);//初始化贪吃蛇游戏的数据ps->dir = RIGHT;ps->score = 0;ps->SingleFoodScore = 10;ps->status = OK;ps->SleepTime = 200;
}//打印食物坐标
void PrintFood(SnakeList* ps)
{SetPos(ps->x,ps->y);wprintf(L"%lc", FOOD);
}//创建食物
void CreateFood(Snake* ps)
{//使用随机种子生成随机数建立坐标slDataType x;slDataType y;//保证坐标位置的有效性again:do{x = rand() % 53 + 2;y = rand() % 26 + 1;} while (x%2 != 0);//确保食物坐标不与蛇身节点重合SnakeList* cur = ps->psnake;//获取指向蛇头的指针while (cur){if (cur->x == x && cur->y == y){goto again;}cur = cur->next;}//申请食物节点SnakeList* pfood = (SnakeList*)malloc(sizeof(SnakeList));if (pfood == NULL){perror("Create:malloc");exit(1);}//赋值坐标值pfood->next = NULL;pfood->x = x;pfood->y = y;//打印坐标PrintFood(pfood);//维护食物节点ps->pFood = pfood;
}//游戏开始
void GameStart(Snake* ps)
{//打印欢迎界面Welcome();//创建地图CreateMap();//初始化蛇身InitSnake(ps);//创建食物CreateFood(ps);
}//打印帮助信息
void PrintHelpInfo(Snake* ps)
{SetPos(64, 10);printf("得分:%d  ", ps->score);printf("每个食物得分:%d\n", ps->SingleFoodScore);SetPos(64, 12);printf("用↑ . ↓ . ← . →分别控制蛇的移动\n");SetPos(64, 13);printf("F3为加速,F4为减速\n");SetPos(64, 15);printf("ESC :退出游戏.space:暂停游戏.");
}//下一个位置有食物
int NextIsFood(SnakeList* psn,Snake* ps)
{return (psn->x == ps->pFood->x) && (psn->y == ps->pFood->y);
}
//吃食物
void EatFood(SnakeList* psn, Snake* ps)
{//头插法psn->next = ps->psnake;ps->psnake = psn;//打印蛇身PrintBody(ps->psnake);//加分ps->score += ps->SingleFoodScore;//释放食物节点free(ps->pFood);//创建新的食物节点CreateFood(ps);
}//下一个节点没有食物
void NoFood(SnakeList* psn, Snake* ps)
{//头插法psn->next = ps->psnake;ps->psnake = psn;//打印蛇身,头插一个节点,最后一个节点覆盖为空格SnakeList* pcur = ps->psnake;while (pcur->next->next){SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//覆盖尾打印节点SetPos(pcur->next->x, pcur->next->y);printf("  ");//释放尾节点free(pcur->next);pcur->next = NULL;
}//撞墙
int KILLBYWALL(Snake* ps)
{if ((ps->psnake->x == 0)|| (ps->psnake->x == 56)|| (ps->psnake->y == 0)|| (ps->psnake->y == 27)){ps->status = KILL_BY_WALL;return 1;}return 0;
}
//撞自己
int KILLBYSELF(Snake* ps)
{SnakeList* pcur = ps->psnake->next;while (pcur){if ((pcur->x == ps->psnake->x)&& (ps->psnake->y == pcur->y)){ps->status = KILL_BY_SELF;return 1;}pcur = pcur->next;}return 0;
}//蛇身移动
void SnakeMove(Snake* ps)
{//蛇向前移动的原理是创建一个新节点,并头插后删除尾节点,空格覆盖之前的尾节点打印SnakeList* nextnode = (SnakeList*)malloc(sizeof(SnakeList));if (nextnode == NULL){perror("SnakeMove:malloc");exit(1);}//确定下一个节点的坐标,下一个节点的坐标根据蛇头的坐标和方向确定switch(ps->dir){case UP:{nextnode->x = ps->psnake->x;nextnode->y = ps->psnake->y - 1;}break;case DOWN:{nextnode->x = ps->psnake->x;nextnode->y = ps->psnake->y + 1;}break;case LEFT:{nextnode->x = ps->psnake->x - 2;nextnode->y = ps->psnake->y;}break;case RIGHT:{nextnode->x = ps->psnake->x + 2;nextnode->y = ps->psnake->y;}break;}//下一个位置是食物的情况if (NextIsFood(nextnode, ps)){EatFood(nextnode,ps);}else//下一个位置没有食物的情况{NoFood(nextnode,ps);}//撞墙KILLBYWALL(ps);//撞自己KILLBYSELF(ps);
}//游戏运行(游戏逻辑)
void GameRun(Snake* ps)
{//打印帮助信息PrintHelpInfo(ps);//设置虚拟按键do{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_SPACE)){SetPos(0, 28);system("pause");}else if (KEY_PRESS(VK_ESCAPE)){ps->status = END_NORMAL;break;}else if (KEY_PRESS(VK_F3)){//休眠最短50ms,单个食物最高20分if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->SingleFoodScore += 2;}}else if (KEY_PRESS(VK_F4)){//休眠最高320ms,单个食物最低2分if (ps->SleepTime < 320){ps->SleepTime += 30;ps->SingleFoodScore -= 2;}}//休眠Sleep(ps->SleepTime);//蛇身移动SnakeMove(ps);} while (ps->status == OK);
}//游戏结束(资源释放)
void GameEnd(Snake* ps)
{SetPos(24, 12);switch(ps->status){case END_NORMAL:printf("您主动退出游戏\n");break;case KILL_BY_SELF:printf("您撞上自己了 ,游戏结束!\n");break;case KILL_BY_WALL:printf("您撞墙了,游戏结束!\n");break;}//释放蛇的节点SnakeList* pcur = ps->psnake;while (pcur){SnakeList* del = pcur;pcur = pcur->next;free(del);}
}
test.c#define _CRT_SECURE_NO_WARNINGS
#include"snake.h"
#include<locale.h>//游戏测试
void test()
{//设置控制台窗⼝的⻓宽:设置控制台窗⼝的⼤⼩,30⾏,100列system("mode con cols=100 lines=30");//设置cmd窗⼝名称system("title 贪吃蛇");//获取句柄HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//隐藏光标操作CONSOLE_CURSOR_INFO CursorInfo;//获取控制台光标信息GetConsoleCursorInfo(hOutput, &CursorInfo);//隐藏控制台光标CursorInfo.bVisible = false; //设置控制台光标状态SetConsoleCursorInfo(hOutput, &CursorInfo);char ch;srand((unsigned int)time(NULL));do{Snake snake = { 0 };//游戏开始(游戏初始化)GameStart(&snake);//游戏运行(游戏逻辑)GameRun(&snake);//游戏结束(资源释放)GameEnd(&snake);SetPos(25, 15);//printf("再来一局吗(Y/N):");ch = getchar();getchar();system("cls");} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}int main()
{//设置C语言为本地环境,为了支持中文字符的打印。setlocale(LC_ALL, "");//游戏测试test();return 0;
}

        我还想和大家说,这种小游戏可以极大的开拓我们的思维,让我们去用心太会里面的过程,等你可以慢慢独立写出这个游戏的时候,你会发现自己的变化。

        期待你努力之后的提升,我们下期再见。

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

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

相关文章

Unity 性能优化之数据面板(Statistics)(一)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、unity 统计数据面板&#xff08;Statistics&#xff09;1.Audio属性2.Graphics属性 二、什么是Draw Call&#xff1f;三、Unity3D stats也可以通过代…

OpenCV的周期性噪声去除滤波器(70)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:OpenCV如何通过梯度结构张量进行各向异性图像分割(69) 下一篇 :OpenCV如何为我们的应用程序添加跟踪栏(71) 目录 目标 理论 如何消除傅里叶域中的周期性噪声&#xff1f; 源代码 解释 结果 目…

设计模式之数据访问对象模式

在Java编程的浩瀚星海中&#xff0c;有一个模式低调却强大&#xff0c;它像是一位默默无闻的超级英雄&#xff0c;支撑起无数应用的数据脊梁——那就是数据访问对象&#xff08;DAO, Data Access Object&#xff09;模式&#xff01;想象一下&#xff0c;如果你能像操纵魔法一样…

Unity技术学习:RenderMesh、RenderMeshInstanced

叠甲&#xff1a;本人比较菜&#xff0c;如果哪里不对或者有认知不到的地方&#xff0c;欢迎锐评&#xff08;不玻璃心&#xff09;&#xff01; 导师留了个任务&#xff0c;渲染大量的、移动的物体。 当时找了几个解决方案&#xff1a; 静态批处理&#xff1a; 这东西只对静…

golang for经典练习 金字塔打印 示例 支持控制台输入要打印的层数

go语言中最经典的for练习程序 金字塔打印 &#xff0c;这也是其他语言中学习循环和条件算法最为经典的联系题。 其核心算法是如何控制内层循环变量j 每行打印的*号数量 j<i*2-1 和空格数量 j1 || j i*2-1 golang中实现实心金字塔 Solid Pyramid和空心金字塔 Hollow Pyram…

CSS浮动(如果想知道CSS有关浮动的知识点,那么只看这一篇就足够了!)

前言&#xff1a;在学习CSS排版的时候&#xff0c;浮动是我们必须要知道的知识点&#xff0c;浮动在设计之初是为了实现文字环绕效果的&#xff0c;但是后来被人们发现浮动在CSS排版中有着很好的实用价值&#xff0c;所以浮动便成为了CSS排版的利器之一。 ✨✨✨这里是秋刀鱼不…

pandas学习笔记12

缺失数据处理 其实在很多时候&#xff0c;人们往往不愿意过多透露自己的信息。假如您正在对用户的产品体验做调查&#xff0c;在这个过程中您会发现&#xff0c;一些用户很乐意分享自己使用产品的体验&#xff0c;但他是不愿意透露自己的姓名和联系方式&#xff1b; 还有一些用…

《尿不湿级》STM32 F103C8T6最小系统板搭建(五)BOOT

一、BOOT是什么&#xff1f; 大多数初学者第一次接触BOOT总是对这个词感到不解&#xff0c;从哪冒出一个奇奇怪怪的东西还要接跳线帽&#xff0c;为什么要配置它才能进行串口程序的下载&#xff1f;为什么不正确配置会导致单片机无法正常启动…… boot&#xff0c;及物动词&…

AI-数学-高中56-成对数据统计-线性回归方程

原作者视频&#xff1a;【成对数据统计】【一数辞典】1线性回归方程_哔哩哔哩_bilibili 注意&#xff1a;高中只学线性回归。 最小二乘法&#xff08;残差和平方最小的直线、方差最小>拟合程度最好&#xff09;&#xff1a;

2.spring security 简单入门

创建springboot 项目&#xff0c;引入spring security坐标 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--spring security坐标--><dependency&g…

17_Scala面向对象高阶功能

文章目录 1.继承1.1 构造对象时,父类对象优于子类对象1.2父类主构造有参数,子类必须要显示地调用父类主构造器并传值 2.封装3.抽象3.1抽象定义3.2子类继承抽象类3.3抽象属性 4.伴生对象4.1创建类和伴生对象4.2调用 1.继承 –和Java一样,权限protected , public.父类定义子类用…

分布式锁之-redis

什么是分布式锁&#xff1f; 即分布式系统中的锁。在单体应用中我们通过锁解决的是控制共享资源访问的问题&#xff0c;而分布式锁&#xff0c;就是解决了分布式系统中控制共享资源访问的问题。与单体应用不同的是&#xff0c;分布式系统中竞争共享资源的最小粒度从线程升级成了…

详细介绍ARM-ORACLE Database 19c数据库下载

目录 1. 前言 2. 获取方式 2.1 ORACLE专栏 2.2 ORACLE下载站点 1. 前言 现有网络上已有非常多关于ORACLE数据库机下载的介绍&#xff0c;但对于ARM平台的介绍不多&#xff0c;借此机会我将该版的下载步骤做如下说明&#xff0c;希望能够一些不明之人提供帮助和参考 2. 获…

Linux理解文件操作 文件描述符fd 理解重定向 dup2 缓冲区 C语言实现自己的shell

文章目录 前言一、文件相关概念与操作1.1 open()1.2 close()1.3 write()1.4 read()1.4 写入的时候先清空文件内容再写入1.5 追加&#xff08;a && a&#xff09; 二、文件描述符2.1 文件描述符 fd 0 1 2 的理解2.2 FILE结构体&#xff1a;的源代码 三、深入理解文件描述…

DETR类型检测网络---思考和Tricks测试

目录 batch_size的影响辅助损失的作用学习率的影响Decoder层数增多的影响3D检测中, feats位置编码和query位置编码是否共享mpl层背景-关于query的生成方式 利用widthformer类似的方式简化注意力机制 batch_size的影响 batch8: batch20: 由实验结果可知:这里实验有问题,横坐标…

JAVA语言开发的智慧城管系统源码:技术架构Vue+后端框架Spring boot+数据库MySQL

通过综合应用计算机技术、网络技术、现代通信技术等多种信息技术&#xff0c;充分融合RS遥感技术、GPS全球定位技术、GIS地理信息系统&#xff0c;开始建设一个动态可视的、实时更新的、精细量化的城市管理系统。智慧城管将采用云平台架构方式进行建设&#xff0c;基于现有数字…

人工智能大模型应用指南

大家好&#xff0c;我是爱编程的喵喵。双985硕士毕业&#xff0c;现担任全栈工程师一职&#xff0c;热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…

直流屏整流模块HG07A220R电源模块HG10A220R

直流屏整流模块HG07A220R电源模块HG10A220R 其他同类型监控模块PM09T电源模块HG22005/S&#xff0c;HG22010/S&#xff0c;HG11010/S&#xff0c;HG11020/S&#xff0c;HG10A220Z&#xff0c;HG10A220F&#xff0c;HG05A220Z&#xff0c;HG07A220Z&#xff0c;HG10A110Z&#x…

免费可商用字体素材大全,办公设计字体合集打包166款

一、素材描述 这是一套免费可商用字体素材&#xff0c;这些字体一般会在办公与设计的时候用到&#xff0c;比如&#xff0c;Photoshop、illustrator、Coreldraw、AfterEffects、Indesign、WPS、Office&#xff0c;等等&#xff0c;想要更好更快地办公与设计&#xff0c;字体还…

【Java】面向对象核心知识点(二),文章层次分明,内容精益求精,代码简单易懂

目录 一、构造方法 1.1 概念 1.2 作用 1.3 代码 二、抽象 2.1 概念 2.2 作用 2.3 注意 2.4 代码 三、接口 3.1 概念 3.2 作用 3.3 注意 3.4 语法 3.5 代码 四、内部类 4.1 成员内部类 4.2 局部内部类 4.3 静态内部类 4.4 匿名内部类 &#xff08;原创文章&…