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

news2024/12/28 19:48:39

引言

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,大一的寒假♥

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

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

相关文章

Unity | 资源热更(YooAsset AB)

目录 一、AssetBundle 1. 插件AssetBundle Browser 打AB包 &#xff08;1&#xff09;Unity&#xff08;我用的版本是2020.3.8&#xff09;导入AssetBundle Browser &#xff08;2&#xff09;设置Prefab &#xff08;3&#xff09;AssetBundleBrowser面板 2. 代码打AB包…

算法:积木游戏学习数学

一、算法描述 小华和小微一起通过玩积木游戏学习数学。 他们有很多积木&#xff0c;每个积木块上都有一个数字&#xff0c;积木块上的数字可能相同。 小华随机拿一些积木挨着排成一排&#xff0c;请小微找到这排积木中数字相同且所处位置最远的2块积木块&#xff0c;计算他们的…

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

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

【大数据】Flink 架构(四):状态管理

《Flink 架构》系列&#xff08;已完结&#xff09;&#xff0c;共包含以下 6 篇文章&#xff1a; Flink 架构&#xff08;一&#xff09;&#xff1a;系统架构Flink 架构&#xff08;二&#xff09;&#xff1a;数据传输Flink 架构&#xff08;三&#xff09;&#xff1a;事件…

GPIO中断

1.EXTI简介 EXTI是External Interrupt的缩写&#xff0c;指外部中断。在嵌入式系统中&#xff0c;外部中断是一种用于处理外部事件的机制。当外部事件发生时&#xff08;比如按下按钮、传感器信号变化等&#xff09;&#xff0c;外部中断可以立即打断正在执行的程序&#xff0…

异常——浅析

异常 本人不才&#xff0c;对于异常只能做基本的介绍&#xff0c;无法给出自己的体会以及无法指明易错点 C异常 c语言处理异常的方式有 assert——直接中断程序 返回错误码——需要查找错误码表确定错误 这两种操作都不是很好&#xff0c;如果一个大型程序&#xff0c;为了找…

UE5动画源码剖析

重点剖析的类&#xff1a; UAnimationInstanceFAnimInstanceProxy 参考&#xff1a;https://zhuanlan.zhihu.com/p/405437842 参考&#xff1a;https://blog.csdn.net/qq_23030843/article/details/109103433 参考&#xff1a;https://ikrima.dev/ue4guide/gameplay-programm…

Opencv——霍夫变换

霍夫直线变换 霍夫直线变换(Hough Line Transform)用来做直线检测 为了加升大家对霍夫直线的理解,我在左图左上角大了一个点,然后在右图中绘制出来经过这点可能的所有直线 绘制经过某点的所有直线的示例代码如下,这个代码可以直接拷贝运行 import cv2 as cv import matplot…

自建DNS劫持服务器,纯内网劫持PS5,屏蔽更新,自动hen

背景&#xff1a;目前PS5首次折腾必须要连外网&#xff0c;还要改DNS&#xff0c;除非使用ESP8266/32&#xff0c; 本文的方法是完全不改DNS&#xff0c;不使用ESP8266,不连接外网的情况下自动折腾 能实现什么&#xff1a; 1.折腾全程不连接外网 2.完全自建hen服务器&#xff…

Vue(十九):ElementUI 扩展实现树形结构表格组件的勾父选子、半勾选、过滤出半勾选节点功能

效果 原理分析 从后端获取数据后,判断当前节点是否勾选,从而判断是否勾选子节点勾选当前节点时,子节点均勾选全勾选与半勾选与不勾选的样式处理全勾选和全取消勾选的逻辑筛选出半勾选的节点定义变量 import {computed, nextTick, reactive, ref} from vue; import {tree} f…

我该坚持纯正原创?还是随波逐流做搬运作者?

本文可能不是一个热点文章&#xff0c;甚至可能不是一个网创者该关心的文章&#xff0c;但是阿阳真心希望&#xff0c;大家可以静下心来看看。 阿阳在网赚领域混了差不多十年&#xff0c;这么多年也经历不少。我不想吹嘘什么&#xff0c;可能是因为年纪大了&#xff0c;觉得低调…

毕业设计----Ajax请求遇到的问题Uncaught TypeError: Cannot read properties of undefi

目录 问题 问题 总的来说&#xff0c;就是我在form表单新增了一个字段&#xff0c;在表单的验证规则中添加了一个名为 code 的自定义验证规则&#xff0c;但是没有提供该规则的实现代码&#xff0c;validate.js提示报错。 所以为了使验证规则生效&#xff0c;添加自定义验证方…

BODIPY FL NHS,BODIPY FL NHS 活化酯,可以实现对特定生物分子的可视化追踪和定位

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;BODIPY FL NHS ester&#xff0c;BODIPY FL NHS&#xff0c;BODIPY FL NHS 活化酯 一、基本信息 产品简介&#xff1a;BODIPY FL NHS ester, by combining BODIPY with NHS ester, this reagent is able to react w…

【CSS】常见

一. 溢出隐藏 1.1 单行文本溢出 .content{max-width:200px; /* 定义容器最大宽度 */overflow:hidden; /* 隐藏溢出的内容 */text-overflow:ellipsis; /* 溢出部分...表示 */white-space: nowrap; /* 确保文本在一行内显示 */ }问题&#xff1a;display:flex 和 ellipsis 冲…

Java学习之基础语法

Java学习之基础语法 本文主要是对于有了其他语言基础的人总结的资料&#xff0c;因此本文只写出了Java与C语言&#xff0c;C等语言的区别之处与部分重点。 1.基础语法&#xff1a; 1.1.包与类&#xff1a; 1.1.1.包&#xff1a; 在Java中&#xff0c;包&#xff08;packag…

Asp.net移除Server, X-Powered-By, 和X-AspNet-Version头

移除X-AspNet-Version很简单,只需要在Web.config中增加这个配置节: <httpRuntime enableVersionHeader"false" />移除Server在Global.asax文件总增加&#xff1a; //隐藏IIS版本 protected void Application_PreSendRequestHeaders() {HttpContext.Current.Res…

【题解 拓扑思维】 C - Building Company

题目描述: 分析&#xff1a; 对于每一个项目&#xff0c;需要满足几个条件&#xff0c;对于每一个条件&#xff0c;表示为第i项工作需要有几个人做。 这几个条件全部满足后&#xff0c;这个项目就可以收入囊下&#xff0c;同时获得新的员工 对于每一个项目的几个条件&#xf…

vxe-table3.0的表格树如何做深层查找,返回搜索关键字的树形结构

vxe-table2.0版本是提供深层查找功能的&#xff0c;因为他的数据源本身就是树形结构&#xff0c;所以深层查找查询出来也是树形结构。 但是vxe-table3.0版本为了做虚拟树功能&#xff0c;将整个数据源由树形垂直结构变成了扁平结构&#xff0c;便不提供深层查询功能&#xff0c…

代码随想录 Leetcode108. 将有序数组转换为二叉搜索树

题目&#xff1a; 代码(首刷自解 2024年1月31日&#xff09;&#xff1a; class Solution { public:TreeNode* recursion(vector<int>& nums, int left, int right) {if (left > right) return nullptr;int mid left (right-left)/2;TreeNode* node new TreeN…

Linux 入门基础知识(一)—— Linux的基本使用

Linux 入门基础知识 一、Linux的基本使用和配置1.1、终端1.2、消耗内存1.3、运行级别1.6、登录前欢迎语1.5、登录后欢迎语1.6、shell1.7、ps aux1.8、设置主机名1.9、whoami和who am i1.10、命令提示符 二、Linux执行命令的过程详解和命令类型2.1、命令执行2.2、hash缓存表2.3、…