第四章 游戏的显示(以贪吃蛇为例)

双缓冲显示

本章将对游戏的刷新显示方法进行介绍

重难点:游戏的双缓冲显示;彩色的双缓冲显示。

“双缓冲显示”习题

1.为什么会闪屏?

2.一个窗口可以有几个屏幕缓冲区?

3.如何将变量的数值写入到屏幕缓冲区的显示字符数组中?sprintg()函数的作用是什么?

4.请简述什么事双缓冲显示。

5.双缓冲显示通常是先将被显示的字符存入缓冲区对应的二维数组中,然后再2由二维数组转存到屏幕缓冲区。在这种由显示字符数组直接转存缓冲区的双缓冲显示的模式下,如何实现多种字体颜色的显示?

回顾,之前是在draw()函数里通过system("cls")来进行已有画面的清除,然后再进行游戏新画面的绘制,由于system函数的显映速度低于游戏刷新的要求,所以画面出现闪烁现象,自行脑部在调用system函数的时候是不是短暂的吧控制权交给了系统,系统往往不止游戏这么一个程序在跑,所以也就是优先权的问题,为了游戏画面闪烁的问题,游戏开发中通常采用双缓冲技术或局部刷新技术(自行百度搜索了解)。

代码新增双缓冲需要用到的变量

创建缓冲区所对应的数组,这里把数组设置的大一点,避免出现数据溢出的情况

舞台边缘的墙壁是有厚度的,对两个缓冲区进行初始化

把两个缓冲区里的光标进行隐藏

不破坏原来的draw函数,重新复制一份命名为draw2,把原来直接输出显示的内容迁移到screenDate数组中,就是把内容先存在这个数组中,然后再通过双缓冲的方式进行交替显示

添加双缓冲显示的函数Show_doubelBuffer()

在主函数里对显示函数进行替换

运行报错,针对sprintf函数,根据提示改为sprintf_s;或者百度对问题进行回答,根据百度搜索选择在最开头增加一段宏代码的方案

虽然能成功运行,但是左端墙体消失,检查发现某个地方少了个else if

切换回之前的draw函数,显示看看效果会发现游戏在运行速度上的差异,于是在主函数中增加sleep函数,让游戏速度慢下来

对比两种显示方式会发现双缓冲模式的游戏中的循环执行明显回快一点,基于OpenGL开发的游戏通常就采用双缓冲的显示模式,篇幅有限,不对console下的双缓冲技术深挖

一个问题:为什么双缓冲模式下的运行效率看起来比单缓冲的效率高,深层次的原因是什么?

#define _CRT_SECURE_NO_DEPRECATE
#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;

HANDLE hOutput, hOutBuf;
COORD coord = { 0,0 };
DWORD bytes = 0;
bool BufferSwapFlag = false;

bool gameOver; //判断游戏是否结束
const int width = 20;
const int height = 20;
char ScreenData[width + 5][height + 5];//这里把数组设置的大一点,避免出现数据溢出的现象
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;

void Initial()
{
	hOutBuf = CreateConsoleScreenBuffer(
		GENERIC_WRITE,
		FILE_SHARE_WRITE,
		NULL,
		CONSOLE_TEXTMODE_BUFFER,
		NULL
		);
	hOutput = CreateConsoleScreenBuffer(
		GENERIC_WRITE,
		FILE_SHARE_WRITE,
		NULL,
		CONSOLE_TEXTMODE_BUFFER,
		NULL
	);
	//隐藏两个缓冲区的光标
	CONSOLE_CURSOR_INFO cci;
	cci.bVisible = 0;
	cci.dwSize = 1;
	SetConsoleCursorInfo(hOutput, &cci);
	SetConsoleCursorInfo(hOutBuf, &cci);

	gameOver = false;
	dir = STOP;
	x = width / 2;
	y = height / 2;
	fruitX = rand() % width;
	fruitY = rand() % height;
	score = 0;
}
void Draw()
{
	system("cls");
	HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
	int textColor = 0X86;
	SetConsoleTextAttribute(h, textColor);
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	cout << endl;
	for (int i = 0; i < height; i++)
	{
		for (int j = 0; j < width; j++)
		{
			if (j == 0)
				cout << "#";
			if (i == fruitY && j == fruitX)
			{
				textColor = 0X84;
				SetConsoleTextAttribute(h, textColor);
				cout << "F";
			}
			else if (i == y && j == x)
			{
				textColor = 0X8a;
				SetConsoleTextAttribute(h, textColor);
				cout << "0";
			}
			else
			{
				bool flagPrint = false;
				for (int k=0 ; k < nTail; k++)
				{
					if ((tailX[k] == j) && tailY[k] == i)
					{
						cout << "o";
						flagPrint = true;
					}
				}
				textColor = 0X86;
				SetConsoleTextAttribute(h, textColor);
				if(!flagPrint)
				    cout << " ";
			}
			if (j == width - 1)
				cout << "#";
		}
		cout << endl;
	}
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	cout << endl;
	cout << "游戏得分" << score << endl;
}

void Draw2()
{
	int i, j;
	int currenLine = 0;

	for (j = 0; j< width + 2; j++)
		ScreenData[currenLine][j]= '#';
	currenLine++;

	for ( i = 0; i < height; i++)
	{
		for (j = 0; j < width; j++)
		{
			if (j == 0)
				ScreenData[currenLine + i][j] = '#';
			else if (i == fruitY && j == fruitX)
			{
				ScreenData[currenLine + i][j] = 'F';
			}
			/*if (i == fruitY && j == fruitX)
			{
				ScreenData[currenLine + i][j] = 'F';
			}*/
			else if (i == y && j == x)
			{
				ScreenData[currenLine + i][j] = '0';
			}
			else
			{
				bool flagPrint = false;
				for (int k = 0; k < nTail; k++)
				{
					if ((tailX[k] == j) && tailY[k] == i)
					{
						ScreenData[currenLine + i][j] = 'o';
						flagPrint = true;
					}
				}
				if (!flagPrint)
					ScreenData[currenLine + i][j] = ' ';
			}
			if (j == width - 1)
				ScreenData[currenLine + i][j] = '#';
		}
	}
	for ( j = 0; j < width + 2; j++)
		ScreenData[currenLine + i][j] = '#';
	currenLine++;
	sprintf(ScreenData[currenLine + i], "游戏得分:%d", score);
}

void Show_doubleBuffer()
{
	HANDLE hBuf;
	WORD textColor;
	int i,j;
	Draw2();

	if (BufferSwapFlag == false)
	{
		BufferSwapFlag == true;
		hBuf = hOutBuf;
	}

	else
	{
		BufferSwapFlag == false;
		hBuf = hOutput;
	}

	//对ScreenData数组的内容进行上色,并将属性传到输出缓冲区hBuf
	for (i=0;i<height+5;i++)
	{
		coord.Y = i;
		for (j=0;j<width+5;j++)
		{
			coord.X = j;
			if (ScreenData[i][j] == '0')
				textColor = 0X03;
			else if (ScreenData[i][j] == 'F')
				textColor = 0X04;
			else if (ScreenData[i][j] == 'O')
				textColor = 0X0a;
			else
				textColor = 0X06;
			WriteConsoleOutputAttribute(hBuf, &textColor, 1, coord, &bytes);
		}
		coord.X = 0;
		WriteConsoleOutputCharacterA(hBuf, ScreenData[i], width, coord, &bytes);
	}

   /*if (BufferSwapFlag == false)
	{
		BufferSwapFlag = true;
		for (i=0;i<height+5;i++)
		{
			coord.Y = i;
			WriteConsoleOutputCharacterA(hOutBuf, ScreenData[i], width, coord, &bytes);
		}
		SetConsoleActiveScreenBuffer(hOutBuf);
	}
	else
	{
		BufferSwapFlag = true;
		for (i = 0; i < height + 5; i++)
		{
			coord.Y = i;
			WriteConsoleOutputCharacterA(hOutput, ScreenData[i], width, coord, &bytes);
		}
		SetConsoleActiveScreenBuffer(hOutput);
	}*/
	SetConsoleActiveScreenBuffer(hBuf);
}
void Input()
{
	if (_kbhit())
	{
		switch (_getch())
		{
		case 'a':
			dir = LEFT;
			break;
		case 'd':
			dir = RIGHT;
			break;
		case 'w':
			dir = UP;
			break;
		case 's':
			dir = DOWN;
			break;
		case 'x':
			gameOver = true;
			break;
		default:
			break;
		}
	}
}
void Logic()
{
	int prevX = tailX[0];
	int prevY = tailY[0];
	int prev2X, prev2Y;
	tailX[0] = x;
	tailY[0] = y;//把新增的蛇身坐标存放在0位

	for (int i = 1; i < nTail; i++)
	{
		prev2X = tailX[i];
		prev2Y = tailY[i];
		tailX[i] = prevX;
		tailY[i] = prevY;
		prevX = prev2X;
		prevY = prev2Y;
	}
	switch (dir)
	{
		//case STOP:
			//break;    为什么这两行不要
	case LEFT:
		x--;
		break;
	case RIGHT:
		x++;
		break;
	case UP:     //向上居然是y--
		y--;
		break;
	case DOWN:
		y++;
		break;
	default:
		break;
	}
	//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
	//{
	//	gameOver = true;
	//}
	for (int i=0; i < nTail;i++)
	{
		if (tailX[i] == x && tailY[i] == y)
			gameOver = true;
	}

	if ( x >= width) x = 0; else if (x < 0)x = width - 1;
	if (y >= height) y = 0; else if (y < 0)y = height - 1;


	if (x == fruitX && y == fruitY)   //蛇头遇到果子
	{
		score += 10;
		fruitX = rand() % width;
		fruitY = rand() % height;
		nTail++;
	}
}
int main()
{
	Initial();
	while (!gameOver)
	{
		//Draw();
		Show_doubleBuffer();
		Input();
		Logic();
		Sleep(240); // 100毫秒,可以根据需要调整速度
	}
	//system("pause");
	return 0;
}

局部更新

重难点:游戏的局部绘制更新;屏幕的坐标定位。

“局部更新”习题

1.局部绘制的操作思路是什么?

2.局部重绘需要先擦除原来的游戏对象,再绘制新的游戏对象。但从游戏循环的角度观察,新绘制的对象在下次循环的时候,其实也是很快就被擦除的。请问为什么局部重绘的方式可以解决游戏画面闪烁的问题?

3.上一章节《贪吃蛇》游戏绘制模块的cout输出,多输出一个空格符或少输出一个空格符都会影响到舞台右墙的排列。同样是cout输出,为什么局部绘制方案中的cout输出空格符却是覆盖舞台原位置的字符?

4.贪吃蛇游戏中,如何实现水果的闪烁,并使得闪烁的频率是亮4下、灭1下?

5.游戏开发在进行框架搭建的时候,轻易不去变更未涉及的模块的内容。当进行贪吃蛇的局部重绘的时候,如果遇到新增的蛇身图形,首先出现在游戏场景的左上角,再下一帧才会跟随在蛇身之后。请问原因是什么?应该如何更改?

局部更新首先要完成全局界面,即先要绘制游戏的场景,我们对于原来的draw()函数进行修改和调整DrawWap(),这个游戏中动的内容主要是蛇,所以需要添加蛇身擦除的函数eraseSnake(),然后是局部更新的DrawLocally()

这里的局部更新,我们首先要知道局部的位置,在技术上要求我们能够在游戏界面上实现定位,所以先介绍如何处理console界面的光标界面。这里使用函数SetConsoleCursorPosition(h, pos);设置控制台上的光标位置,x,y坐标都加一的原因是这里有两个坐标系,一个是console窗口的坐标,一个是游戏窗口的坐标,这里给出的x,y坐标是游戏场景坐标,游戏中墙壁的厚度是1,所以要加上一个偏移量,控制台的句柄这里需要全局变量

现在对Drawmap()函数进行修改(把draw改为drawmap)

原来的draw函数现在需要绘制游戏场景即墙壁,所以做这样的修改,setpos函数的调用其实是在告知程序四道墙的位置在屏幕的哪里,可以想想setpos函数为什么传进去-1的参数

把eraseSnake()和DrawLocally()注释掉运行后发现墙壁存在问题

#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;
bool gameOver; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;

HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);

void Initial()
{
	gameOver = false;
	dir = STOP;
	x = width / 2;
	y = height / 2;
	fruitX = rand() % width;
	fruitY = rand() % height;
	score = 0;
}
//光标位置
void setPos(int X, int Y)
{
	COORD pos;
	pos.X = X + 1;
	pos.Y= Y + 1;
	SetConsoleCursorPosition(h, pos);  //设置控制台上光标的位置
}
void DrawMap()
{
	system("cls");
	
	int textColor = 0X86;
	SetConsoleTextAttribute(h, textColor);
	setPos(-1, -1);//绘制顶部的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
\
	for (int i = 0; i < height; i++)
	{
		setPos(-1, i);//绘制左右的墙
		for (int j = 0; j < width; j++)
		{
			if (j == 0)
				cout << "#";
			else if (j == width - 1)
				cout << "#";
			else
				cout << " ";
		}
		cout << endl;
	}
setPos(-1, height);//绘制下方的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	cout << endl;
	cout << "游戏得分" << score << endl;
}
void Input()
{
	if (_kbhit())
	{
		switch (_getch())
		{
		case 'a':
			dir = LEFT;
			break;
		case 'd':
			dir = RIGHT;
			break;
		case 'w':
			dir = UP;
			break;
		case 's':
			dir = DOWN;
			break;
		case 'x':
			gameOver = true;
			break;
		default:
			break;
		}
	}
}
void Logic()
{
	int prevX = tailX[0];
	int prevY = tailY[0];
	int prev2X, prev2Y;
	tailX[0] = x;
	tailY[0] = y;//把新增的蛇身坐标存放在0位

	for (int i = 1; i < nTail; i++)
	{
		prev2X = tailX[i];
		prev2Y = tailY[i];
		tailX[i] = prevX;
		tailY[i] = prevY;
		prevX = prev2X;
		prevY = prev2Y;
	}
	switch (dir)
	{
		//case STOP:
			//break;    为什么这两行不要
	case LEFT:
		x--;
		break;
	case RIGHT:
		x++;
		break;
	case UP:     //向上居然是y--
		y--;
		break;
	case DOWN:
		y++;
		break;
	default:
		break;
	}
	//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
	//{
	//	gameOver = true;
	//}
	for (int i = 0; i < nTail; i++)
	{
		if (tailX[i] == x && tailY[i] == y)
			gameOver == true;
	}

	if (x >= width) x = 0; else if (x < 0)x = width - 1;
	if (y >= height) y = 0; else if (y < 0)y = height - 1;


	if (x == fruitX && y == fruitY)   //蛇头遇到果子
	{
		score += 10;
		fruitX = rand() % width;
		fruitY = rand() % height;
		nTail++;
	}
}
int main()
{
	Initial();
	DrawMap();
	while (!gameOver)
	{
		//Draw();
		Input();
		//eraseSnake();
		Logic();
		//DrawLocally();
	
	}
	system("pause");
	return 0;
}

 

发现墙壁是出了问题的 ,检查代码想一下为什么这样修改(与上方代码作对比)

一定要有一个概念,墙是在游戏的舞台里面还是舞台外面

下面是擦除蛇身,知道蛇身的位置,擦除起来就会简单,输出一个空格就能擦除了,擦除以后要能重新绘制出来

完善两个函数

运行后会发现出错了,没有出现蛇身

#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;
bool gameOver; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;

HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);

void Initial()
{
	gameOver = false;
	dir = STOP;
	x = width / 2;
	y = height / 2;
	fruitX = rand() % width;
	fruitY = rand() % height;
	score = 0;
}
//光标位置
void setPos(int X, int Y)
{
	COORD pos;
	pos.X = X + 1;
	pos.Y= Y + 1;
	SetConsoleCursorPosition(h, pos);  //设置控制台上光标的位置
}
void DrawMap()
{
	system("cls");
	
	int textColor = 0X06;
	SetConsoleTextAttribute(h, textColor);
	setPos(-1, -1);//绘制顶部的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
\
	for (int i = 0; i < height; i++)
	{
		setPos(-1, i);//绘制左右的墙
		for (int j = 0; j < width+2; j++)
		{
			if (j == 0)
				cout << "#";
			else if (j == width + 1)
				cout << "#";
			else
				cout << " ";
		}
		cout << endl;
	}
setPos(-1, height);//绘制下方的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	cout << endl;
	cout << "游戏得分" << score << endl;
}
void eraseSnake()
{
	for (int i=0; i < nTail; i++)
	{
		setPos(tailX[i], tailY[i]);
			cout << " ";
	}
}
void DrawLocally()
{
	setPos(fruitX, fruitY);
	SetConsoleTextAttribute(h, 0x04);
	cout << "F";

	for (int i=0;i<nTail;i++)
	{
		setPos(tailX[i], tailY[i]);
		if (i == 0)
		{
			SetConsoleTextAttribute(h, 0x09);
			cout << "0";
		}
		else
		{
			SetConsoleTextAttribute(h, 0x0a);
			cout << "o";
		}
	}
}
void Input()
{
	if (_kbhit())
	{
		switch (_getch())
		{
		case 'a':
			dir = LEFT;
			break;
		case 'd':
			dir = RIGHT;
			break;
		case 'w':
			dir = UP;
			break;
		case 's':
			dir = DOWN;
			break;
		case 'x':
			gameOver = true;
			break;
		default:
			break;
		}
	}
}
void Logic()
{
	int prevX = tailX[0];
	int prevY = tailY[0];
	int prev2X, prev2Y;
	tailX[0] = x;
	tailY[0] = y;//把新增的蛇身坐标存放在0位

	for (int i = 1; i < nTail; i++)
	{
		prev2X = tailX[i];
		prev2Y = tailY[i];
		tailX[i] = prevX;
		tailY[i] = prevY;
		prevX = prev2X;
		prevY = prev2Y;
	}
	switch (dir)
	{
		//case STOP:
			//break;    为什么这两行不要
	case LEFT:
		x--;
		break;
	case RIGHT:
		x++;
		break;
	case UP:     //向上居然是y--
		y--;
		break;
	case DOWN:
		y++;
		break;
	default:
		break;
	}
	//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
	//{
	//	gameOver = true;
	//}
	for (int i = 0; i < nTail; i++)
	{
		if (tailX[i] == x && tailY[i] == y)
			gameOver == true;
	}

	if (x >= width) x = 0; else if (x < 0)x = width - 1;
	if (y >= height) y = 0; else if (y < 0)y = height - 1;


	if (x == fruitX && y == fruitY)   //蛇头遇到果子
	{
		score += 10;
		fruitX = rand() % width;
		fruitY = rand() % height;
		nTail++;
	}
}
int main()
{
	Initial();
	DrawMap();
	while (!gameOver)
	{
		//Draw();
		Input();
		eraseSnake();
		Logic();
		DrawLocally();
	
	}
	system("pause");
	return 0;
}

设置一个断点检查看看什么问题

nTail为0,前面可能没有对它进行初始化 

似乎游戏直接结束(但我的代码并没有┭┮﹏┭┮)

#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;

bool gameOver; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail = 1;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;

HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);

void Initial()
{
	gameOver = false;
	dir = STOP;
	x = width / 2;
	y = height / 2;
	fruitX = rand() % width;
	fruitY = rand() % height;
	score = 0;
}
//光标位置
void setPos(int X, int Y)
{
	COORD pos;
	pos.X = X + 1;
	pos.Y= Y + 1;
	SetConsoleCursorPosition(h, pos);  //设置控制台上光标的位置
}
void DrawMap()
{
	system("cls");
	
	int textColor = 0X06;
	SetConsoleTextAttribute(h, textColor);

	setPos(-1, -1);//绘制顶部的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
\
	for (int i = 0; i < height; i++)
	{
		setPos(-1, i);//绘制左右的墙
		for (int j = 0; j < width+2; j++)
		{
			if (j == 0)
				cout << "#";
			else if (j == width + 1)
				cout << "#";
			else
				cout << " ";
		}
		cout << endl;
	}
    setPos(-1, height);//绘制下方的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	cout << endl;
	cout << "游戏得分" << score << endl;
}
void eraseSnake()
{
	for (int i=0; i < nTail; i++)
	{
		setPos(tailX[i], tailY[i]);
			cout << " ";
	}
}
void DrawLocally()
{
	setPos(fruitX, fruitY);
	SetConsoleTextAttribute(h, 0x04);
	cout << "F";

	for (int i=0;i<nTail;i++)
	{
		setPos(tailX[i], tailY[i]);
		if (i == 0)
		{
			SetConsoleTextAttribute(h, 0x09);
			cout << "0";
		}
		else
		{
			SetConsoleTextAttribute(h, 0x0a);
			cout << "o";
		}
	}
}
void Input()
{
	if (_kbhit())
	{
		switch (_getch())
		{
		case 'a':
			dir = LEFT;
			break;
		case 'd':
			dir = RIGHT;
			break;
		case 'w':
			dir = UP;
			break;
		case 's':
			dir = DOWN;
			break;
		case 'x':
			gameOver = true;
			break;
		default:
			break;
		}
	}
}
void Logic()
{
	int prevX = tailX[0];
	int prevY = tailY[0];
	int prev2X, prev2Y;
	tailX[0] = x;
	tailY[0] = y;//把新增的蛇身坐标存放在0位

	for (int i = 1; i < nTail; i++)
	{
		prev2X = tailX[i];
		prev2Y = tailY[i];
		tailX[i] = prevX;
		tailY[i] = prevY;
		prevX = prev2X;
		prevY = prev2Y;
	}
	switch (dir)
	{
		//case STOP:
			//break;    为什么这两行不要
	case LEFT:
		x--;
		break;
	case RIGHT:
		x++;
		break;
	case UP:     //向上居然是y--
		y--;
		break;
	case DOWN:
		y++;
		break;
	default:
		break;
	}
	//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
	//{
	//	gameOver = true;
	//}
	for (int i = 0; i < nTail; i++)
	{
		if (tailX[i] == x && tailY[i] == y)
			gameOver == true;
	}

	if (x >= width) x = 0; else if (x < 0)x = width - 1;
	if (y >= height) y = 0; else if (y < 0)y = height - 1;


	if (x == fruitX && y == fruitY)   //蛇头遇到果子
	{
		score += 10;
		fruitX = rand() % width;
		fruitY = rand() % height;
		nTail++;
	}
}
int main()
{
	Initial();
	DrawMap();
	while (!gameOver)
	{
		//Draw();
		Input();
		eraseSnake();
		Logic();
		DrawLocally();
	
	}
	system("pause");
	return 0;
}

我们检查一下gameover的条件,蛇头的坐标tailX[0]和Y【】坐标相等了

正确的条件应该是蛇头和蛇身相撞,把i的初值改为1

运行一下速度太快了光标还不停闪烁,主函数添加sleep(100);,到initial函数把光标闪烁处理一下

再运行看一下会发现 新增加的蛇身一开始出现在屏幕左上角之后才成为蛇身,看一下游戏的Logic()函数,应该是吃到苹果后蛇身链表就更新,所以把这块代码的位置调整一下再运行

如果想要苹果闪烁,增加一个状态控制的布尔变量fruitFlash进行初始化

运行后发现得分没变化

修改一下游戏得分的代码

#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;

bool gameOver,fruitFlash; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail = 1;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;

HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);

void Initial()
{
	SetConsoleTitleA("Console_贪吃蛇");
		COORD dSize = { 80,25 };
	SetConsoleScreenBufferSize(h, dSize);
	CONSOLE_CURSOR_INFO _cursor = { 1,false };
	gameOver = false;
	fruitFlash = false;
	dir = STOP;
	x = width / 2;
	y = height / 2;
	fruitX = rand() % width;
	fruitY = rand() % height;
	score = 0;
}
//光标位置
void setPos(int X, int Y)
{
	COORD pos;
	pos.X = X + 1;
	pos.Y= Y + 1;
	SetConsoleCursorPosition(h, pos);  //设置控制台上光标的位置
}
void DrawMap()
{
	system("cls");
	
	int textColor = 0X06;
	SetConsoleTextAttribute(h, textColor);

	setPos(-1, -1);//绘制顶部的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
\
	for (int i = 0; i < height; i++)
	{
		setPos(-1, i);//绘制左右的墙
		for (int j = 0; j < width+2; j++)
		{
			if (j == 0)
				cout << "#";
			else if (j == width + 1)
				cout << "#";
			else
				cout << " ";
		}
		cout << endl;
	}
    setPos(-1, height);//绘制下方的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	/*cout << endl;
	cout << "游戏得分" << score << endl;*/
}
void eraseSnake()
{
	for (int i=0; i < nTail; i++)
	{
		setPos(tailX[i], tailY[i]);
			cout << " ";
	}
}
void DrawLocally()
{
	if (!fruitFlash)
	{
		setPos(fruitX, fruitY);
		SetConsoleTextAttribute(h, 0x04);
		cout << "F";
		fruitFlash = true;
	}
	else
	{
		setPos(fruitX, fruitY);
		SetConsoleTextAttribute(h, 0x04);
		cout << " ";
		fruitFlash = false;
	}
	setPos(fruitX, fruitY);
	SetConsoleTextAttribute(h, 0x04);
	cout << "F";

	for (int i=0;i<nTail;i++)
	{
		setPos(tailX[i], tailY[i]);
		if (i == 0)
		{
			SetConsoleTextAttribute(h, 0x09);
			cout << "0";
		}
		else
		{
			SetConsoleTextAttribute(h, 0x0a);
			cout << "o";
		}
	}
	setPos(0, height + 1);
	SetConsoleTextAttribute(h, 0x06);
	cout << "游戏得分" << score;
}
void Input()
{
	if (_kbhit())
	{
		switch (_getch())
		{
		case 'a':
			dir = LEFT;
			break;
		case 'd':
			dir = RIGHT;
			break;
		case 'w':
			dir = UP;
			break;
		case 's':
			dir = DOWN;
			break;
		case 'x':
			gameOver = true;
			break;
		default:
			break;
		}
	}
}
void Logic()
{
	int prevX = tailX[0];
	int prevY = tailY[0];
	int prev2X, prev2Y;
	tailX[0] = x;
	tailY[0] = y;//把新增的蛇身坐标存放在0位

	switch (dir)
	{
		//case STOP:
			//break;    为什么这两行不要
	case LEFT:
		x--;
		break;
	case RIGHT:
		x++;
		break;
	case UP:     //向上居然是y--
		y--;
		break;
	case DOWN:
		y++;
		break;
	default:
		break;
	}
	//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
	//{
	//	gameOver = true;
	//}
	


	if (x == fruitX && y == fruitY)   //蛇头遇到果子
	{
		score += 10;
		fruitX = rand() % width;
		fruitY = rand() % height;
		nTail++;
	}

	for (int i = 1; i < nTail; i++)
	{
		prev2X = tailX[i];
		prev2Y = tailY[i];
		tailX[i] = prevX;
		tailY[i] = prevY;
		prevX = prev2X;
		prevY = prev2Y;
	}
	for (int i = 1; i < nTail; i++)
	{
		if (tailX[i] == x && tailY[i] == y)
			gameOver == true;
	}

	if (x >= width) x = 0; else if (x < 0)x = width - 1;
	if (y >= height) y = 0; else if (y < 0)y = height - 1;
}
int main()
{
	Initial();
	DrawMap();
	while (!gameOver)
	{
		//Draw();
		Input();
		eraseSnake();
		Logic();
		DrawLocally();
		Sleep(100);
	}
	system("pause");
	return 0;
}

游戏界面

“游戏界面”习题

1、如何更换字符图案?有什么注意事项?

2、Console界面的游戏,将游戏舞台的宽高比设置为1:1的时候,为什么实际画面的宽高比并不是1:1而呈现为长方形?可能的原因是?                                                                                                     A.屏幕的宽高比是16:9,渲染时游戏画面被拉伸了;                                                                           B.Console下单字节字符尺寸的宽高比是1:2                                                                                       C.Console界面下字符的行间距大于字间距:                                                                                     D.观察者自己的视觉误差

下面对游戏界面做简单美化 ,一个游戏有必要为用户提供提示信息,接下来添加三个函数Prompt_info()信息提示;showScore()分数显示;gameOver_info()游戏结束界面显示;                  其中前两个函数的位置希望由参数决定,所以我们传入两个参数。

 来看看函数的具体设计

 

如果想要挑选一些特殊的符号可以这样操作:在开始菜单中输入charmap,根据喜好的风格选择字符后复制过来。

完了把Prompt_info(3,1)改为(5,1)。(把这些注释向左移一点)

运行看看效果,同时注意不同平台对不同字符集的支持程度会有差异,故而有时需要更换其他字符

 

完善细节:

 

完整代码:

#include <iostream>
#include <windows.h>
#include <conio.h>
using namespace std;

bool gameOver, fruitFlash; //判断游戏是否结束
const int width = 20;
const int height = 20;
int x, y, fruitX, fruitY, score;
int tailX[100], tailY[100];
int nTail = 1;
enum eDirection { STOP = 0, LEFT, RIGHT, UP, DOWN };
eDirection dir;

HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);

void Initial()
{
	SetConsoleTitleA("Console_贪吃蛇");
	COORD dSize = { 80,25 };
	SetConsoleScreenBufferSize(h, dSize);
	CONSOLE_CURSOR_INFO _cursor = { 1,false };
	gameOver = false;
	fruitFlash = false;
	dir = STOP;
	x = width / 2;
	y = height / 2;
	fruitX = rand() % width;
	fruitY = rand() % height;
	score = 0;
}
//光标位置
void setPos(int X, int Y)
{
	COORD pos;
	pos.X = X + 1;
	pos.Y = Y + 1;
	SetConsoleCursorPosition(h, pos);  //设置控制台上光标的位置
}
void DrawMap()
{
	system("cls");

	int textColor = 0X06;
	SetConsoleTextAttribute(h, textColor);

	setPos(-1, -1);//绘制顶部的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	\
		for (int i = 0; i < height; i++)
		{
			setPos(-1, i);//绘制左右的墙
			for (int j = 0; j < width + 2; j++)
			{
				if (j == 0)
					cout << "#";
				else if (j == width + 1)
					cout << "#";
				else
					cout << " ";
			}
			cout << endl;
		}
	setPos(-1, height);//绘制下方的墙
	for (int i = 0; i < width + 2; i++)
		cout << "#";
	/*cout << endl;
	cout << "游戏得分" << score << endl;*/
}
void eraseSnake()
{
	for (int i = 0; i < nTail; i++)
	{
		setPos(tailX[i], tailY[i]);
		cout << " ";
	}
}
void DrawLocally()
{
	if (!fruitFlash)
	{
		setPos(fruitX, fruitY);
		SetConsoleTextAttribute(h, 0x04);
		cout << "F";
		fruitFlash = true;
	}
	else
	{
		setPos(fruitX, fruitY);
		SetConsoleTextAttribute(h, 0x04);
		cout << " ";
		fruitFlash = false;
	}
	setPos(fruitX, fruitY);
	SetConsoleTextAttribute(h, 0x04);
	cout << "F";

	for (int i = 0; i < nTail; i++)
	{
		setPos(tailX[i], tailY[i]);
		if (i == 0)
		{
			SetConsoleTextAttribute(h, 0x09);
			cout << "0";
		}
		else
		{
			SetConsoleTextAttribute(h, 0x0a);
			cout << "o";
		}
	}
	setPos(0, height + 1);
	SetConsoleTextAttribute(h, 0x06);
	cout << "游戏得分" << score;
}
void Input()
{
	if (_kbhit())
	{
		switch (_getch())
		{
		case 'a':
			dir = LEFT;
			break;
		case 'd':
			dir = RIGHT;
			break;
		case 'w':
			dir = UP;
			break;
		case 's':
			dir = DOWN;
			break;
		case 'x':
			gameOver = true;
			break;
		default:
			break;
		}
	}
}
void Logic()
{
	int prevX = tailX[0];
	int prevY = tailY[0];
	int prev2X, prev2Y;
	tailX[0] = x;
	tailY[0] = y;//把新增的蛇身坐标存放在0位

	switch (dir)
	{
		//case STOP:
			//break;    为什么这两行不要
	case LEFT:
		x--;
		break;
	case RIGHT:
		x++;
		break;
	case UP:     //向上居然是y--
		y--;
		break;
	case DOWN:
		y++;
		break;
	default:
		break;
	}
	//if (x > width || x<0 || y>height || y < 0) //蛇头遇到墙壁则停止
	//{
	//	gameOver = true;
	//}



	if (x == fruitX && y == fruitY)   //蛇头遇到果子
	{
		score += 10;
		fruitX = rand() % width;
		fruitY = rand() % height;
		nTail++;
	}

	for (int i = 1; i < nTail; i++)
	{
		prev2X = tailX[i];
		prev2Y = tailY[i];
		tailX[i] = prevX;
		tailY[i] = prevY;
		prevX = prev2X;
		prevY = prev2Y;
	}
	for (int i = 1; i < nTail; i++)
	{
		if (tailX[i] == x && tailY[i] == y)
			gameOver =true;
	}

	if (x >= width) x = 0; else if (x < 0)x = width - 1;
	if (y >= height) y = 0; else if (y < 0)y = height - 1;
}
void Prompt_info(int _x,int _y)
{
	int initialX = 20, initialY = 0;

	SetConsoleTextAttribute(h, 0x0F);
	setPos(_x + initialX, _y + initialY);
	cout << "■游戏说明:"; 
	initialY++;  //空一行,可以根据需要调整
	setPos(_x + initialX, _y + initialY);
	cout << "        A.蛇身自撞,游戏结束";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        B.蛇可撞墙";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "■操作说明:";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        □向左移动:←A";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        □向右移动:→D";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        □向下移动:↓S";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        □向上移动:↑W";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        □开始游戏,任意方向键";
	initialY++;
	setPos(_x + initialX, _y + initialY);
	cout << "        □退出游戏: x键退出";
	initialY++;
}

void showScore(int _x, int _y)
{
	setPos(_x + 20, _y + 17);
	SetConsoleTextAttribute(h, 0x0F);
	cout << "当前积分";
	SetConsoleTextAttribute(h, 0x0c);
	cout << score;
}

void gameOver_info()
{
	setPos(5, 8);
	SetConsoleTextAttribute(h, 0xec);
	cout << "游戏结束!!!";
	setPos(3, 9);
	cout << "Y重新开始/N退出";
}
int main()
{
	Initial();
	DrawMap();
	Prompt_info(5,1);

	while (!gameOver)
	{
		//Draw();
		Input();
		eraseSnake();
		Logic();
		DrawLocally();

		showScore(3,1);
		Sleep(100);
	}
	gameOver_info();
	setPos(0, height + 3);
	system("pause");
	return 0;
}

 

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值