C++简单小项目之贪吃蛇

学了C++之后不免有点手痒想学着做点小东西,于是我就找到了一个外国老哥做的超简单编写小游戏,甚至不需要图形引擎等渲染图形,直接在屏幕缓冲区就可以玩

链接我放在这里:

5.贪吃蛇-从头开始编程_哔哩哔哩_bilibiliicon-default.png?t=N7T8https://www.bilibili.com/video/BV16h4y1d766?p=5&vd_source=bb22981532b659f84ba71d756abd5fe2

他的代码链接我也放在这里:

snakeCPP/snakeCPP.cpp at master · jpromanonet/snakeCPP (github.com)

当然我这里是对老哥的视频做的贪吃蛇的代码的一些详解,因为我自己看他的视频的时候看的云里雾里的,他并没有将所有细节都讲清楚。我想着或许有人也会像我一样,所以写了这篇博客,之前我并没有很多写博客的经历,我尽量把他的代码嚼碎了喂给大家,让大家尽可能懂他在讲什么,以及代码的运行逻辑。

那接下来我们就"dive right into the code"!

首先要做的是创建一个屏幕缓冲区,目的是让我们的游戏画面能够在黑匣子中显示,这需要以下几行代码:

int main()
{
	wchar_t* screen = new wchar_t[nScreenHeight * nScreenWidth];
	for (int i = 0; i < (nScreenWidth) * (nScreenHeight); i++) screen[i] = L'@';
	HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
	SetConsoleActiveScreenBuffer(hConsole);
	DWORD dwBytesWritten = 0;

	while (1)
		WriteConsoleOutputCharacter(hConsole, screen, (nScreenWidth) * (nScreenHeight), { 0,0 }, &dwBytesWritten);



}

我觉得不少人开头看到这个头就已经开始痛了 ,我也不例外,但是别担心,我会一句一句的将他们的作用解释。

我们先来了解下屏幕缓冲区是什么东西吧。

这是来自Learn Microsoft.com的解释,相信也讲的足够直观了。

我们接下来将代码逐行分解:

	wchar_t* screen = new wchar_t[nScreenWidth * nScreenHeight];

     通过这行代码,我们创建了一个可以存储游戏屏幕信息的缓冲区。我们可以通过修改 screen 指针来更新游戏屏幕上的字符,然后将这些字符渲染到屏幕上,以实现游戏的显示效果。
    通常,在游戏编程中,我们会在内存中创建一个虚拟的游戏屏幕,然后根据游戏逻辑不断更新它,最后将其显示在实际的屏幕上。

    也就是我们在内存中申请了一块大小为nScreenWidth * nScreenHeight的 数组并用名为screen的宽字符型指针指向他以便我们日后对他进行操作。

for (int i = 0; i < (nScreenWidth) * (nScreenHeight); i++) screen[i] = L'@';

这行代码相信不用我多解释了,就是将缓冲区所有的位置填上'@'号,当然,因为我们用的是宽字符,所以我们需要在字符前面加一个L。如果想要了解更多关于宽字符的信息不妨百度一下即可。

HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);

这行代码就是这其中最复杂的一行代码了,但是不要紧,我们庖丁解牛慢慢来看。

 这行代码功能为创建一个新的控制台屏幕缓冲区,返回一个HANDLE句柄以供以后操作使用。

关于句柄的使用我用简单的语言稍微讲解下:

         系统为每个进程在内存中分配一定的区域,用来存放各个句柄,即一个个32位无符号整型值(32位操作系统中)。
         每个32位无符号整型值相当于一个指针指向内存中的另一个区域(我们不妨称之为区域A)。而区域A中存放的正是对象在内存中的地址。
         当对象在内存中的位置发生变化时,区域A的值被更新,变为当前时刻对象在内存中的地址,而在这个过程中,区域A的位置以及对应句柄的值是不发生变化的。
        句柄主要有以下几个特点:

         1.句柄是对象生成时系统指定的,属性是只读的,程序员不能修改句柄。
         2.不同的系统中,句柄的大小(字节数)是不同的,可以使用sizeof()来计算句柄的大小。
         3.通过句柄,程序员只能调用系统提供的服务(即API调用),不能像使用指针那样,做其它的事。
 

一句话概括,可以把他想想成指针的指针,如果想了解关于句柄更详细的内容,也可以参考以下这篇博客:

句柄详解,什么是句柄?句柄有什么用?__Stack_的博客-CSDN博客

然后我们再来讲讲他里面的五个参数:

         CreateConsoleScreenBuffer()-->Window API中的一个函数,用与在控制台上写入字符。这个函数用于操作控制台屏幕缓冲区,并将字符写入指定位置。

        1.dwDesiredAccess-->对控制台屏幕缓冲区的访问。 有关访问权限的列表,参阅 "控制台缓冲区安全性和访问权限"。在此为   "GENERIC_READ | GENERIC_WRITE"        这是一个标志,指定了对控制台屏幕缓冲区的访问权限。

         "GENERIC_READ"     表示可以读取缓冲区内容,"GENERIC_WRITE"     表示可以写入缓冲区内容,因此这个参数表示你可以读取和写入缓冲区。
         使用 "|" 是因为为"GENERIC_READ"=0x80000000,"GENERIC_WRITE"=0x40000000可以在此按照按位或的操作同时置1,如果为"&"则会被同时置0

        2.dwShareMode-->此参数可以为零,表示无法共享缓冲区,也可以是以下一个或多个值:
                a.FILE_SHARE_READ=0x00000001    可以在控制台屏幕缓冲区上执行其他打开操作,以便进行读取访问。
                b.FILE_SHARE_WRITE=0x00000002    可以在主机屏幕缓冲区上执行其他打开操作,以便进行写入访问。
         这里写0,表示不共享缓冲区

        3.*lpSecurityAttributes-->指向 SECURITY_ATTRIBUTES 结构的指针,该结构确定是否可由子进程继承返回的句柄。
            如果 lpSecurityAttributes 为 NULL,则无法继承句柄。 结构的 lpSecurityDescriptor 成员指定新控制台屏幕缓冲区的安全描述符。 
            如果 lpSecurityAttributes 为 NULL,则控制台屏幕缓冲区将获取默认的安全描述符。 主机屏幕缓冲区的默认安全描述符中的 ACL 来自创建者的主要令牌或模拟令牌
            此处设置为NULL表示不指定安全属性
 

        4.dwFlags-->要创建的控制台屏幕缓冲区的类型。 唯一支持的屏幕缓冲区类型是 CONSOLE_TEXTMODE_BUFFER。
        5.lpScreenBufferData-->保留参数,理应为NULL 

        最终,CreateConsoleScreenBuffer 函数将返回一个 HANDLE 类型的句柄 hConsole,
        我们可以使用这个句柄来执行与控制台屏幕缓冲区相关的操作,比如写入内容或将缓冲区设置为活动屏幕缓冲区,从而更新显示。

        通常,我们可以在这个缓冲区中绘制你想要在控制台上显示的文本、图形或其他信息,然后使用 WriteConsoleOutputCharacter 或其他相关函数来将内容写入缓冲区, 并使用 SetConsoleActiveScreenBuffer 来更新控制台屏幕上显示的内容。

 

SetConsoleActiveScreenBuffer(hConsole);

        SetConsoleActiveScreenBuffer() 用于设置指定的控制台屏幕缓冲区为活动屏幕缓冲区。我们此处指定hConsole创建的屏幕缓冲区为活动屏幕缓冲区。
        这意味着设置为活动的屏幕缓冲区将用于在控制台上显示内容,而不是默认的标准输出(通常是 CMD 窗口)。
 

DWORD dwBytesWritten = 0;

    DWORD 是 Windows API 中的一种数据类型,表示双字(Double Word),通常是 32 位无符号整数。
    在这里,DWORD dwBytesWritten 是一个用于输出的参数,用于存储一个操作写入的字节数。
    在 Windows API 中,DWORD 常用于表示字节数、句柄、错误码等。
 

然后就是while循环中的最后一句代码,我们还是一个一个参数解释他的作用:

WriteConsoleOutputCharacter(hConsole, screen, (nScreenWidth) * (nScreenHeight), { 0,0 }, &dwBytesWritten);

              HANDLE  hConsoleOutput,      // 控制台屏幕缓冲区的句柄。控制台屏幕缓冲区的句柄。 此句柄必须具有 GENERIC_WRITE 访问权限。hConsole具有此权限
             LPCTSTR lpCharacter,         // 要写入的字符数据。为screen上的数据。
             DWORD   nLength,            // 要写入的字符数。将屏幕缓冲区选中位置重新写入
             COORD   dwWriteCoord,       // 写入操作的起始坐标,{0,0}从头开始
             LPDWORD lpNumberOfCharsWritten // 用于接收成功写入的字符数

然后我们运行上面的代码:

可以看到整个屏幕缓冲区被填满了5,我们再稍微修改下参数,继续加深我们对代码的理解:

#include <iostream>
#include<Windows.h>
#include<vector>
#include<string>
int nScreenWidth = 120;
int nScreenHeight = 40;

int main()
{
	std::string str = "Hello,World";
	size_t n = str.size();
	wchar_t* screen = new wchar_t[nScreenHeight * nScreenWidth];
	for (int i = 0; i < (nScreenWidth) * (nScreenHeight); i++) screen[i] = str[i%11];
	HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
	SetConsoleActiveScreenBuffer(hConsole);
	DWORD dwBytesWritten = 0;
	int i = 1000000;
	while (i)
	{
		WriteConsoleOutputCharacter(hConsole, screen, (nScreenWidth) * (nScreenHeight), { 0,0 }, &dwBytesWritten);
		i--;
	}

}

 我们将Hello,World作为样本输入,让他重复打印该字符串:

效果立竿见影!

如果我们让 writeconsoleoutputcharacter()中的显示字符长度改为12:

WriteConsoleOutputCharacter(hConsole, screen, 12, { 0,0 }, &dwBytesWritten);

可以看到屏幕缓冲区便只打印了一个helloworld

那到这里可以有人又会有疑问了:为什么要用while循环将writeconsoleoutputcharacter()套住呢?

答案其实是显而易见的:我们只占用了一瞬间的屏幕缓冲区,这行write....执行完之后会继续执行后面的代码,屏幕缓冲区上的数据也就被刷掉了,所以我们需要用while循环不断让屏幕缓冲区显示我们想要的内容!

 顺带提一下,我们上面使用的函数都是WINDOWS API的函数,所以需要包含头文件Windows.h

介绍完了最超纲的部分,我们就正式进入游戏设计的部分了。

我们先设计食物的部分:

int nFoodX = rand() % nScreenWidth;
int nFoodY = (rand() % (nScreenHeight - 3)) + 3;

然后是贪吃蛇本身,我们打算用链表来实现蛇的身体:我们需要引入#include<list>

struct sSnakeSegment {
	int x;
	int y;
};
list<sSnakeSegment> snake = { {60,15},{61,15},{62,15},{63,15},{64,15} };

通过预设坐标设置小蛇初始长度和位置。

再下来是关于小蛇的位置标识,我们用一个变量来表示小蛇的方向,以此同时我们还需要一个表示小蛇上一个方向的变量,具体原因我们后面会讲到:

		int nSnakeDirection = rand()%4;
		int nSnakeDirectionOld = nSnakeDirection;

接下来就是对小蛇具体四个方向的定义,同样的我们依旧需要每个方向定义两个变量。

我们定义nSnakeDirection=0是上,1是右,2是下,3是左

bool bKeyLeft = false, bKeyRight = false, bKeyLeftOld = false, bKeyRightOld = false;
bool bKeyUp = false, bKeyDown = false, bKeyUpOld = false, bKeyDownOld = false;

我们还需要一个表示小蛇存活与否和小蛇获得的分数的变量:

bool bDead = false;
int nScore = 0;

做完这些我们就可以开始写对于小蛇方向的判断了,当然在此之前我们需要输入一个方向让电脑知道我们想要小蛇走的方向:

bKeyRight = (0x8000 & GetAsyncKeyState((unsigned char)('\x27'))) != 0;
bKeyLeft = (0x8000 & GetAsyncKeyState((unsigned char)('\x25'))) != 0;
bKeyDown = (0x8000 & GetAsyncKeyState((unsigned char)('\x28'))) != 0;
bKeyUp = (0x8000 & GetAsyncKeyState((unsigned char)('\x26'))) != 0;

这里出现了一个新的函数GetAsyncKeyState(),同样是一个WINDOWS API的函数:

               GetAsyncKeyState 函数:这是一个 Windows API 函数,用于获取指定键的状态。
                它接受一个键的虚拟键码作为参数,并返回一个 SHORT 类型(2Bytes,16bits)的值,表示键的状态。
                如果指定的键被按下,则返回值的最高位(最左边的位)将被设置为1,否则为0。

                (unsigned char)('\x27') 和 (unsigned char)('\x25'):这些代码将十六进制字符转换为无符号字符,表示右箭头键和左箭头键的虚拟键码。
                \x27 表示右箭头键的虚拟键码,\x25 表示左箭头键的虚拟键码。
                此处使用unsigned char 进行强制类型转换是为了保证代码的健壮性和可移植性,因为在不同的环境中虚拟键码可能有所不同

                (0x8000 & GetAsyncKeyState(...)) != 0:这是一个位运算,用于检查键的状态。
                GetAsyncKeyState 返回的状态值与 0x8000(二进制的最高位为1)进行按位与运算,以检查最高位是否被设置为1。
                如果最高位为1,表示键被按下,那么条件为真,bKeyRight 或 bKeyLeft 将被设置为 true。

                这里将等号右边的式子看成判断,就不难看出返回的是bool值,并将其赋给bKeyRight和bKeyLeft。
 

 获得了输入,我们就可以开始对小蛇的方向逻辑进行判断了:

				if (bKeyRight && !bKeyRightOld)
				{
					if (nSnakeDirectionOld != 3)
				    	nSnakeDirection = 1;
				}

				if (bKeyLeft && !bKeyLeftOld)
				{
					if (nSnakeDirectionOld != 1)
						nSnakeDirection = 3;
				}

				if (bKeyUp && !bKeyUpOld)
				{
					if (nSnakeDirectionOld != 2)
						nSnakeDirection = 0;
				}

				if (bKeyDown && !bKeyDownOld)
				{
					if (nSnakeDirectionOld != 0)
						nSnakeDirection = 2;
				}
				nSnakeDirectionOld = nSnakeDirection;
				bKeyRightOld = bKeyRight;
				bKeyLeftOld = bKeyLeft;
				bKeyUpOld = bKeyUp;
				bKeyDownOld = bKeyDown;

写完这串代码我们就可以来解释为什么对一个方向要定义两个变量了。

对于nSnakeDirection,nSnakeDirectionOld的目的是为了记录小蛇上一个方位是什么,禁止用户的反方向输入,避免小蛇暴死,比如说小蛇正在向右移动,这样就可以在用户按下左方向键的时候禁止此次输入,避免小蛇直接180度转弯创死自己。

而对于每个方向键的Old,我们这样理解:

哦当然,我们这一段代码是要放在while(!bDead),也就是小蛇没有死的循环中持续执行的。

试想一下如果我们的外层if判断没有Old的判断会是什么情况:

        (在校对时笔者发现笔者改良后的代码去掉那四个方向的Old也是完全没有问题的,至于为什么大数的代码中要呢是因为他的代码不是这样的,这个我会放在最后用大叔的源代码讲解一下)

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

对蛇的方位进行判断完成后,我们便要对蛇的运动来设置一下了

			switch (nSnakeDirection)
			{			
			case 0:// Up
				snake.push_front({ snake.front().x, snake.front().y - 1 });
				break;				
			case 1:// Right
				snake.push_front({ snake.front().x + 1, snake.front().y });
				break;				
			case 2:// Down
				snake.push_front({ snake.front().x, snake.front().y + 1 });
				break;				
			case 3:// Left
				snake.push_front({ snake.front().x - 1, snake.front().y });
				break;
			}

值得注意的是,这里使用的是.front()方法而非.begin()方法

因为.front()返回首元素的引用,.begin()返回指向首元素的迭代器    

 这段Switch case代码控制了蛇头的移动,当蛇头面向不同方位时.向snake链表的头插入一个新元素, 同时结合后面的pop_back()方法弹出蛇尾就可以在视觉上形成蛇前进的效果。
 

小蛇的运动写完了,我们接下来写小蛇的碰撞。小蛇的碰撞检测主要来源于三个方面:

1.小蛇与边界的检测

2.小蛇与食物的检测

3.小蛇与自身的检测

if (snake.front().x == nFoodX && snake.front().y == nFoodY) 
{
	nScore++;
	BonusCount++;
    while (screen[nFoodY * nScreenWidth + nFoodX] != L' ') 
        {
            nFoodX = rand() % nScreenWidth;
            nFoodY = (rand() % (nScreenHeight - 3)) + 3;
        }
}

食物碰撞检测,如果食物和头坐标相同说明蛇吃到了食物。

  

      这段代码生成新的食物坐标代码,用于避免食物生成在蛇的体内,可以如此理解:

当食物的坐标不是空格时,进入循环重新生成随机食物坐标,直至生成的位置是空格不满足循环,此时确定食物坐标退出循环
 

吃完东西相应的提醒也需要增长:

for (int i = 0; i < 5; i++)
    snake.push_back({ snake.back().x, snake.back().y });

然后是边界检测:

			if (snake.front().x < 0 || snake.front().x >= nScreenWidth)
				bDead = true;
			if (snake.front().y < 3 || snake.front().y >= nScreenHeight)
				bDead = true;

非常简单易懂,在此不再赘述。

for (list<sSnakeSegment>::iterator i = snake.begin(); i != snake.end(); i++)
    if (i != snake.begin() && i->x == snake.front().x && i->y == snake.front().y)
		bDead = true;

最后再来popback一下弹出蛇尾


			//通过将尾部弹出实现蛇身向前移动
			snake.pop_back();

小蛇的运动逻辑就差不多写完了。我们来绘制一下小蛇和边界:

for (int i = 0; i < nScreenWidth * nScreenHeight; i++) screen[i] = L' ';
for (int i = 0; i < nScreenWidth; i++) 
{
	screen[i] = L' ';//第一行
	screen[2 * nScreenWidth + i] = L'=';//第三行
}
wsprintf(&screen[nScreenWidth + 5], L"Snake Game demo by Sulyvahn   Score: %d", nScore);

            wsprintf 函数的作用是将格式化的文本消息填充到指定位置的字符数组中,以便后续在屏幕上显示。
            在这个例子中,它将游戏标题和分数格式化后写入到屏幕上的特定位置,用于显示游戏的状态信息。
            这是游戏界面更新的一部分,用于向玩家显示相关信息。
 

然后我们打印一下小蛇,给他画个骨骼

for (auto s : snake)
	screen[s.y * nScreenWidth + s.x] = bDead ? L'+' : L'O';
screen[snake.front().y * nScreenWidth + snake.front().x] = bDead ? L'X' : L'@';

 画一下食物:

screen[nFoodY * nScreenWidth + nFoodX] = L'$';

那基本上我们所有的逻辑都已经写完了,当游戏结束时,我们要提示用户按下空格再玩一遍

if (bDead)
	wsprintf(&screen[3 * nScreenWidth + 40], L"    按下空格键重新游玩    ");

当没有检测到输入空格时循环等待用户输入

		while ((0x8000 & GetAsyncKeyState((unsigned char)('\x20'))) == 0);

将他们套上循环:

这就是最终代码

#include <iostream>
#include <list>
#include <thread>
using namespace std;
#include <Windows.h>
#include<vector>
// Defining the console screen size
int nScreenWidth = 120;
int nScreenHeight = 30;

struct sSnakeSegment {
	int x;
	int y;
};

struct sBonusFood
{
	int x;
	int y;
};
std::vector<sBonusFood> BonusFood(4);


int main()
{
	wchar_t* screen = new wchar_t[nScreenWidth * nScreenHeight];
	for (int i = 0; i < nScreenWidth * nScreenHeight; i++) screen[i] = L' ';
	HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
	SetConsoleActiveScreenBuffer(hConsole);
	DWORD dwBytesWritten = 0;
	while (1) {
		list<sSnakeSegment> snake = { {60,15},{61,15},{62,15},{63,15},{64,15} };
		int nFoodX = rand() % nScreenWidth;
		int nFoodY = (rand() % (nScreenHeight - 3)) + 3;
		int nScore = 0;
		int nSnakeDirection = rand()%4;
		int nSnakeDirectionOld = nSnakeDirection;
		bool bDead = false;
		bool bKeyLeft = false, bKeyRight = false, bKeyLeftOld = false, bKeyRightOld = false;
		bool bKeyUp = false, bKeyDown = false, bKeyUpOld = false, bKeyDownOld = false;
		while (!bDead) {
			auto t1 = chrono::system_clock::now();//记录当前时刻的系统时钟
			while ((chrono::system_clock::now() - t1) < ((nSnakeDirection % 2 == 1) ? 120ms : 200ms))
			{ 
				bKeyRight = (0x8000 & GetAsyncKeyState((unsigned char)('\x27'))) != 0;
				bKeyLeft = (0x8000 & GetAsyncKeyState((unsigned char)('\x25'))) != 0;
				bKeyDown = (0x8000 & GetAsyncKeyState((unsigned char)('\x28'))) != 0;
				bKeyUp = (0x8000 & GetAsyncKeyState((unsigned char)('\x26'))) != 0;

				if (bKeyRight && !bKeyRightOld)
				{
					if (nSnakeDirectionOld != 3)
					{
						nSnakeDirection = 1;
					}

				}

				if (bKeyLeft && !bKeyLeftOld)
				{
					if (nSnakeDirectionOld != 1)
					{
						nSnakeDirection = 3;
					}

				}

				if (bKeyUp && !bKeyUpOld)
				{
					if (nSnakeDirectionOld != 2)
					{
						nSnakeDirection = 0;
					}
				}

				if (bKeyDown && !bKeyDownOld)
				{
					if (nSnakeDirectionOld != 0)
					{
						nSnakeDirection = 2;
					}
				}
				nSnakeDirectionOld = nSnakeDirection;
				bKeyRightOld = bKeyRight;
				bKeyLeftOld = bKeyLeft;
				bKeyUpOld = bKeyUp;
				bKeyDownOld = bKeyDown;
			}

			switch (nSnakeDirection)
			{			
			case 0:// Up
				snake.push_front({ snake.front().x, snake.front().y - 1 });
				break;				
			case 1:// Right
				snake.push_front({ snake.front().x + 1, snake.front().y });
				break;				
			case 2:// Down
				snake.push_front({ snake.front().x, snake.front().y + 1 });
				break;				
			case 3:// Left
				snake.push_front({ snake.front().x - 1, snake.front().y });
				break;
			}

			if (snake.front().x == nFoodX && snake.front().y == nFoodY)
			{
				nScore++;
					while (screen[nFoodY * nScreenWidth + nFoodX] != L' ') 
					{
						nFoodX = rand() % nScreenWidth;
						nFoodY = (rand() % (nScreenHeight - 3)) + 3;
					}
				}

				for (int i = 0; i < 5; i++)
					snake.push_back({ snake.back().x, snake.back().y });
			}

			if (snake.front().x < 0 || snake.front().x >= nScreenWidth)
				bDead = true;
			if (snake.front().y < 3 || snake.front().y >= nScreenHeight)
				bDead = true;

			for (list<sSnakeSegment>::iterator i = snake.begin(); i != snake.end(); i++)
				if (i != snake.begin() && i->x == snake.front().x && i->y == snake.front().y)
					bDead = true;

			snake.pop_back();


			for (int i = 0; i < nScreenWidth * nScreenHeight; i++) screen[i] = L' ';

			for (int i = 0; i < nScreenWidth; i++) {
				screen[i] = L' ';//第一行
				screen[2 * nScreenWidth + i] = L'=';//第三行
			}
			wsprintf(&screen[nScreenWidth + 5], L"Snake Game demo by Sulyvahn   Score: %d", nScore);//第二行

			for (auto s : snake)
				screen[s.y * nScreenWidth + s.x] = bDead ? L'+' : L'O';


			screen[snake.front().y * nScreenWidth + snake.front().x] = bDead ? L'X' : L'@';

			screen[nFoodY * nScreenWidth + nFoodX] = L'$';

			if (bDead)
				wsprintf(&screen[3 * nScreenWidth + 40], L"    按下空格键重新游玩    ");


			WriteConsoleOutputCharacter(hConsole, screen, nScreenWidth * nScreenHeight, { 0,0 }, &dwBytesWritten);

		}


		while ((0x8000 & GetAsyncKeyState((unsigned char)('\x20'))) == 0);
	}

	return 0;
}

回答最开始留下的问题:四个方向的Old有什么意义:

        这是大叔的源代码:

if (bKeyRight && !bKeyRightOld)
				{
					nSnakeDirection++;
					if (nSnakeDirection == 4) nSnakeDirection = 0;
				}

				if (bKeyLeft && !bKeyLeftOld)
				{
					nSnakeDirection--;
					if (nSnakeDirection == -1) nSnakeDirection = 3;
				}

				bKeyRightOld = bKeyRight;
				bKeyLeftOld = bKeyLeft;
switch (nSnakeDirection)
			{
				// Up
			case 0:
				snake.push_front({ snake.front().x, snake.front().y - 1 });
				break;
				// Right
			case 1:
				snake.push_front({ snake.front().x + 1, snake.front().y });
				break;
				// Down
			case 2:
				snake.push_front({ snake.front().x, snake.front().y + 1 });
				break;
				// Left
			case 3:
				snake.push_front({ snake.front().x - 1, snake.front().y });
				break;
			}

可以看到大叔的代码其实只定义了两个方向:相对于小蛇前进方向的左右方向,这是不太符合我们的操作习惯的,会带来这个问题:如果小蛇是向下移动的,我们想让小蛇向我们的左边前进,我们会下意识按左方向键,但是实际上这是相对小蛇的左边,因此小蛇实际会往我们 期望的反方向跑......这是我们不希望看到的......

有点扯远了,按照大叔的Switch case写法,如果不加Old来限制,我们按下右方向键时,bKeyRight一直等于true,nsnakedirection就会一直加1。在switch case中小蛇会一直往自己的右手边走,然后直接把自己卷起来,结果就是小蛇直接自杀了。

所以大叔才需要Old来限制这样发生。

Ending :不知不觉写了一万三千多字了,这是一个小项目而已,接下来我也会继续根据我的思路给他优化,丰富他的功能,如果有机会的话我还会继续传上来分享我的优化丰富过程。:)

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值