贪吃蛇---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();
	//初始化创建蛇,传ps
	InitSnake(ps);
	//初始化创建食物,传ps
	CreateFood(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();
	//初始化创建蛇,传ps
	InitSnake(ps);
	//初始化创建食物,传ps
	CreateFood(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,大一的寒假♥

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ 之LeetCode刷题记录(二十四)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅。 目标&#xff1a;执行用时击败90%以上使用 C 的用户。 119. 杨辉三角 II 给定一个非负索引 rowIndex&#xff0c;返回「杨辉三角」的第 rowI…

基于stm32F4卷积神经网络手写数字识别项目

加我微信hezkz17 可以申请加入嵌入式人工智能技术研究开发交流答疑群&#xff0c;赠送企业嵌入式AI 图像理解/音/视频项目核心开发资料 1 采用CNN BP反向传播算法更新权重系数 2 原理解析 3 实现策略 训练与识别分离&#xff0c;先在电脑上训练好CNN BP神经网络的模型&#…

音视频数字化(数字与模拟-音频广播)

在互联网飞速发展的今天,每晚能坐在电视机前面的人越来越少,但是每天收听广播仍旧是很多人的习惯。 从1906年美国费森登在实验室首次进行无线电广播算起,“广播”系统已经陪伴人们115年了。1916年,收音机开始上市,收音机核心是“矿石”。1920年开始“调幅”广播,1941年开…

又涨又跌 近期现货黄金价格波动怎么看?

踏入2024年一月的下旬&#xff0c;现货黄金价格可以说没了之前火热的状态&#xff0c;盘面上是又涨又跌。面对这样的行情&#xff0c;很多投资者不知道如何看了。下面我们就来讨论一下怎么把握近期的行情。 先区分走势类型。在现货黄金市场中有两种主要的走势类型&#xff0c;一…

WebAssembly核心编程[1]:wasm模块实例化的N种方式

当我们在一个Web应用中使用WebAssembly&#xff0c;最终的目的要么是执行wasm模块的入口程序&#xff08;通过start指令指定的函数&#xff09;&#xff0c;要么是调用其导出的函数&#xff0c;这一切的前提需要创建一个通过WebAssembly.Instance对象表示的wasm模块实例(源代码…

基于flask的个人博客项目从0到1

项目展示(持续完善中…) 首页 文章时间线页面 笔记页面 留言页面 关于页面 后台页面-文章管理 后台页面-笔记页面 后台页面-分类 后台管理-新增标签 后台管理-标签页面 后台管理-新增标签 后台管理-关于页面 2.项目详述 该博客开源地址点击跳转&#xff0c;该项目已部署上…

【代码随想录19】235.二叉搜索树的最近公共祖先 701.二叉搜索树中的插入操作 450.删除二叉搜索树中的节点

目录 235.二叉搜索树的最近公共祖先题目描述参考代码 701.二叉搜索树中的插入操作题目介绍参考代码 450.删除二叉搜索树中的节点题目描述参考代码 235.二叉搜索树的最近公共祖先 题目描述 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖…

自治系统 AS 、路由选择协议、

目录 路由算法分类&#xff08;自适应&#xff09; 分层次的路由选择协议 自治系统 AS (Autonomous System) 2 大类路由选择协议 自治系统和内部网关协议、外部网关协议 路由选择协议属于网络层控制层面的内容 路由算法分类&#xff08;自适应&#xff09; 分层次的路由选…

vue3中多个表格怎么同时滚动并且固定表头

说明&#xff1a;这里需分为两种情况来做。第一种亲情况就是没有修改过el-table这个组件的样式&#xff1b;第二种情况就是修改过el-table组件的样式。第一种较为简单就简单略过&#xff0c;这里主要提及第二种做法。 1.需求效果 2.第一种没有修改过el-table这个组件的样式的做…

存内计算技术—解决冯·诺依曼瓶颈的AI算力引擎

文章目录 存内计算技术背景CSDN首个存内计算开发者社区硅基光电子技术存内计算提升AI算力知存科技存算一体芯片技术基于存内计算的语音芯片的实现挑战 参考文献 存内计算技术背景 存内计算技术是一种革新性的计算架构&#xff0c;旨在克服传统冯诺依曼架构的瓶颈&#xff0c;并…

详讲api网关之kong的基本概念及安装和使用(二)

consul的服务注册与发现 如果不知道consul的使用&#xff0c;可以点击上方链接&#xff0c;这是我写的关于consul的一篇文档。 upstreamconsul实现负载均衡 我们知道&#xff0c;配置upstream可以实现负载均衡&#xff0c;而consul实现了服务注册与发现&#xff0c;那么接下来…

防御第五次作业-防火墙综合实验(nat、双机热备、带宽、选路)

目录 拓扑图 要求 1 2 3 4 5 办公区上网用户限制流量不超过60M 销售部限流 6 7 拓扑图 说明&#xff1a;基本配置完成。所有设备均可出网。 要求 1.办公区设备可以通过电信和移动两条链路上网&#xff0c;且需要保留一个公网ip不能用来转换。 2.分公司设备可以通…

CANoe实际项目中文件夹的规划

本人&#xff0c;之前设计了一个CANoe工程&#xff0c;由于工程设计之初没有设计好文档的归纳分类&#xff0c;导致文件查找起来非常费劲。 为了避免以后出现文件混乱&#xff0c;不可查找的问题&#xff0c;故特此归纳说明。 建立工程时&#xff1a; 第1步就应该设计好文档…

InputNumber数字输入框(antd-design组件库)简单使用

1.InputNumber数字输入框 通过鼠标或键盘&#xff0c;输入范围内的数值。 2.何时使用 当需要获取标准数值时。 组件代码来自&#xff1a; 数字输入框 InputNumber - Ant Design 3.本地验证前的准备 参考文章【react项目antd组件-demo:hello-world react项目antd组件-demo:hello…

采用GaussDB(for MySQL)完成商场会员卡管理系统设计

这篇文章介绍了如何购买、配置、连接、测试 GaussDB数据库&#xff0c;并且最终采用Qt开发了一个具体的软件演示了数据库的具体应用&#xff0c;演示了数据库整体的使用过程。 一、什么是GaussDB&#xff1f; GaussDB是华为自主创新研发的分布式关系型数据库。该产品支持分布…

排序链表---归并--链表OJ

https://leetcode.cn/problems/sort-list/submissions/499363940/?envTypestudy-plan-v2&envIdtop-100-liked 这里我们直接进阶&#xff0c;用时间复杂度O(nlogn)&#xff0c;空间复杂度O(1)&#xff0c;来解决。 对于归并&#xff0c;如果自上而下的话&#xff0c;空间复…

SpringAop实现访问日志功能的添加

AOP 是 Spring 体系中非常重要的两个概念之一&#xff08;另外一个是 IoC&#xff09;&#xff0c;今天这篇文章就来带大家通过实战的方式&#xff0c;在编程猫 SpringBoot 项目中使用 AOP 技术为 controller 层添加一个切面来实现接口访问的统一日志记录。 #一、关于 AOP AO…

springboot3+vue3支付宝交易案例-结算支付

springboot3vue3支付宝交易案例-结算支付&#xff01;今天下午整理了一下结算的内容。遇到了很多问题。汇总分享给大家。 第一个问题&#xff1a;支付宝结算后&#xff0c;返回的交易编码&#xff0c;和交易时间&#xff0c;交易状态&#xff0c;都应该使用varchar来存。 第二…

DMA+串口空闲中断实现RS485不定长数据接收和发送

目录 1、环境说明2、实现不定长数据接收需要做哪些事&#xff1f;2.1、数据的接收与缓存2.2、数据帧的结束判断2.3、数据帧的长度计算 3、RS485串口实现不定长数据发送4、代码实现结语&#xff1a; 1、环境说明 单片机型号;Cortex-M4架构&#xff0c;AT32F437 说明&#xff1a…

C语言操作符

文章目录 1:算术操作符2:移位操作符(移动的是二进制序列中的补码)2.1:知识补充(原码,反码,补码与二进制)2.2:左移操作符(<<)2.2:右移操作符(>>)2.2.1:逻辑右移2.2.2:算术右移 3:位操作符(运算用的是二进制位的补码)3.1:按位与操作符(&)3.2:按位或操作符(|)3.3:…