【C语言】项目实践-贪吃蛇小游戏(Windows环境的控制台下)

一.游戏要实现基本的功能:

• 贪吃蛇地图绘制

• 蛇吃食物的功能 (上、下、左、右方向键控制蛇的动作)

• 蛇撞墙死亡

• 蛇撞自身死亡

• 计算得分

• 蛇身加速、减速

• 暂停游戏

二.技术要点

C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。

三.补充知识:Win32 API

1.Win32 API介绍

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

2.控制台程序

平常我们运行起来的命令提示符(黑框框程序)其实就是控制台程序,

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

mode con cols=100 lines=30

也可以通过命令设置控制台窗口的名字

title 贪吃蛇


这些能在控制台窗口执行的命令,也可以调用C语言函数system(需要包含头文件<windows.h>)来执行。

#include<windows.h>

int main()
{

		//设置控制台窗口的⻓宽:设置控制台窗口的⼤小,30行,100列
		system("mode con cols=100 lines=30");
		//设置cmd窗口名称
		system("title 贪吃蛇");
		return 0;
} 0;
(因相关命令知识较多,本文仅根据游戏需要处使用相关命令,诸如换行、改标题,不会具体讲解某个命令,感兴趣的读者可自行了解)
参考:mode命令
参考:title命令

3.控制台屏幕上的坐标COORD

正常我们运行程序输出信息,仅仅通过‘\n’来实现不同行当输出,并且输出默认其实位置时是都是定格开始,但是如果我们要创建墙体,在特定出打印相关信息,我们就需要能够将光标移动到对应位置。这就涉及到窗体的坐标系了。

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

但需要注意的是控制台屏幕上的坐标COORD的x,y的一个大小并不是一一对应关系,由于宽窄字符的区别,x的一个单位长度设置的较小,在长度上,y的一个单位差不多相当于x的2个单位。

COORD的声明:

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

给坐标值赋值:

1 COORD pos = { 10, 15 };

4.GetStdHandle

在知道坐标系的概念后,我们还不能直接的去操控光标的移动,就像我们访问一些博物馆,机密的地方时,我们需要递交身份证或者一些凭证来获得访问权限一样,为了能够对光标进行操作,我们也需要凭证来获得权限。

GetStdHandle是⼀个Windows API函数。它用于从⼀个特定的标准设备(标准输入、标准输出或标准错误)中取得⼀个句柄(用来标识不同设备的数值),这个句柄就相当于凭证,通过句柄我们可以操作对应设备。

GetStdHandle的返回值就是对应设备的句柄,形参通过接受STD_INPUT_HANDLE、 STD_OUTPUT_HANDLE、STD_ERROR_HANDLE三个值来获得输入输出及错误设备的句柄

1 HANDLE GetStdHandle(DWORD nStdHandle);
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
含义
STD_INPUT_HANDLE((DWORD)-10)标准输入设备。 最初,这是输入缓冲区 CONIN$ 的控制台
STD_OUTPUT_HANDLE((DWORD)-11)标准输出设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$
STD_ERROR_HANDLE((DWORD)-12)标准错误设备。 最初,这是活动控制台屏幕缓冲区 CONOUT$

5.GetConsoleCursorInfo

在凭证后,我们有了对光标操作的权限,之后要实现对光标的一些操作,我们就要找到存放光标信息的箱子,将原有的信息按照我们想要的进行修改,然后将修改的箱子在放回去。

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类型的箱子用来存放GetConsoleCursorInfo获得的光标信息。

6.CONSOLE_CURSOR_INFO

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

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

dwSize是由光标填充的字符单元格的百分比。 此值介于1到100之间。 改变dwsize光标外观会变化,范围从完全填充单元格到单元底部的水平线条。

• bVisible是表示游标的可见性。 如果光标可见,则此成员为 TRUE,否则为FLASE(注意需要包含<stdbool.h>头文件)。

7.SetConsoleCursorInfo

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

#include<windows.h>
#include<stdbool.h>
int main()
{

		// 获得标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	// 定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = {0};

	// 获取和 houtput 句柄相关的控制台上的光标信息,存放在 cursor_info 中
	GetConsoleCursorInfo(houtput, &cursor_info);

	// 修改光标的占比
	cursor_info.dwSize = 100;

	// 设置和 houtput 句柄相关的控制台上的光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);
	
	system("pause");
		return 0;
}

#include<windows.h>
#include<stdbool.h>
int main()
{
		// 获得标准输出设备的句柄
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	// 定义一个光标信息的结构体
	CONSOLE_CURSOR_INFO cursor_info = {0};

	// 获取和 houtput 句柄相关的控制台上的光标信息,存放在 cursor_info 中
	GetConsoleCursorInfo(houtput, &cursor_info);

	cursor_info.bVisible = false;

	// 设置和 houtput 句柄相关的控制台上的光标信息
	SetConsoleCursorInfo(houtput, &cursor_info);
	
	system("pause");
		return 0;
}

8.SetConsoleCursorPosition

设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。

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

8.1贪吃蛇游戏光标定位

如果直接使用SetConsoleCursorPosition,步骤过于繁琐因此,我们将上述步骤专门分装成用于光标定位的函数。

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

  9.GetAsyncKeyState

在贪吃蛇游戏中,我们会通过按上下左右键来改变方向,通过按F3,F4改变速度等,因此我们需要能够检测键是否被按过因为本游戏是按一次键相应作出变换,因此,我们不考虑一直按键的情况。

为了区分对应的键及相关信息的传递,我们规定的虚拟键值这一概念,对应键有对应的整型值(16进制形式),注意这不要与ASCII值混淆。

参考:虚拟键码 (Winuser.h) - Win32 apps

GetAsyncKeyState就是这个用途的函数,函数原型如下:
SHORT GetAsyncKeyState(
int vKey
)

将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。

GetAsyncKeyState 的返回值是short类型,在上⼀次调用GetAsyncKeyState 函数后,如果返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最高是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1.

实例:检测数字键:

#include <stdio.h>
#include <windows.h>
int main()
{
while (1)
{
if (KEY_PRESS(0x30))
{
printf("0\n");
}
else if (KEY_PRESS(0x31))
{
printf("1\n");
}
else if (KEY_PRESS(0x32))
{
printf("2\n");
}
else if (KEY_PRESS(0x33))
{
printf("3\n");
}
else if (KEY_PRESS(0x34))
{
printf("4\n");
}
else if (KEY_PRESS(0x35))
{
printf("5\n");
}
else if (KEY_PRESS(0x36))
{
printf("6\n");
}
else if (KEY_PRESS(0x37))
{
printf("7\n");
}
else if (KEY_PRESS(0x38))
{
printf("8\n");
}
else if (KEY_PRESS(0x39))
{
printf("9\n");
}
}
return 0;
}


9.1贪吃蛇游戏按键识别:

在贪吃蛇游戏中,我们可以通过GetAsyncKeyState返回值按位与1的值是1还是0来判断键是否被按过,为了方便,在这里笔者定义了一个宏命令。

1 #define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

四.补充知识:C语言的国际化与本地化

1.C语言国际化与本土化的由来

在游戏地图上,我们打印墙体使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★

但是,我们发现我们无法直接打印出来,这是为什么呢?因为这类字符属于宽字符。而因为字符属于窄字符。

普通的字符是占⼀个字节的,这类宽字符是占用2个字节。过去C语言并不适合非英语国家(地区)使用。C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。

C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就就法用 ASCII 码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样⼀来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (),在俄语编码中用会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是⼀样的,不⼀样的只是128--255的这⼀段。

至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。⼀个字节只能表256种符号,肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示256 x 256 = 65536 个符号。

后来为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型

wchar_t 和宽字符的输入和输出函数,加入了<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数

2.<locale.h>本地化

<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不⼀样行为的部分,使用该头文文件会自动检测系统使用的地区、时间等。 在标准中,依赖地区的部分有以下几项:

• 数字量的格式

• 货币量的格式

• 字符集

• 日期和时间的表示形式

实验代码:

#include <stdio.h>      /* printf */
#include <time.h>       /* time_t, struct tm, time, localtime, strftime */
#include <locale.h>     /* struct lconv, setlocale, localeconv */

int main()
{
    time_t rawtime;
    struct tm* timeinfo;
    char buffer[80];

    struct lconv* lc;

    time(&rawtime);
    timeinfo = localtime(&rawtime);

    int twice = 0;

    do {
        printf("Locale is: %s\n", setlocale(LC_ALL, NULL));

        strftime(buffer, 80, "%c", timeinfo);
        printf("Date is: %s\n", buffer);

        lc = localeconv();
        printf("Currency symbol is: %s\n-\n", lc->currency_symbol);

        setlocale(LC_ALL, "");
    } while (!twice++);

    return 0;
}

3.类项

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

• LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。

• LC_CTYPE:影响字符处理函数的行为。

• LC_MONETARY:影响货币格式。

• LC_NUMERIC:影响 printf() 的数字格式。

• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。

• LC_ALL - 针对所有类项修改,将以上所有类别设置为给定的语言环境。

参考:每个类项

4.setlocale

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

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

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

C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。

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

setlocale(LC_ALL, "C");

当地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点。

当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale函数就可以切换到本地模式,这种模式下程序会适应本地环境。

比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等

1 setlocale(LC_ALL, " ");

4.宽字符的打印:wprintf

那如果想在屏幕上打印宽字符,怎么打印呢?

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

附:读者可能会疑惑我们平时使用的printf()不是也可以打印宽字符汉字吗,两个函数的打印有什么区别吗?

printf()打印的汉字在中国大陆常见的是 GB2312、GBK 或者 UTF-8 编码。这个编码通常由系统的 locale 设置决定。

wprintf()打印的汉字通常采用的编码方式是宽字符编码,比如 UTF-16 或者 UTF-32。因此打印宽字符时选用wprintf()会更好

五.贪吃蛇游戏实现:

1.规划思路如下:

#include"snake.h"


void test()
{
	int ch = 0;
	srand((unsigned int)time(NULL));//通过rand产生随机坐标来产生随机食物的效果
	do
	{
		snake sk = { 0 };
		GameStart(&sk);
		GameRun(&sk);
		GameEnd(&sk);
		SetPos(25, 15);
		printf("您是否再来一局?(Y/N):");
		ch = getchar();
		while (getchar() != '\n');
	} while (ch == 'Y'|| ch == 'y');

}

int main()
{
	setlocale(LC_ALL, "");//修改当前地区为本地模式,为了支持中文宽字符及特殊字符的打印
	test();//测试逻辑
	SetPos(0, 27);//将光标移动至地图下方,不会破坏地图
	return 0;
}

2.游戏主逻辑:

程序开始就设置程序支持本地模式,然后进入游戏的主逻辑。主逻辑分为3个过程:

• 游戏开始(GameStart)完成游戏的初始化

• 游戏运行(GameRun)完成游戏运行逻辑的实现

• 游戏结束(GameEnd)完成游戏结束的说明,实现资源释放

2.1游戏开始(GameStart)

这个模块完成游戏的初始化任务:

• 控制台窗口大小的设置

• 控制台窗口名字的设置

• ⿏标光标的隐藏

• 打印欢迎界面

• 创建地图

• 初始化蛇

• 创建第⼀个食物

void GameStart(psnake ps)
{
	WelcomeToGame();//设置窗口名称大小、隐藏光标,打印欢迎界面
	CreatMap();//打印欢迎界面
	InitSnake(ps);//初始化蛇及其相关信息
	CreateFood(ps);//创造第一个食物

}
2.1.1打印欢迎界面
2.1.1.1.窗口设置

由于本项目是通过控制台窗口实现的,但是win系统升级之类缘故,C语言开发环境调用的窗口,使用的初始设置无法很好的实现我们想要的效果,因此,这里要先更改一下设置。首先运行程序,弹出黑框,然后鼠标移动到如下区域右键,点击设置。

将默认终端应用程序改为windows控制台主机。

更改完成后,如果对颜色不太满意,可以右键弹出窗口,点击设置,在颜色中调整窗体,在这里,笔者为了演示效果,调成了灰色。

在游戏正式开始之前,我们可以做⼀些功能提醒,这里我们可以先设置窗体大小,名称,需要注意的是我们除了创建地图之外,地图旁边还需留有空间显示游戏相关信息及提示。因此笔者设计窗体

行30,列100.地图大小是实现⼀个棋盘26行,58列的棋盘(行和列可以根据自己的情况修改)。

	//设置控制台窗口的大小,30行,100列
    //mode为DOS命令
	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);//设置控制台光标状态

void WelcomeToGame()
{

	//SetPos调整光标位置
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇小游戏!");
	SetPos(40, 25);
	system("pause");
	system("cls");
	SetPos(25, 12);
	printf("用↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(25, 13);
	printf("加速将能得到更高的分数。\n");
	SetPos(40, 25);
	system("pause");
	system("cls");
}
void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;
	houtput = GetStdHandle(STD_OUTPUT_HANDLE);
	SetConsoleCursorPosition(houtput, pos);

}

在上述代码中,我们使用pause指令,实现程序不退出的效果即屏幕上”请按任意键退出“,如果不加上这句换或者其他相似效果的语句,程序直接退出,我们是无法看到窗体标题的改变的,同时为了实现换界面的效果,我们可以在pause语句后加上cls清屏指令。

2.1.2.创建地图

创建地图就是将墙打印出来,因为是宽字符打印,所以使用wprintf函数,打印格式串前使用L

打印地图的关键是要算好坐标,才能在想要的位置打印墙体。为了方便修改,如同蛇身、食物、

墙等特殊字符。笔者同一使用使用预处理指令定义。墙体定义如下:

#define WALL L'□'

坐标设定如下:上:(0,0)到(56,0) 下:(0,26)到(56,26) 左:(0,1)到(0,25) 右:(56,1)到(56,25)


创建地图函数CreateMap:

void CreatMap()
{
	int i = 0;
	SetPos(0,0);
	//宽字符一块墙体站X两个单位大小
	//上(0,0)-(56, 0)
	for(i = 0;i < 58; i = i + 2)
	{
		wprintf(L"%c", WALL);
	}
	SetPos(0, 26);
	//下(0,26)-(56, 26)
	for (i = 0; i < 58; i = i + 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
    //x是0,y从0开始增⻓
	for (i = 0; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//x是56,y从0开始增⻓
	for (i = 0; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}
2.1.3.蛇身创建及初始化蛇身
a.蛇身节点

在游戏运行的过程中,蛇每次吃⼀个食物,蛇的身体就会变长⼀节,如果我们使用链表存储蛇的信息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇身节点在地图上的坐标就行,然后根据坐标移动光标打印对应节点就行,同样的对于食物来说也是这样,我们只要创建结构体存储对应的位置信息就行了,所以蛇节点(食物节点)结构如下:

蛇身打印的宽字符:

 #define BODY L'●'
b.整条蛇及相关信息的维护

x,y就是对应一节身体的位置。这里我们还创建指向一节身体的指针方便调用。同时,在本项目中与蛇有关的信息还有诸如蛇的状态(撞墙、吃到自己、按ESC结束)、移动速度、蛇前进的方向、得分、食物分数、等等,如我们分开存放这些信息,就会显得非常杂乱、不利于我们后期游戏的维护,因此我们这里运用面向对象的思想,创建一个结构体,将这些与蛇有关的信息集中存放在一起。结构体如下:

typedef struct Snake
{
	pSnakeNode _Psnake;//维护整条蛇的指针,指向代表蛇头的节点
	pSnakeNode _Pfood;//维护⻝物的指针
	enum DIRECTION _Dir;//蛇头的方向默认是向右
	enum STATE _State;//游戏状态
	int _Score;//当前获得分数
	int _FoodScore;//默认每个⻝物10分
	int _SleepTime;//每走一步休眠时间
}snake,*psnake;
c.蛇的速度

需要特别说明的是,蛇的速度的实现跟读者想象的不同,如果将游戏运行,我们发现蛇是走一步顿一步的,其实我们并不是让蛇真的有个速度动起来,我们只是让玩家觉得它动起来了,蛇身移动的前进是依靠打印字符与空格实现的,一系列的连续动作最终也不过是通过不断循环实现的,如我们直接运行程序,那么依赖打印实现的蛇身体的移动,必然是迅速均衡的,因此如果想实现蛇存在移动速度的感觉,我们必须使得程序运行速度上存在差异,因此在这里,我们使用休眠Sleep指令,改变程序的运行,赋予蛇速度,休眠时间短,速度就快,休眠时间长,速度就慢

d.蛇的方向

方向仅用来判断,用枚举存放不同方向就行

//方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};
e.蛇的状态(游戏状态)

状态仅用来判断,用枚举存放不同状态就行

//游戏状态
enum STATE
{
	OK,//正常运行
	END_NORMAL,//撞墙
	KILL_BY_WALL,//咬到自己
	KILL_BY_SELF,//正常结束
};
d.初始化

蛇最开始长度设为5节,每节对应链表的⼀个节点,蛇身的每⼀个节点都有自己的坐标。

创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,从一个固定的位置出发,访问节点存储的坐标位置信息,将蛇的每⼀节打印在屏幕上。

• 蛇的初始位置从 (24,5) 开始。

注意:因为墙体为宽字符,从(0,0)坐标开始打印,X轴方向上一个墙体为站两个单位长度,所以蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半出现在墙体中,另外⼀般在墙外的现象,坐标不好对齐。

再设置当前游戏的状态,蛇移动的速度,默认的方向,初始成绩,每个食物的分数。

• 游戏状态是:OK

• 蛇的移动速度:200毫秒

• 蛇的默认方向:RIGHT

• 初始成绩:0

• 每个食物的分数:10

初始化蛇⾝函数:InitSnake

void InitSnake(psnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	//创建蛇身节点,并初始化坐标
    //头插法:创建新的头插入原链表中,并将新节点作为新蛇头
	for(i = 0;i < 5;i++)
	{

		//创建蛇身的节点
			cur = (pSnakeNode)malloc(sizeof(SnakeNode));
			if (NULL == cur)
			{
				perror("InitSnake:malloc()");
				exit(1);
			}

			//设置坐标

			cur->next = NULL;
			cur->x = POS_X + i*2;
			cur->y = POS_Y;

			//头插法

			if (ps->_Psnake == NULL)
			{
				ps->_Psnake = cur;
			}
			else
			{
				cur->next = ps->_Psnake;
				ps->_Psnake = cur;
			}
	}

	//打印蛇的身体:改变光标位置,在对应光标位置打印身体字符

	cur = ps->_Psnake;
	while(cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%c",BODY);
		cur = cur->next;
	}

	//初始化贪吃蛇数据

	ps->_Dir = RIGHT;
	ps->_FoodScore = 10;
	ps->_Score = 0;
	ps->_SleepTime = 200;
	ps->_State = OK;
}

2.1.4.创建第⼀个食物

• 先随机生成食物的坐标:食物必须生成在墙内,且不能生成在蛇身上(循环遍历蛇身节点,根据坐标是否相等判断,因为,蛇头不可能吃到蛇头,只需从蛇头之后开始循环即可。),因此食物的节点坐标x必须在2~54内,y坐标必须在1~25之内(rand生成是按0~52,0~24在加上数来实现),同时这个食物坐标的生成必须是随机不定的,对此我们可以使用rand函数与srand函数。

• x坐标必须是2的倍数:如果食物的x坐标不是2的倍数,就会出现蛇一半碰到食物,能一半未碰到食物,但是这碰到的食物是宽字符比窄字符宽出的那部分,这部分是多打印的,我们蛇的节点并未存储相关坐标信息,因此不好判断坐标是否相等蛇是否吃到食物,因此食物X坐标必须是2的倍数

• 创建食物节点,打印食物

CreateFood:

void CreateFood(psnake ps)
{
	int x = 0;
	int y = 0;
again:

	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对⻬

	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_Psnake; // 获取指向蛇头的指针

	//⻝物不能和蛇身冲突

	while(cur)
	{
		if(cur->x == x && cur->y == y)
		{
			goto again;//goto语法,如果⻝物和蛇身冲突,则重新生成坐标
		}
		cur = cur->next;
	}

	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));//创建⻝物
	if (NULL == pfood)
	{
		perror("CreateFood:malloc()");
		exit(1);
	}
	pfood->x = x;//创键食物后,在维护贪吃蛇相关信息的结构体内更新游戏信息,方便维护管理
	pfood->y = y;
	SetPos(x, y);
	wprintf(L"%c", FOOD);
	ps->_Pfood = pfood;
}

2.2.游戏运行(GameRun)

void GameRun(psnake ps)
{
	do
	{
		//打印右侧帮助信息
		PrintHelpInfo(ps);
		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))//检测是否按过空格,按过则游戏暂停
		{
			pause();
		}
		else if (KEY_PRESS(VK_F3))//检测是否按过F3,按过则游戏加速
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;//游戏加速就是休眠时间变短
				ps->_FoodScore += 2;//游戏加速,每个食物能获得分数上涨
			}
		}
		else if (KEY_PRESS(VK_F4))//检测是否按过F4,按过则游戏减速
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;//游戏减速就是休眠时间变长
				ps->_FoodScore -= 2;//游戏减速,每个食物能获得分数下降
			}
			if (ps->_SleepTime == 350)//蛇能减到的最低速度
			{
				ps->_FoodScore = 1;
			}
		}
		else if (KEY_PRESS(VK_ESCAPE))//检测是否按过ESC键,按过则游戏正常结束
		{
			ps->_State = END_NORMAL;//更新蛇的状态
			break;
		}
		//蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);//蛇移动一步
	} while (ps->_State == OK);//判断蛇的状态是否正常进行游戏
}
2.2.1帮助信息及得分打印

游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64, 15)

PrintHelpInfo:
void PrintHelpInfo(psnake ps)
{
	//打印提示信息

	SetPos(64, 10);
	printf("得分: %2d", ps->_Score);
	SetPos(64, 11);
	printf("每个食物得分: %2d", ps->_FoodScore);
	SetPos(64, 14);
	printf("游戏说明:");
	SetPos(64,15);
	printf("不能穿墙,不能咬到自己");
	SetPos(64, 16);
	printf("用↑ . ↓ . ← . → 分别控制蛇的移动");
	SetPos(64, 17);
	printf("F3为加速,F4为减速");
	SetPos(64, 18);
	printf("ESC:退出游戏。Space:暂停游戏。");
}
2.2.2.方向、状态、速度判断(按键检测)

根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。

如果游戏继续,就是根据前面介绍虚拟键值相关信息检测按键情况,确定蛇下一步的方向,或者是否加速减速,是否暂停或者退出游戏。

需要的虚拟按键的罗列:

• 上:VK_UP

• 下:VK_DOWN

• 左:VK_LEFT

• 右:VK_RIGHT

• 空格:VK_SPACE

• ESC:VK_ESCAPE

• F3:VK_F3

• F4:VK_F4

确定了蛇的方向和速度,蛇就可以移动了。

		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))//检测是否按过空格,按过则游戏暂停
		{
			pause();
		}
		else if (KEY_PRESS(VK_F3))//检测是否按过F3,按过则游戏加速
		{
			if(ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;//游戏加速就是休眠时间变短
				ps->_FoodScore += 2;//游戏加速,每个食物能获得分数上涨
			}
		}
		else if (KEY_PRESS(VK_F4))//检测是否按过F4,按过则游戏减速
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;//游戏减速就是休眠时间变长
				ps->_FoodScore -= 2;//游戏减速,每个食物能获得分数下降
			}
			if(ps->_SleepTime == 350)//蛇能减到的最低速度
			{
				ps->_FoodScore = 1;
			}
		}
		else if (KEY_PRESS(VK_ESCAPE))//检测是否按过ESC键,按过则游戏正常结束
		{
			ps->_State = END_NORMAL;//更新蛇的状态
			break;
		}
a.Pause
void pause()//按一次空格,游戏暂停
{
	while(1)//通过死循环的休眠程序达到暂停的效果
	{
		Sleep(200);
		if(KEY_PRESS(VK_SPACE))
		{
			break;///再按一次空格,游戏继续
		}
	}
}
b.KEY_PRESS

检测按键状态,笔者封装了⼀个宏
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 1 ? 1 : 0)
2.2.3蛇身移动(SnakeMove)
2.2.3.1.SnakeMove:

蛇身移动是通过先创建下一个节点,根据移动方向和蛇头的坐标,获得下⼀个步位置上节点的坐标,之后看下一个位置是否是食物(NextIsFood),是食物就做吃食处理(EatFood),将食物的节点直接作为新蛇头,并销毁之前创建新节点,如果不是食物则做前进⼀步的处理(NoFood),将创建节点作为新蛇头插入原链表,循环遍历蛇身,打印蛇身字符,但是在原来蛇尾处应该打印两个空字符消除屏幕上原蛇尾,否则,蛇身会越来越长,最后再释放原来的尾节点。

除此之外,蛇身移动后,还应该判断此次移动是否会造成撞墙(KillByWall)或者撞上蛇身(KillBySelf),从而影响游戏的状态,导致游戏结束。

void SnakeMove(psnake ps)
{
	//创建下⼀个节点
	pSnakeNode cur = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (NULL == cur)
	{
		perror("InitSnake:malloc()");
		exit(1);
	}
	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
	switch(ps->_Dir)
	{
	case UP:
		cur->x = ps->_Psnake->x;
		cur->y = ps->_Psnake->y - 1;
		break;
	case DOWN:
		cur->x = ps->_Psnake->x;
		cur->y = ps->_Psnake->y + 1;
		break;
	case LEFT:
		cur->x = ps->_Psnake->x - 2;
		cur->y = ps->_Psnake->y;
		break;
	case RIGHT:
		cur->x = ps->_Psnake->x + 2;
		cur->y = ps->_Psnake->y;
		break;
	}

	//如果下一个位置就是⻝物
	if(NextIsFood(ps,cur))
	{
		EatFood(ps,cur);
	}
	else//如果没有⻝物
	{
		NotFood(ps, cur);
	}
	KillByWall(ps);
	KillBySelf(ps);
}
a.NextIsFood
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(psnake ps,pSnakeNode pn)
{
	return (ps->_Pfood->x == pn->x && ps->_Pfood->y == pn->y);
}
b.EatFood
//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(psnake ps, pSnakeNode pn)
{
	//头插法:将属于食物的节点插入到原链表中,作为新的蛇头
	ps->_Pfood->next = ps->_Psnake;
	ps->_Psnake = ps->_Pfood;
	pSnakeNode cur = ps->_Psnake;

	//打印蛇
	while(cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%c",BODY);
		cur = cur->next;
	}
	ps->_Score += ps->_FoodScore;
	free(pn);
    pn = NULL;
	CreateFood(ps);//吃掉食物后再创建一个食物
}
c.NotFood

将下⼀个节点头插入蛇的身体,并将之前蛇身最后⼀个节点打印为空格,释放掉蛇身的最后⼀个节点

需要注意的是:释放最后⼀个结点后,还得将指向在最后⼀个结点的指针改为NULL,保证蛇尾打印可以正常结束,不会越界访问。

//pSnakeNode pn 是下一节点的地址
//pSnake ps 维护蛇的指针
void NotFood(psnake ps, pSnakeNode pn)
{
	//头插法:将创建的节点插入到原链表中,作为新蛇头   
	pn->next = ps->_Psnake;
	ps->_Psnake = pn;
	pSnakeNode cur = ps->_Psnake;
	//打印蛇
	while(cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}

	//最后⼀个位置打印空格,然后释放节点,将最后节点的next置为NULL否则会越界访问
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}
d.KillByWall

判断蛇头的坐标是否和墙的坐标冲突

//pSnake ps 维护蛇的指针
void KillByWall(psnake ps)
{
	if(ps->_Psnake->x == 0 || ps->_Psnake->x == 56 || ps->_Psnake->y == 0 || ps->_Psnake->y == 25)
	{
		ps->_State = KILL_BY_WALL;
	}
}
e.KillBySelf

判断蛇头的坐标是否和蛇身体的坐标冲突

//pSnake ps 维护蛇的指针
void KillBySelf(psnake ps)
{
	pSnakeNode cur = ps->_Psnake->next;
	while(cur)
	{
		if((ps->_Psnake->x ==  cur->x) && (ps->_Psnake->y == cur->y))
		{
			ps->_State = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

2.3游戏结束


游戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇身节点。

void GameEnd(psnake ps)
{
	pSnakeNode cur = ps->_Psnake;
	SetPos(24, 12);
	switch(ps->_State)
	{
	case END_NORMAL:
		printf("您退出了游戏,游戏结束!");
		break;
	case KILL_BY_SELF:
		printf("您吃到了自己,游戏结束!");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙,游戏结束!");
		break;
	}
	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}

}

2.4重来一局相关问题

正常一场游戏不管怎么样,一运行必然是直接开始游戏,因此笔者这里采用do..while结构来实现这个效果,而游戏结束后程序应该询问玩家是否再玩一局,这时以输入Y/或N为重开的标志,考虑到玩家可能出于手误或其他原因出现;”大小写不分“、”多个输入“等多种问题,scanf使用不安全,因此笔者这里采用getchar()来接受玩家的输入值,同时判断重来的条件也大小写都可以。

但是需要注意的是getchar函数是自动的从输入缓冲区读取字符的,因此假设当上一句我们输入Y

,我们是会回车键确定输入的,但是回车键按过的同时,输入缓冲区是会存入一个'\n',因此下一句getchar()是不会等玩家输入的,它会直接读取‘\n’,同时又相当于按了回车键确定输入,因此游戏会出现直接结束的Bug.

对此我们通过while (getchar() != '\n');循环读取来清空缓冲区的输入,就可避免这个问题,同属由于getchar一次只读一个字符,我们还解决了一次输入多个字符可能造成的问题。

最后我们在循环开头加上清屏指令,清楚可能的残留。

void test()
{
	int ch = 0;
	srand((unsigned int)time(NULL));//通过rand产生随机坐标来产生随机食物的效果
	do
	{
		system("cls");
		snake sk = { 0 };
		GameStart(&sk);
		GameRun(&sk);
		GameEnd(&sk);
		SetPos(25, 15);//使询问是否重来的句子位置美观一点
		printf("您是否再来一局?(Y/N):");
		ch = getchar();
		while (getchar() != '\n');
	} while (ch == 'Y'|| ch == 'y');
}

六:参考代码

1.snake.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#define WALL L'□'
#define BODY L'■'
#define FOOD L'★'

//蛇的初始位置

#define POS_X 24
#define POS_Y 5
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 1 ? 1 : 0)
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<locale.h>
#include<stdbool.h>
#include<time.h>

//方向
enum DIRECTION
{
	UP = 1,
	DOWN,
	LEFT,
	RIGHT
};

//游戏状态
enum STATE
{
	OK,//正常运行
	END_NORMAL,//撞墙
	KILL_BY_WALL,//咬到自己
	KILL_BY_SELF,//正常结束
};


//蛇身节点

typedef struct SnakeNode
{
	short x;
	short y;
	struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

typedef struct Snake
{
	pSnakeNode _Psnake;//维护整条蛇的指针
	pSnakeNode _Pfood;//维护⻝物的指针
	enum DIRECTION _Dir;//蛇头的方向默认是向右
	enum STATE _State;//游戏状态
	int _Score;//当前获得分数
	int _FoodScore;//默认每个⻝物10分
	int _SleepTime;//每走一步休眠时间
}snake,*psnake;

//设置光标的坐标

void SetPos(short x, short y);

//游戏初始化

void GameStart(psnake ps);

//欢迎界面

void WelcomeToGame();

//创建地图

void CreatMap();

//创建⻝物

void CreateFood(psnake ps);

//初始化蛇

void InitSnake(psnake ps);

//游戏运行过程

void GameRun(psnake ps);

//打印帮助信息

void PrintHelpInfo(psnake ps);

//蛇的移动

void SnakeMove(psnake ps);

//下一个节点是⻝物

int NextIsFood(psnake ps, pSnakeNode pn);

//吃⻝物

void EatFood(psnake ps, pSnakeNode pn);

//不吃⻝物

void NotFood(psnake ps, pSnakeNode pn);

//暂停响应

void pause();

//撞墙检测

void KillByWall(psnake ps);

//撞自身检测

void KillBySelf(psnake ps);

//游戏结束

void GameEnd(psnake ps);

2.snake.c

#include"snake.h"

//设置光标的坐标

void SetPos(short x, short y)
{
	COORD pos = { x,y };
	HANDLE houtput = NULL;

	//获取标准输出的句柄(用来标识不同设备的数值)

	houtput = GetStdHandle(STD_OUTPUT_HANDLE);

	//设置标准输出上光标的位置为pos

	SetConsoleCursorPosition(houtput, pos);

}

void GameStart(psnake ps)
{
	WelcomeToGame();//设置窗口名称大小、隐藏光标,打印欢迎界面
	CreatMap();//打印欢迎界面
	InitSnake(ps);//初始化蛇及其相关信息
	CreateFood(ps);//创造第一个食物

}

void GameRun(psnake ps)
{
	do
	{
		//打印右侧帮助信息
		PrintHelpInfo(ps);
		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))//检测是否按过空格,按过则游戏暂停
		{
			pause();
		}
		else if (KEY_PRESS(VK_F3))//检测是否按过F3,按过则游戏加速
		{
			if (ps->_SleepTime >= 50)
			{
				ps->_SleepTime -= 30;//游戏加速就是休眠时间变短
				ps->_FoodScore += 2;//游戏加速,每个食物能获得分数上涨
			}
		}
		else if (KEY_PRESS(VK_F4))//检测是否按过F4,按过则游戏减速
		{
			if (ps->_SleepTime < 350)
			{
				ps->_SleepTime += 30;//游戏减速就是休眠时间变长
				ps->_FoodScore -= 2;//游戏减速,每个食物能获得分数下降
			}
			if (ps->_SleepTime == 350)//蛇能减到的最低速度
			{
				ps->_FoodScore = 1;
			}
		}
		else if (KEY_PRESS(VK_ESCAPE))//检测是否按过ESC键,按过则游戏正常结束
		{
			ps->_State = END_NORMAL;//更新蛇的状态
			break;
		}
		//蛇每次⼀定之间要休眠的时间,时间短,蛇移动速度就快
		Sleep(ps->_SleepTime);
		SnakeMove(ps);//蛇移动一步
	} while (ps->_State == OK);//判断蛇的状态是否正常进行游戏
}

void GameEnd(psnake ps)
{
	pSnakeNode cur = ps->_Psnake;
	SetPos(24, 12);
	switch(ps->_State)
	{
	case END_NORMAL:
		printf("您退出了游戏,游戏结束!");
		break;
	case KILL_BY_SELF:
		printf("您吃到了自己,游戏结束!");
		break;
	case KILL_BY_WALL:
		printf("您撞到了墙,游戏结束!");
		break;
	}
	//释放蛇身的节点
	while (cur)
	{
		pSnakeNode next = cur->next;
		free(cur);
		cur = next;
	}

}

void SnakeMove(psnake ps)
{
	//创建下⼀个节点
	pSnakeNode cur = (pSnakeNode)malloc(sizeof(SnakeNode));
	if (NULL == cur)
	{
		perror("InitSnake:malloc()");
		exit(1);
	}
	//确定下一个节点的坐标,下一个节点的坐标根据,蛇头的坐标和方向确定
	switch(ps->_Dir)
	{
	case UP:
		cur->x = ps->_Psnake->x;
		cur->y = ps->_Psnake->y - 1;
		break;
	case DOWN:
		cur->x = ps->_Psnake->x;
		cur->y = ps->_Psnake->y + 1;
		break;
	case LEFT:
		cur->x = ps->_Psnake->x - 2;
		cur->y = ps->_Psnake->y;
		break;
	case RIGHT:
		cur->x = ps->_Psnake->x + 2;
		cur->y = ps->_Psnake->y;
		break;
	}

	//如果下一个位置就是⻝物
	if(NextIsFood(ps,cur))
	{
		EatFood(ps,cur);
	}
	else//如果没有⻝物
	{
		NotFood(ps, cur);
	}
	KillByWall(ps);
	KillBySelf(ps);
}

//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
int NextIsFood(psnake ps,pSnakeNode pn)
{
	return (ps->_Pfood->x == pn->x && ps->_Pfood->y == pn->y);
}

//pSnakeNode pn 是下一个节点的地址
//pSnake ps 维护蛇的指针
void EatFood(psnake ps, pSnakeNode pn)
{
	//头插法:将属于食物的节点插入到原链表中,作为新的蛇头
	ps->_Pfood->next = ps->_Psnake;
	ps->_Psnake = ps->_Pfood;
	pSnakeNode cur = ps->_Psnake;

	//打印蛇
	while(cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%c",BODY);
		cur = cur->next;
	}
	ps->_Score += ps->_FoodScore;
	free(pn);
    pn = NULL;
	CreateFood(ps);//吃掉食物后再创建一个食物
}

//pSnakeNode pn 是下一节点的地址
//pSnake ps 维护蛇的指针
void NotFood(psnake ps, pSnakeNode pn)
{
	//头插法:将创建的节点插入到原链表中,作为新蛇头   
	pn->next = ps->_Psnake;
	ps->_Psnake = pn;
	pSnakeNode cur = ps->_Psnake;
	//打印蛇
	while(cur->next->next)
	{
		SetPos(cur->x, cur->y);
		wprintf(L"%c", BODY);
		cur = cur->next;
	}

	//最后⼀个位置打印空格,然后释放节点
	SetPos(cur->next->x, cur->next->y);
	printf("  ");
	free(cur->next);
	cur->next = NULL;
}

void WelcomeToGame()
{
	//设置控制台窗口的大小,30行,100列
    //mode为DOS命令
	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);//设置控制台光标状态
	//SetPos调整光标位置
	SetPos(40, 15);
	printf("欢迎来到贪吃蛇小游戏!");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
	SetPos(25, 12);
	printf("用↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n");
	SetPos(25, 13);
	printf("加速将能得到更高的分数。\n");
	SetPos(40, 25);//让按任意键继续的出现的位置好看点
	system("pause");
	system("cls");
}

void CreatMap()
{
	int i = 0;
	SetPos(0,0);
	//宽字符一块墙体站X两个单位大小
	//上(0,0)-(56, 0)
	for(i = 0;i < 58; i = i + 2)
	{
		wprintf(L"%c", WALL);
	}
	SetPos(0, 26);
	//下(0,26)-(56, 26)
	for (i = 0; i < 58; i = i + 2)
	{
		wprintf(L"%c", WALL);
	}
	//左
    //x是0,y从0开始增⻓
	for (i = 0; i < 26; i++)
	{
		SetPos(0, i);
		wprintf(L"%c", WALL);
	}
	//x是56,y从0开始增⻓
	for (i = 0; i < 26; i++)
	{
		SetPos(56, i);
		wprintf(L"%c", WALL);
	}
}

void InitSnake(psnake ps)
{
	pSnakeNode cur = NULL;
	int i = 0;
	//创建蛇身节点,并初始化坐标
    //头插法:创建新的头插入原链表中,并将新节点作为新蛇头
	for(i = 0;i < 5;i++)
	{

		//创建蛇身的节点
			cur = (pSnakeNode)malloc(sizeof(SnakeNode));
			if (NULL == cur)
			{
				perror("InitSnake:malloc()");
				exit(1);
			}

			//设置坐标

			cur->next = NULL;
			cur->x = POS_X + i*2;
			cur->y = POS_Y;

			//头插法

			if (ps->_Psnake == NULL)
			{
				ps->_Psnake = cur;
			}
			else
			{
				cur->next = ps->_Psnake;
				ps->_Psnake = cur;
			}
	}

	//打印蛇的身体:改变光标位置,在对应光标位置打印身体字符

	cur = ps->_Psnake;
	while(cur)
	{
		SetPos(cur->x,cur->y);
		wprintf(L"%c",BODY);
		cur = cur->next;
	}

	//初始化贪吃蛇数据

	ps->_Dir = RIGHT;
	ps->_FoodScore = 10;
	ps->_Score = 0;
	ps->_SleepTime = 200;
	ps->_State = OK;
}

void CreateFood(psnake ps)
{
	int x = 0;
	int y = 0;
again:

	//产生的x坐标应该是2的倍数,这样才可能和蛇头坐标对⻬

	do
	{
		x = rand() % 53 + 2;
		y = rand() % 25 + 1;
	} while (x % 2 != 0);

	pSnakeNode cur = ps->_Psnake; // 获取指向蛇头的指针

	//⻝物不能和蛇身冲突

	while(cur)
	{
		if(cur->x == x && cur->y == y)
		{
			goto again;
		}
		cur = cur->next;
	}

	pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));//创建⻝物
	if (NULL == pfood)
	{
		perror("CreateFood:malloc()");
		exit(1);
	}
	pfood->x = x;
	pfood->y = y;
	SetPos(x, y);
	wprintf(L"%c", FOOD);
	ps->_Pfood = pfood;
}

void PrintHelpInfo(psnake ps)
{
	//打印提示信息

	SetPos(64, 10);
	printf("得分: %2d", ps->_Score);
	SetPos(64, 11);
	printf("每个食物得分: %2d", ps->_FoodScore);
	SetPos(64, 14);
	printf("游戏说明:");
	SetPos(64,15);
	printf("不能穿墙,不能咬到自己");
	SetPos(64, 16);
	printf("用↑ . ↓ . ← . → 分别控制蛇的移动");
	SetPos(64, 17);
	printf("F3为加速,F4为减速");
	SetPos(64, 18);
	printf("ESC:退出游戏。Space:暂停游戏。");
}

void pause()//按一次空格,游戏暂停
{
	while(1)//通过死循环的休眠程序达到暂停的效果
	{
		Sleep(200);
		if(KEY_PRESS(VK_SPACE))
		{
			break;///再按一次空格,游戏继续
		}
	}
}
//pSnake ps 维护蛇的指针
void KillByWall(psnake ps)
{
	if(ps->_Psnake->x == 0 || ps->_Psnake->x == 56 || ps->_Psnake->y == 0 || ps->_Psnake->y == 25)
	{
		ps->_State = KILL_BY_WALL;
	}
}

//pSnake ps 维护蛇的指针
void KillBySelf(psnake ps)
{
	pSnakeNode cur = ps->_Psnake->next;
	while(cur)
	{
		if((ps->_Psnake->x ==  cur->x) && (ps->_Psnake->y == cur->y))
		{
			ps->_State = KILL_BY_SELF;
			return;
		}
		cur = cur->next;
	}
}

3.test.c

#include"snake.h"


void test()
{
	int ch = 0;
	srand((unsigned int)time(NULL));//通过rand产生随机坐标来产生随机食物的效果
	do
	{
		system("cls");
		snake sk = { 0 };
		GameStart(&sk);
		GameRun(&sk);
		GameEnd(&sk);
		SetPos(25, 15);
		printf("您是否再来一局?(Y/N):");
		ch = getchar();
		while (getchar() != '\n');
	} while (ch == 'Y'|| ch == 'y');
}

int main()
{
	setlocale(LC_ALL, "");//修改当前地区为本地模式,为了支持中文宽字符及特殊字符的打印
	test();//测试逻辑
	SetPos(0, 27);//将光标移动至地图下方,不会破坏地图
	return 0;
}

  ​​​​​​​七​​​​​​​:附录参考:汉字字符集编码查询;中文字符集编码:GB2312、BIG5、GBK、GB18030、Unicode
 

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

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

相关文章

用队列实现栈——leetcode刷题

题目的要求是用两个队列实现栈&#xff0c;首先我们要考虑队列的特点&#xff1a;先入先出&#xff0c;栈的特点&#xff1a;后入先出&#xff0c;所以我们的目标就是如何让先入栈的成员后出栈&#xff0c;后入栈的成员先出栈。 因为有两个队列&#xff0c;于是我们可以这样想&…

支付宝支付流程

第一步前端&#xff1a;点击去结算&#xff0c;前端将商品的信息传递给后端&#xff0c;后端返回一个商品的订单号给到前端&#xff0c;前端将商品的订单号进行存储。 对应的前端代码&#xff1a;然后再跳转到支付页面 // 第一步 点击去结算 然后生成一个订单号 // 将选中的商…

SQL 基础 | AVG 函数的用法

在SQL中&#xff0c;AVG()是一个聚合函数&#xff0c;用来计算某个列中所有值的平均值。 它通常与GROUP BY子句一起使用&#xff0c;以便对分组后的数据进行平均值计算。 AVG()函数在需要了解数据集中某个数值列的中心趋势时非常有用。 以下是AVG()函数的一些常见用法&#xff…

DETR类型检测网络实验2---优化测试

补全reference_point Anchor-DETR提出用预定义的参考点生成query_pos; DBA-DETR提出预定义参考信息由(x,y)增至(x,y,w,h) 那么在3D检测任务中是否可以把预定义参考信息补全为(x,y,z,l,w,h,sint,cost),而query_pos都是使用xy两个维度(因为是bev网络). (这种方法在Sparse-DETR中…

CMakeLists.txt语法规则:部分常用命令说明一

一. 简介 前一篇文章简单介绍了CMakeLists.txt 简单的语法。文章如下&#xff1a; CMakeLists.txt 简单的语法介绍-CSDN博客 接下来对 CMakeLists.txt语法规则进行具体的学习。本文具体学习 CMakeLists.txt语法规则中常用的命令。 二. CMakeLists.txt语法规则&#xff1a;…

探索LLM在广告领域的应用——大语言模型的新商业模式和新个性化广告的潜力

概述 在网络搜索引擎的领域中&#xff0c;广告不仅仅是一个补充元素&#xff0c;而是构成了数字体验的核心部分。随着互联网经济的蓬勃发展&#xff0c;广告市场的规模已经达到了数万亿美元&#xff0c;并且还在持续扩张。广告的经济价值不断上升&#xff0c;它已经成为支撑大…

C++初阶之模板初阶

一、泛型编程 如何实现一个通用的交换函数呢&#xff1f; void Swap(int& left, int& right) {int temp left;left right;right temp; } void Swap(double& left, double& right) {double temp left;left right;right temp; } void Swap(char& left,…

spring boot3多模块项目工程搭建-上(团队开发模板)

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 目录 写在前面 多模块结构优缺点 模块介绍 Common 模块&#xff1a; API 模块&#xff1a; Web 模块&#xff1a; Service 模块&#xff1a; DAO 模块&#xff1a; 搭建步骤 1.创建 父…

JavaWeb_请求响应_简单参数实体参数

一、SpringBoot方式接收携带简单参数的请求 简单参数&#xff1a;参数名与形参变量名相同&#xff0c;定义形参即可接收参数。并且在接收过程中&#xff0c;会进行自动的类型转换。 启动应用程序后&#xff0c;在postman中进行测试&#xff1a; 请求成功&#xff0c;响应回了O…

Flask教程3:jinja2模板引擎

文章目录 模板的导入与使用 模板的导入与使用 Flask通过render_template来实现模板的渲染&#xff0c;要使用这个方法&#xff0c;我们需要导入from flask import rander_template&#xff0c;模板中注释需放在{# #}中 模板的第一个参数为指定的模板文件名称&#xff0c;如自定…

微信小程序生成二维码加密(CryptoJS4.0加密PHP8.0解密)AES方式加密

1、小程序创建 crypto-js.js和crypto.js两个文件&#xff08;点击文件即可&#xff09; 2、小程序js页面引入 var crypto require(../../utils/crypto.js);//注意路径是否正确3、使用 let data {id: that.data.id,name: dx}console.log(JSON.stringify(data))console.log(&…

【论文阅读】Learning Texture Transformer Network for Image Super-Resolution

Learning Texture Transformer Network for Image Super-Resolution 论文地址Abstract1. 简介2.相关工作2.1单图像超分辨率2.2 Reference-based Image Super-Resolution 3. 方法3.1. Texture TransformerLearnable Texture Extractor 可学习的纹理提取器。Relevance Embedding.…

【八股】AQS,ReentrantLock实现原理

AQS 概念 AQS 的全称是 AbstractQueuedSynchronized &#xff08;抽象队列同步器&#xff09;&#xff0c;在java.util.concurrent.locks包下面。 AQS是一个抽象类&#xff0c;主要用来构建锁和同步器&#xff0c;比如ReentrantLock, Semaphore, CountDownLatch&#xff0c;里…

Leetcode—163. 缺失的区间【简单】Plus

2024每日刷题&#xff08;126&#xff09; Leetcode—163. 缺失的区间 实现代码 class Solution { public:vector<vector<int>> findMissingRanges(vector<int>& nums, int lower, int upper) {int n nums.size();vector<vector<int>> an…

基于遗传优化模糊控制器的水箱水位控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 模糊控制器原理 4.2 遗传算法原理 4.3 遗传优化模糊控制器的工作流程 5.完整工程文件 1.课题概述 基于遗传优化模糊控制器的水箱水位控制系统simulink建模与仿真。对比模糊控制器和基于遗传优化的…

Python基础详解一

一&#xff0c;print打印 print("hello word") print(hello word) 双引号和单引号都可以 二&#xff0c;数据类型 Python中常用的有6种值的类型 输出类型信息 print(type(11)) print(type("22")) print(type(22.2)) <class int> <class str&…

飞书API(7):MySQL 入库通用版本

一、引入 在上一篇介绍了如何使用 pandas 处理飞书接口返回的数据&#xff0c;并将处理好的数据入库。最终的代码拓展性太差&#xff0c;本篇来探讨下如何使得上一篇的最终代码拓展性更好&#xff01;为什么上一篇的代码拓展性太差呢&#xff1f;我总结了几点&#xff1a; 列…

深入理解 Java 并发:AbstractQueuedSynchronizer 源码分析

序言 在多线程编程中&#xff0c;同步机制是保障线程安全和协调线程之间操作顺序的重要手段。AQS 作为 Java 中同步机制的基础框架&#xff0c;为开发者提供了一个灵活且高效的同步工具。本文将通过对 AQS 源码的分析&#xff0c;解读 AQS 的核心实现原理&#xff0c;并深入探…

wireshark的安装使用及相关UDP、TCP、 ARP

初步了解&#xff1a; 进入wireshark后如图&#xff1a; 从图中可以看到很多网络连接在操作的时候我们需要监测哪些 我们可以直接在本地的运行框中输入ipconfig来查看 如图&#xff1a; 从以上图片中我们可以清楚地看到哪些网络连接已经连接的我们只需要按需监测他们即可 但…

【LinuxC语言】信号集与sigprocmask

文章目录 前言一、信号集1.1 操作信号集相关的函数1.2 信号屏蔽字1.3 sigprocmask1.4 示例代码 总结 前言 在Linux C编程中&#xff0c;信号是一种重要的进程间通信机制&#xff0c;用于通知进程发生了特定的事件。然而&#xff0c;程序在执行过程中可能会收到各种各样的信号&…
最新文章