C++实现不闪屏的字符游戏--贪吃蛇

一.写在前面的废话
贪吃蛇游戏早在大一刚学编程的时候就写过了,虽然那时候有各种bug…最近有个同学问我不闪屏的贪吃蛇怎么写,我学习了一位大佬的博客,动手做了一个不闪屏的版本。

二.实现方法
首先我们要弄清楚闪屏的原因。因为贪吃蛇是一直在动的,我们就需要不停地输出。当然我们不能一幅图一幅图的输出,也就是说我们要让贪吃蛇看起来在一个框框内移动。普遍的实现方法是,用清屏(system(“cls”))和输出(cout)交替进行。但是,如果我们的地图比较大,就可能出现比较明显的闪屏(尤其是下半部分地图,闪的人眼睛疼)。
比方说我们的地图边界是“#”,地图的第一个“#”和最后一个“#”的输出时间是有差别的,如果显示完所有“#”马上擦除,再来一次,则显示缓冲区不包含所有“#”的状态居多,这就导致了闪屏。
解决办法是,使用两个缓冲区,显示一个缓冲区的同时,将要输出的数据写入另一个缓冲区,这样交替进行就可以无缝衔接。
下面放代码和代码说明。

三. 代码实现

#include<iostream>
#include<time.h>
#include<conio.h>
#include<Windows.h>
#include<string.h>
using namespace std;

#define H 21         
#define W 41//20行40列的地图
HANDLE hOutput, hOutBuf;//控制台屏幕缓冲区句柄
HANDLE *houtpoint;//显示指针
COORD coord = { 0,0 };//双缓冲处理显示
DWORD bytes = 0;
bool showCircle = false;//判断显示哪个缓冲区
int snakeHeadX = 1;
int snakeHeadY = 2;//蛇头'@'的初始坐标
char direction = 'd';//初始方向向右
bool gameRunning = true;//判断游戏是否结束
int foodX, foodY;//食物'$'的坐标
int snakeLength = 1;//蛇身初始长度为1
int foodEaten = 0;//吃掉的食物数,用来计算游戏难度和分数。
int score = 0;
char scoreArray[1000];//分数

struct snakeBody {//蛇身结构体,存蛇身'*'的坐标
	int x[H*W];
	int y[H*W];
}snake;

char Map[H][W]= {
	"########################################",
	"#*@                                    #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"#                                      #",
	"########################################",
};

void gameover() {//控制游戏结束
	gameRunning = false;
	showCircle = !showCircle;
	if (showCircle)
	{
		houtpoint = &hOutput;
	}
	else
	{
		houtpoint = &hOutBuf;
	}
	memset(Map, 255, sizeof(Map));//将地图清零
	for (int i = 0; i < H; ++i)
	{
		coord.X = 44;//以(45,5)为起点
		coord.Y = i + 5;
		WriteConsoleOutputCharacterA(*houtpoint, (char*)Map[i], W, coord, &bytes);
	}
	//打印"Game Over!"
	coord.X = 44 + H / 2 - 3;
	coord.Y = 5;
	WriteConsoleOutputCharacterA(*houtpoint, "GAME OVER", 9, coord, &bytes);
	//设置新的缓冲区为活动显示缓冲
	SetConsoleActiveScreenBuffer(*houtpoint);
}

void display() {//显示游戏画面,可以加上游戏等级,分数等。
	if (gameRunning) {
		showCircle = !showCircle;
		if (showCircle)
		{
			houtpoint = &hOutput;
		}
		else
		{
			houtpoint = &hOutBuf;
		}
		coord.X = 51;
		coord.Y = 3;
		score = foodEaten * 100;
		sprintf_s(scoreArray, "Score:%d", score);//格式
		WriteConsoleOutputCharacterA(*houtpoint, scoreArray, strlen(scoreArray), coord, &bytes);
		for (int i = 0; i < H; ++i)
		{
			coord.X = 44;//以(45,5)为起点
			coord.Y = i + 5;
			WriteConsoleOutputCharacterA(*houtpoint, (char*)Map[i], W, coord, &bytes);
		}
		//设置新的缓冲区为活动显示缓冲
		SetConsoleActiveScreenBuffer(*houtpoint);
	}
	Sleep(200-foodEaten*10);//吃的越多速度越快,增加难度
}

void setMap() {//更新蛇、食物的坐标
	Map[snakeHeadX][snakeHeadY] = '@';
	for (int i = 0; i < snakeLength; ++i) {
		Map[snake.x[i]][snake.y[i]] = '*';
	}
	Map[foodX][foodY] = '$';
}

void spawnFood() {//设置食物坐标
	while (1) {
		srand((unsigned)time(NULL)); //初始化随机数
		foodX = rand() % 20;
		foodY = rand() % 40;
		if (Map[foodX][foodY] != '@'&&Map[foodX][foodY] != '*'&&Map[foodX][foodY] != '#')//不重合
			break;
	}
}

void snakeMove() {//蛇移动
	int pre_snakeHeadX = snakeHeadX;//蛇头的上一个坐标
	int pre_snakeHeadY = snakeHeadY;
	if (_kbhit()) {//是否按下键盘
		char tempControl = _getch();
		switch (tempControl) {
		case 'w':
			if (direction == 's')
				break;
			direction = tempControl;
			break;
		case 'a':
			if (direction == 'd')
				break;
			direction = tempControl;
			break;
		case 's':
			if (direction == 'w')
				break;
			direction = tempControl;
			break;
		case 'd':
			if (direction == 'a')
				break;
			direction = tempControl;
			break;
		}
	}
	switch (direction) {
	case 'a': snakeHeadY--; break;
	case 'w': snakeHeadX--; break;
	case 's': snakeHeadX++; break;
	case 'd': snakeHeadY++; break;
	}
	//判断游戏是否结束
	if (Map[snakeHeadX][snakeHeadY] == '#' || Map[snakeHeadX][snakeHeadY] == '*')
		gameover();
	//判断是否吃到食物
	if (Map[snakeHeadX][snakeHeadY] == '$') {
		foodEaten++;
		snake.x[snakeLength] = pre_snakeHeadX;
		snake.y[snakeLength] = pre_snakeHeadY;
		snakeLength++;
		spawnFood();
		return;
	}
	//更新蛇身坐标
	Map[snake.x[0]][snake.y[0]] = ' ';
	for (int i = 0; i < snakeLength - 1; ++i)
	{
		snake.x[i] = snake.x[i + 1];
		snake.y[i] = snake.y[i + 1];
	}
	snake.x[snakeLength - 1] = pre_snakeHeadX;
	snake.y[snakeLength - 1] = pre_snakeHeadY;
}

int main() {
	//创建新的控制台缓冲区
	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);
	//游戏开始
	spawnFood();
	snake.x[0] = 1;
	snake.y[0] = 1;//蛇身初始位置
	setMap();
	display();
	while (gameRunning) {
		snakeMove();
		setMap();
		display();
	}
	_getch();//等待游戏结束
	
}

四. 代码说明
1.几乎所有变量都是全局变量,也有用类实现的,但是我觉得全局变量更简洁一些。
2.字符的含义分别是:#代表墙,撞上就game over;@代表蛇头;*代表蛇身,初始长度为1;$代表食物;可走的路为’ '。
3.我在声明地图的时候就做了初始化,这样每次设置地图只需对蛇和食物的坐标作修改。正如图所示,游戏开始时贪吃蛇的蛇身长度为1,蛇头向右边前进。
4.整个程序由 控制游戏结束、打印地图、设置地图、产生食物、蛇移动5个模块组成,分别写成5个函数。
5.我的x坐标是放在二维数组前一位的,y坐标是放在二维数组后一位的,也就是从上往下数是x坐标,从左往右数是y坐标。
6.打印分数的时候用到了sprintf函数,作用是把int型的score格式化后存入字符串scoreArray中。顺便一提,如果用vs运行代码,应当写成sprintf_s。
7.蛇身的坐标变化过程是这样的:我们首先用一个结构体snake(含有数组x[]和y[])来保存蛇身的坐标,用一个整型变量snakeLength保存蛇身的长度。当蛇吃了一个食物(蛇头的位置变成了食物的位置),蛇身长度要加一,我们将新增加的蛇身加到蛇头的上一个位置,这样其他的蛇身坐标就不用变化。新的蛇身坐标存入数组:

snake.x[snakeLength] = pre_snakeHeadX;
snake.y[snakeLength] = pre_snakeHeadY;
snakeLength++;

也就是说,新加入的蛇身坐标排在数组的后面。那么,怎么实现蛇身的移动呢?我们考虑后一个蛇身的坐标变成前一个蛇身的坐标,最前面的蛇身的坐标变成蛇头的上一个坐标:

 for (int i = 0; i < snakeLength - 1; ++i)
	{
		snake.x[i] = snake.x[i + 1];
		snake.y[i] = snake.y[i + 1];
	}
	snake.x[snakeLength - 1] = pre_snakeHeadX;
	snake.y[snakeLength - 1] = pre_snakeHeadY;

当然还要注意,与前进方向相反的键入是无效的。
8.关于运行:我在vs上面运行的时候会出现按键延迟的情况,就是我的蛇卡卡的,只能隔行走,不能直接转弯到相邻的行。但是我在dev上面运行就不会有这种问题。知道的大佬可以告诉我为什么吗…

五. 增加游戏趣味性的设计
贪吃蛇的形状可以更好看。比如在本博客开始提到的大佬,他利用ASCII做了个很好看的贪吃蛇游戏。
在本贪吃蛇程序中,我的设计是吃的食物越多,贪吃蛇的速度越快,每吃一个食物速度就比初始速度快5%(通过减少Sleep的时间实现)。我还显示了分数,一个食物100分。之前我有考虑增加排行榜,初步设想是采用文件读写,文件保留最高的10个分数以及时间,实现方法应该不难,但由于作业多(可能是懒吧)我还没有动手去实现…
如果大家有什么问题或者建议,欢迎提出!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值