贪吃蛇---C语言---详解

引言

C语言已经学了不短的时间的,这期间已经开始C++和Python的学习,想给我的C语言收个尾,想起了小时候见过别人的老人机上的贪吃蛇游戏,自己父母的手机又没有这个游戏,当时成为了我的一大遗憾,这两天发现C语言实现这个项目似乎并不难,于是查了一些WindowsAPI的控制台函数,实现了这一游戏。如果你觉得你的C语言基础语法学的差不多了,又想实现贪吃蛇这样一个小游戏,那么就跟我一起来实现它吧。下面是最终成品的样子:

本贪吃蛇是用控制台实现,其中¥是贪吃蛇的食物,⚪是贪吃蛇,■是墙体。

Win32 API

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

控制台程序

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

我们可以用cmd命令来控制控制台窗口的长宽:比如设置窗口大小,30行,100列

mode con cols=100 lines=30

同时也可以通过命令修改窗口的名字:

title 贪吃蛇

这里注意一下,在改名字之后加一个getchar()保证程序处在运行状态,这样才能正确观察到要改后的名字。

这些能在控制台窗口执行的命令,像我上方图片中的代码一样,可以用C语言函数system来执行。

代码放在下面:

#include<stdlib.h>
int main()
{system("mode con cols=100 lines=30");//设置窗口大小system("title 贪吃蛇");//改窗口标题getchar();return 0;
}

这里注意一下system的头文件是

#include <stdlib.h>

控制台上的坐标COORD

COORD是Windows API中定义的一个结构体,表示一个字符在控制台屏幕上的坐标,下面是关于对COORD的定义:

typedef struct _COORD{SHORT x;SHORT y;
}COORD, *PCOORD;

其中x轴和y轴如图

同时可以给上方结构体(坐标)赋值:

COORD pos = {10,15};

GetStdHandle

GetStdHandle是一个Windows API函数。它用于一个特定的标准设备(标准输入,标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。

HANDLE GetStdHandle(DWORD nStdHandle);//函数的参数为标准设备

句柄是什么?

句柄相当于一个操作工具,你可以通过操作某设备的句柄去获得和修改某标准设备的信息

实例(获得句柄)

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

GetConsoleCursorInfo

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

BOOL WINAPI GetConsoleInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);

实例:

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值) 
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 
//通过传CursorInfo的地址并通过函数将当前光标信息传给CursorInfo

CONSOLE_CURSOR_INFO

在上一份代码中CursorInfo,里面存的是光标信息,类型是CONSOLE_CURSOR_INFO,我们可以来看看这个类型是如何定义的

typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;//这个变量是表示光标所占一个格的百分比BOOL bVisible;//这个变量是决定光标是否可见
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
  • dwSize,由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完 全填充单元格到单元底部的水平线条。
  • bVisible,游标的可见性。如果光标可见,则此成员为TRUE

我们在运行打印贪吃蛇的过程中将光标设置为不可见,就不会影响到整个游戏的美观

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

SetConsoleCursorInfo

上方的GetConsoleCursorInfo是通过函数获取光标信息,这次的函数是通过函数实在改变控制台光标信息,下面是本函数声明

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);//设置控制台光标状态 

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);
//通过以上代码可以将光标设置到10 5 位置上

看到这里,我们是否可以考虑封装一个函数,可以专门通过传入坐标来控制光标位置,于是封装了一个这样的函数Setpos

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

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)

这样就可以通过向KEY_PRESS传入键值直接监测按键是否被按过了。

下面是关于不同键值介绍的链接

Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Learn

不过目前我们知道:

  1. VK_UP 向上箭头键 
  2. VK_DOWN 向下箭头键
  3. VK_LEFT 向左箭头键
  4. VK_RIGHT 向右箭头键
  5. VK_ESCAPE ESC按键
  6. VK_F3 F3按键
  7. VK_F4 F4按键

这些VK_XXX已经是头文件中用宏定义好的常量,直接用就行,不需要知道具体的值

就足够用了

贪吃蛇地图设计与分析

地图

如果想用控制台窗口打印地图,就需要了解一下控制台窗口坐标的知识

如下图所示,横向是X轴,从左向右增长,纵向是Y轴,从上到下依次增长

在地图上,我们打印墙体用宽字符■,打印蛇用宽字符●,打印食物我这里用的是宽字符¥(因为我个人比较喜欢)如果你在字符表里如果有别的喜欢的字符,也当然可以灵活的根据个人爱好改变

刚刚我介绍的时候介绍的字符是宽字符,意思是占两个字节的字符,普通的字符占一个字节

可以看看占两个字节字符和占一个字节字符的区别:

 由观察可以发现,一个占两字节的字符在控制台打印的时候也是占两个一字节字符所占的位置的

这里还需要引入一下C语言的国际化特性相关的知识,过去C语言并不适合非英语国家(地区)使用。 C语言最初假定字符都是但自己的。但是这些假定并不是在世界的任何地方都适用。

下面引用一段介绍:

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

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

刚才打印方框的过程提到了本地化,如果不进行本地化,■将无法被程序编译识别,最终只会打印问号,所以接下来我们讲讲如何运用<locale.h>以及其函数对编译环境进行本地化。

<locale.h>本地化

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

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

  • 数字的格式
  • 货币量的格式
  • 字符集
  • 日期和时间的表示形式

类项

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

  • LC_COLLATE
  •  LC_CTYPE 
  • LC_MONETARY
  • LC_NUMERIC
  • LC_TIME
  •  LC_ALL---针对所有类项修改

关于每个类项的详细说明,可参考

setlocale 函数

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

setlocale函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。

setlocale的第一个参数可以是前面说明的类项中的一个,那么只会影响一个类项,如果第一个参数是LC_ALL,那么就直接影响所有类项。

C标准给第二个参数仅定义了2种可能的取值:"C"和""。

在任意程序执行开始,都会隐藏式执行调用:

setlocale(LC_ALL,"C");

当地区设为"C"时,库函数按正常方式执行。

如果想在程序运行时改变地区,就只能显示调用setlocale函数。用""作为第二个参数,调用setlocale函数就可以切换到本地模式,这种模式会适应本地环境。

当切换到我们本地模式后,就可以支持一些宽字符(如汉字)的占位输出了。

setlocale(LC_ALL, " ");//切换到本地环境

宽字符的打印

#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列,围绕周围画出地图:

蛇和食物

初始化的时候,假设蛇长为5,蛇的每个节点宽字符●。这里要注意的是,蛇的每个节点和食物出现的X轴位置都要保证是二的倍数,不然会出现蛇和食物无法对齐或者蛇一半卡在墙体中的情况。

代码环节

数据的结构设计

上面说了这么多,到现在终于可以讲代码了,在学了这些控制台操作和地图分析之后,相信其实聪明的你已经基本能大概想出来如何去实现贪吃蛇的逻辑了,在开始代码之前,来介绍一下我们对我们对贪吃蛇数据的维护和设计

这里讲一下定义的每个节点的结构体:

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 GAME_STATUES status;//游戏当前状态enum DIRECTION dir;//蛇当前方向
}Snake, * pSnake;

在维护整条蛇的结构体类型中,定义了两个枚举类型,分别用来表示

游戏当前的状态:

enum GAME_STATUES {OK = 1,//游戏正常运行ESC,  //点击ESC主动退出KILL_BY_WALL,//撞到墙游戏结束KILL_BY_SELF //咬到自己游戏结束
};

蛇当前的前进方向:

enum DIRECTION {UP = 1,//上DOWN, //下LEFT, //左RIGHT //右
};

贪吃蛇项目流程设计 

这里介绍整个游戏过程中的运行逻辑,我们基本也是这个顺序展开代码

 游戏主函数:运行逻辑

#include"greedy_snake.h"
int main()
{srand((unsigned int)time(NULL));//随机初始化种子,相关内容可以参考我之前的扫雷博客int ch;do {Snake snake = { 0 };//创建一个维护整个贪吃蛇的数据类型snake.pSnake = NULL;GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("想要再来一局吗?Y/N:");ch = getchar();} while (ch == 'Y' || ch == 'y');SetPos(0, 26);return 0;
}

GameStart-游戏开始的数据初始化和维护

void GameStart(pSnake ps)
{//下面五行使光标不可见HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO rem;GetConsoleCursorInfo(houtput, &rem);rem.bVisible = 0;SetConsoleCursorInfo(houtput, &rem);setlocale(LC_ALL, "");//设置为本地类项//初始化界面system("mode con cols=100 lines=30");//设置窗口大小system("title 贪吃蛇");//改窗口标题//下面是打印欢迎和介绍信息SetPos(32, 10);printf("欢迎来到贪吃蛇小游戏!\n");SetPos(33, 15);system("pause");system("cls");SetPos(29, 9);printf("游戏介绍:");SetPos(33, 11);printf("通过↑ ← ↓ →控制蛇的移动");SetPos(33, 13);printf("可以通过F3加速,F4减速");SetPos(33, 15);printf("更高的速度下可以获得更高的分数");SetPos(33, 17);printf("可以使用空格暂停");SetPos(33, 19);system("pause");//这个命令可以使游戏暂停,按任意键继续//绘制地图CreateMap();//初始化创建蛇,传psInitSnake(ps);//初始化创建食物,传psCreateFood(ps);
}

SetPos-设置光标位置

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

CreateMap-绘制地图

void CreateMap()
{system("cls");SetPos(0, 0);//这里的WALL在头文件中用宏定义:#define WALL L'■'//上for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//左和右for (int i = 1; i <= 25; i++) {SetPos(0, i);wprintf(L"%lc", WALL);SetPos(56, i);wprintf(L"%lc", WALL);}//打印右侧边框的提示介绍信息SetPos(62, 15);printf("通过↑←↓→控制蛇的移动");SetPos(62, 16);printf("可以通过F3加速,F4减速");SetPos(62, 17);printf("更高的速度下可以获得更高的分数");SetPos(62, 18);printf("可以使用空格暂停");
}

CreateFood-初始化创建食物

void CreateFood(pSnake ps)
{int xx = 0;int yy = 0;//生成的地址不能在地图外,不能在蛇身上do{xx = rand() % 53 + 2;yy = rand() % 25 + 1;if (xx % 2 == 0) {pSnakeNode pcur = ps->pSnake;while (pcur) {if (xx == pcur->x && yy == pcur->y)goto again;pcur = pcur->next;}break;}again:;//循环直到生成正确的地址} while (1);pSnakeNode PFood = (SnakeNode*)malloc(sizeof(SnakeNode));if (PFood == NULL) {perror("malloc food fail:");exit(1);}PFood->x = xx;PFood->y = yy;ps->pFood = PFood;SetPos(xx, yy);//食物在宏中定义为:#define FOOD L'¥'wprintf(L"%lc", FOOD);
}

GameRun-游戏运行维护函数

void GameRun(pSnake ps)
{do {//打印游戏帮助信息SetPos(62, 10);printf("总分:%d\n", ps->Score);SetPos(62, 11);printf("食物分值:%2d\n", 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 = ESC;break;}else if (KEY_PRESS(VK_F3)) {//F3设置加速if (ps->SleepTime >= 80) {ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)) {//F4设置减速if (ps->FoodWeight > 2) {ps->SleepTime += 30;ps->FoodWeight -= 2;}}else if (KEY_PRESS(VK_SPACE))//空格设置暂停{while (1) {Sleep(100);if (KEY_PRESS(VK_SPACE)) {break;}}}//睡一下Sleep(ps->SleepTime);//根据按键控制蛇的运动和吃食物,并打印SnakeMove(ps);} while (ps->status == OK);
}

SnakeMove-蛇移动

void SnakeMove(pSnake ps)
{//根据在GameRun中获得的方向设置生成蛇的下一个节点pSnakeNode pNext = (SnakeNode*)malloc(sizeof(SnakeNode));if (pNext == NULL) {perror("malloc pNext fail:");exit(1);}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 (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {//如果吃上食物EatFood(ps, pNext);}else {//如果没吃上食物NotEatFood(ps, pNext);KillByWall(ps);//判断是否撞墙KillBySelf(ps);//判断是否咬到自己}
}
EatFood-吃到食物后蛇增长
void EatFood(pSnake ps,pSnakeNode pNext)
{//将新节点赋给蛇pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode pcur = ps->pSnake;while (pcur) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}ps->Score += ps->FoodWeight;//释放并创建新食物free(ps->pFood);CreateFood(ps);
}
NotEatFood-没有吃到食物向后移动
void NotEatFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode 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;SetPos(pcur->x, pcur->y);//蛇的身体在头文件中用宏定义为:#define BODY L'●'wprintf(L"%lc", BODY);
}
KillByWall-撞墙判定
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;//如果撞墙改变游戏状态}
}
KillBySelf-咬到自己判定
void KillBySelf(pSnake ps)
{pSnakeNode pcur = ps->pSnake->next;while (pcur) {if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;//如果要到自己改变游戏状态return;}pcur = pcur->next;}
}

GameEnd-游戏善后,释放蛇

void GameEnd(pSnake ps)
{//打印结束信息SetPos(20, 11);switch (ps->status){case ESC:printf("正常退出游戏\n");SetPos(20, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_WALL:printf("撞墙了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_SELF:printf("咬到自己了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;}//释放蛇pSnakeNode pcur = ps->pSnake;pSnakeNode del = ps->pSnake;while (pcur) {del = pcur;pcur = pcur->next;free(del);}ps->pSnake = NULL;SetPos(0, 26);free(ps->pFood);ps = NULL;
}

代码汇总

写了这么多,大概就介绍完了所有函数,现在将它们放到三个文件中,相应创建文件CV一下应该就能在你们的VS跑了

头文件-greedy_snake.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<windows.h>
#include<locale.h>
#include<time.h>
#define POS_X 24
#define POS_Y 5
#define WALL L'■' 
#define BODY L'●' 
#define FOOD L'¥' 
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)enum GAME_STATUES {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 Snake
{pSnakeNode pSnake;//指向蛇头节点的指针pSnakeNode pFood;//指向食物的食物指针int Score;//当前分数int FoodWeight;//食物比重int SleepTime;//休眠时间enum GAME_STATUES status;//游戏当前状态enum DIRECTION dir;//蛇当前方向
}Snake, * pSnake;//游戏开始的维护
void GameStart(pSnake ps);//绘制地图
void CreateMap();//初始化蛇
void InitSnake(pSnake ps);//初始化食物
void CreateFood(pSnake ps);//设置光标位置
void SetPos(int x, int y);//游戏运行维护函数
void GameRun(pSnake ps);//游戏结束善后
void GameEnd(pSnake ps);//蛇移动
void SnakeMove(pSnake ps);

源文件-greedy_snake.c

#include"greedy_snake.h"//设置光标位置
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}
void CreateMap()
{system("cls");SetPos(0, 0);//上for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2)wprintf(L"%lc", WALL);//左for (int i = 1; i <= 25; i++) {SetPos(0, i);wprintf(L"%lc", WALL);SetPos(56, i);wprintf(L"%lc", WALL);}SetPos(62, 15);printf("通过↑←↓→控制蛇的移动");SetPos(62, 16);printf("可以通过F3加速,F4减速");SetPos(62, 17);printf("更高的速度下可以获得更高的分数");SetPos(62, 18);printf("可以使用空格暂停");
}//初始化蛇
void InitSnake(pSnake ps)
{//创建五个蛇身节点pSnakeNode pcur = NULL;for (int i = 0; i < 5; i++) {pcur = (SnakeNode*)malloc(sizeof(SnakeNode));if (pcur == NULL) {perror("malloc 节点 fail:");exit(1);}pcur->x = POS_X + 2 * i;pcur->y = POS_Y;pcur->next = NULL;if (ps->pSnake == NULL) {ps->pSnake = pcur;}else {pcur->next = ps->pSnake;ps->pSnake = pcur;}}//打印蛇身pcur = ps->pSnake;while (pcur) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}//贪吃蛇信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;ps->status = OK;
}void CreateFood(pSnake ps)
{int xx = 0;int yy = 0;do{xx = rand() % 53 + 2;yy = rand() % 25 + 1;if (xx % 2 == 0) {pSnakeNode pcur = ps->pSnake;while (pcur) {if (xx == pcur->x && yy == pcur->y)goto again;pcur = pcur->next;}break;}again:;} while (1);pSnakeNode PFood = (SnakeNode*)malloc(sizeof(SnakeNode));if (PFood == NULL) {perror("malloc food fail:");exit(1);}PFood->x = xx;PFood->y = yy;ps->pFood = PFood;SetPos(xx, yy);wprintf(L"%lc", FOOD);
}void GameStart(pSnake ps)
{//下面五行使光标不可见HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO rem;GetConsoleCursorInfo(houtput, &rem);rem.bVisible = 0;SetConsoleCursorInfo(houtput, &rem);setlocale(LC_ALL, "");//设置为本地类项//初始化界面system("mode con cols=100 lines=30");//设置窗口大小system("title 贪吃蛇");//改窗口标题SetPos(32, 10);printf("欢迎来到贪吃蛇小游戏!\n");SetPos(33, 15);system("pause");system("cls");SetPos(29, 9);printf("游戏介绍:");SetPos(33, 11);printf("通过↑ ← ↓ →控制蛇的移动");SetPos(33, 13);printf("可以通过F3加速,F4减速");SetPos(33, 15);printf("更高的速度下可以获得更高的分数");SetPos(33, 17);printf("可以使用空格暂停");SetPos(33, 19);system("pause");//绘制地图CreateMap();//初始化创建蛇,传psInitSnake(ps);//初始化创建食物,传psCreateFood(ps);
}void EatFood(pSnake ps,pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode pcur = ps->pSnake;while (pcur) {SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);pcur = pcur->next;}ps->Score += ps->FoodWeight;free(ps->pFood);CreateFood(ps);
}void NotEatFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;//打印蛇pSnakeNode 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;SetPos(pcur->x, pcur->y);wprintf(L"%lc", BODY);
}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 pcur = ps->pSnake->next;while (pcur) {if (pcur->x == ps->pSnake->x && pcur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}pcur = pcur->next;}
}void SnakeMove(pSnake ps)
{pSnakeNode pNext = (SnakeNode*)malloc(sizeof(SnakeNode));if (pNext == NULL) {perror("malloc pNext fail:");exit(1);}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 (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {EatFood(ps, pNext);}else {NotEatFood(ps, pNext);KillByWall(ps);KillBySelf(ps);}
}void GameRun(pSnake ps)
{do {//打印游戏帮助信息SetPos(62, 10);printf("总分:%d\n", ps->Score);SetPos(62, 11);printf("食物分值:%2d\n", 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 = ESC;break;}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;}}else if (KEY_PRESS(VK_SPACE)){while (1) {Sleep(100);if (KEY_PRESS(VK_SPACE)) {break;}}}//睡一下Sleep(ps->SleepTime);//根据按键控制蛇的运动和吃食物,并打印SnakeMove(ps);} while (ps->status == OK);
}void GameEnd(pSnake ps)
{SetPos(20, 11);switch (ps->status){case ESC:printf("正常退出游戏\n");SetPos(20, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_WALL:printf("撞墙了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;case KILL_BY_SELF:printf("咬到自己了,游戏结束!\n");SetPos(23, 13);printf("你的得分是%d", ps->Score);break;}pSnakeNode pcur = ps->pSnake;pSnakeNode del = ps->pSnake;while (pcur) {del = pcur;pcur = pcur->next;free(del);}ps->pSnake = NULL;SetPos(0, 26);free(ps->pFood);ps = NULL;
}

运行文件-snake_run.c

#include"greedy_snake.h"
int main()
{srand((unsigned int)time(NULL));int ch;do {Snake snake = { 0 };snake.pSnake = NULL;GameStart(&snake);GameRun(&snake);GameEnd(&snake);SetPos(20, 15);printf("想要再来一局吗?Y/N:");ch = getchar();} while (ch == 'Y' || ch == 'y');SetPos(0, 26);return 0;
}

 运行截图

 

 

 

结尾

到这里,本篇博客的内容基本上就结束了,写博客不易,如果感觉对你有帮助的话,还请留个赞留个关注再走啊。博主的C语言语法学习之路到现在也算是真正结束,统计下来C语言将近学了三四遍了,在后面的时间里,我准备好好开始过数据结构的内容,这些时日是没有特别多的时间去写题攻算法了,给自己报了一堆比赛还需要去准备,还是要先把C++和Python在假期赶快速成一下,数据结构系统仔细的过上一遍,给未来打好基础。后期我还会继续产出有意思的内容,请大家多多关注我吧!

在这里记录一下,今天是2024.1.31,大一的寒假♥

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

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

相关文章

【LeetCode】每日一题 2024_1_30 使循环数组所有元素相等的最少秒数(哈希、贪心、扩散)

文章目录 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01;题目&#xff1a;使循环数组所有元素相等的最少秒数题目描述代码与解题思路 LeetCode&#xff1f;启动&#xff01;&#xff01;&#xff01; 今天的题目类型差不多是第一次见到&#xff0c;原来题目描述…

【PyCharm教程】PyCharm 安装、卸载和升级包

PyCharm 为特定的 Python 解释器提供了安装、卸载和升级 Python 包的方法。默认情况下&#xff0c;PyCharm 使用 pip 来管理项目包。对于 Conda 环境&#xff0c;您可以使用conda 包管理器。 在 PyCharm 中&#xff0c;您可以在Python 包工具窗口和 Python 解释器Settings/Pre…

化工企业能源在线监测管理系统,能源管理新利器

化工企业在开展化工生产活动时&#xff0c;能源消耗量较大&#xff0c;其节能潜力空间也较大&#xff0c;因此必须控制能耗强度&#xff0c;促进能效水平的稳步提升。化工企业通过能源现状的分析&#xff0c;能够实现能源使用情况的实时反馈与监管&#xff0c;从而达到节能减排…

直播不仅可以带货,还可以远程协作

直播是一种非常直观高效的信息共享模式&#xff0c;目前直播带货比较火&#xff0c;但我也不懂&#xff0c;现就我涉及的领域和实践做一些分享&#xff0c;目前我所做的直接互动分发系统在软硬件全系统闭环下&#xff0c;结合100ms级的低延迟&#xff0c;基本可以让人有深入其境…

全志R128基础组件开发——显示与屏幕驱动②

sys_config 参数说明 LCD 接口参数说明 lcd_driver_name Lcd 屏驱动的名字&#xff08;字符串&#xff09;&#xff0c;必须与屏驱动的名字对应。 lcd_model_name Lcd 屏模型名字&#xff0c;非必须&#xff0c;可以用于同个屏驱动中进一步区分不同屏。 lcd_if Lcd Inte…

Trinamic推出步进电机低压微型电机驱动芯片

前言 TRINAMIC运动控制有限公司宣布推出全球最小的具有专利技术StealthChop™的单芯片电机驱动器。TMC2300为2相步进电机设置了高达1.2A RMS的标准和1.8V…11V DC的电压范围&#xff0c;它只需最低的功耗&#xff0c;让人根本觉察不到有电机的存在。 从现在开始&#xff0c;您只…

手把手教测试,全网内容最全最深-jmeter-Recording Controller(录制控制器)

5.1.6.14.Recording Controller(录制控制器) 第一步&#xff1a; 第二步&#xff1a;点击启动按钮&#xff0c;生成证书。证书在jmeter的bin目录下。 第三步&#xff1a;设置代理 第四步&#xff1a;抓取https包需要安装证书&#xff0c;在浏览器edge中安装 未完待续。。。 手…

防火墙的基础知识点

目录 1. 防火墙的定义&#xff1a; 2. 防火墙分类&#xff1a; 3. 防火墙的发展进程&#xff1a; 3.1传统防火墙 (包过滤防火墙)---一个严格的规则表&#xff1a; 3.2传统防火墙(应用代理防火墙)---每个应用添加代理 3.3传统防火墙 (状态检测防火墙)---首次检建立会话表…

【MBtiles数据索引和服务发布】GeoServer改造Springboot番外系列二

xyz地图服务访问示例&#xff1a;http://192.168.1.240:8081/gmserver/raster/xyz/firstWP:Imagery-raster/{z}/{x}/{y}.jpg 访问示例如下&#xff1a; mbtiles目录结构 根据z&#xff0c;x&#xff0c;y获取对应mbtiles文件路径的工具方法 说明&#xff1a;重点是使用getMb…

数据结构总结

数据结构总结 数据结构系列的文章从基本数据类型到数据结构&#xff0c;涵盖整型、布尔值、字符串、列表、元组、字典、集合、堆、栈、链表、树和图&#xff0c;以及队列和环等相关内容。 这里再补充一点&#xff1a;树和图都有更高阶的“玩法”&#xff0c;那就是加权——为每…

面对近期行情大起大落的伦敦银需要关注什么?

近期经常有听到投资者抱怨说&#xff0c;伦敦银价格没有明显趋势&#xff0c;很难做。确实&#xff0c;我们从日线图看&#xff0c;金价处于一个比较宽幅的横盘区间当中&#xff0c;近期的行情也是大涨大跌。投资者认为&#xff0c;面对大起大落的行情无从下手。下面我们就来讨…

必读人工智能数据管理的要点!

人工智能数据管理指企业如何管理系统中的数据。它是企业整体AI治理框架的关键组成部分。 在数据治理中&#xff0c;您可能希望包括以下要素&#xff1a; 可用性 数据提供给有需求的人员访问和使用。本文将回答企业中可访问数据人员的问题。 易用性 数据是结构化、已标注且…

河南省考后天网上确认,请提前准备证件照哦

✔报名时间&#xff1a;2024年1月18号一1月24号 ✔报名确认和缴费&#xff1a;2024年1月 31号一2月4号 ✔准考证打印&#xff1a;2024年3月12号一3月17号 ✔笔试时间&#xff1a;2024年3月16日-2024年3月17日。 ✔面试时间&#xff1a;面试时间拟安排在2024年5月中旬 报名网址&…

CCF-CSP 202312-2 因子化简(Java、C++、Python)

文章目录 因子化简题目背景问题描述输入格式输出格式样例输入样例输出样例解释子任务 满分代码JavaCPython线性筛法 因子化简 题目背景 质数&#xff08;又称“素数”&#xff09;是指在大于 1 的自然数中&#xff0c;除了 1 和它本身以外不再有其他因数的自然数。 问题描述…

2024.1.30

快速排序降序 #include<stdio.h> #include<string.h> #include<stdlib.h> int quick_sort(int arr[],int low,int high) {//基准值int keyarr[low];int low1low,high1high;if(low>high) return 0;while(low<high) {//high开始比较while(low1<high1…

安卓native报错:.cmake\api\v1\reply was not a directory

目录 前言一、报错信息二、报错分析总结 前言 之前编译的native工程提示找不到变量&#xff0c;使用Android studio新建的native工程也报错&#xff0c;这个原因苦了我好久&#xff0c;今天有时间仔细分析了下&#xff0c;希望能对同样有此问题的小伙伴有帮助。 一、报错信息…

提高 NFS Azure 文件共享性能

本文内容 适用于增加预读大小以提高读取吞吐量Nconnect另请参阅 本文介绍如何提高 NFS Azure 文件共享的性能。 适用于 展开表 文件共享类型SMBNFS标准文件共享 (GPv2)、LRS/ZRS 标准文件共享 (GPv2)、GRS/GZRS 高级文件共享 (FileStorage)、LRS/ZRS 增加预读大…

炒黄金 vs 炒股:探寻投资路线的差异和各自的优势

在当前不景气的股市&#xff0c;人们越来越关注分散投资的方式&#xff0c;以期降低风险并稳定资产。炒黄金成为了一个备受关注的投资选择&#xff0c;与传统炒股相比&#xff0c;它到底有什么区别呢&#xff1f;本文将从多个维度深入分析这两种投资方式的差异以及各自的优势。…

企业数字化转型会遇到哪些问题?思路是怎样的?

企业数字化转型在互联网时代的迅速发展下&#xff0c;行业领军企业都在逐步实现数字经济的发展&#xff0c;响应国家号召&#xff0c;将数字化技术应用完全应用到自身的企业&#xff0c;以提升企业价值。 那企业为什么要进行数字化转型呢? 数字化转型是在企业信息化已经完成的…

【Matplotlib】科研绘图——折线图

文章目录 1、导入2、定义Font及Style3、设置图像大小及坐标刻度4、数据准备5、自定义draw6、其他设置7、效果图 1、导入 import matplotlib import matplotlib.pyplot as plt from matplotlib.backends.backend_pdf import PdfPages import numpy as np import pandas as pd %…