一.写在前面的废话
贪吃蛇游戏早在大一刚学编程的时候就写过了,虽然那时候有各种bug…最近有个同学问我不闪屏的贪吃蛇怎么写,我学习了一位大佬的博客,动手做了一个不闪屏的版本。
二.实现方法
首先我们要弄清楚闪屏的原因。因为贪吃蛇是一直在动的,我们就需要不停地输出。当然我们不能一幅图一幅图的输出,也就是说我们要让贪吃蛇看起来在一个框框内移动。普遍的实现方法是,用清屏(system(“cls”))和输出(cout)交替进行。但是,如果我们的地图比较大,就可能出现比较明显的闪屏(尤其是下半部分地图,闪的人眼睛疼)。
比方说我们的地图边界是“#”,地图的第一个“#”和最后一个“#”的输出时间是有差别的,如果显示完所有“#”马上擦除,再来一次,则显示缓冲区不包含所有“#”的状态居多,这就导致了闪屏。
解决办法是,使用两个缓冲区,显示一个缓冲区的同时,将要输出的数据写入另一个缓冲区,这样交替进行就可以无缝衔接。
下面放代码和代码说明。
三. 代码实现
#include
#include
#include
#include
#include
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个分数以及时间,实现方法应该不难,但由于作业多(可能是懒吧)我还没有动手去实现…
如果大家有什么问题或者建议,欢迎提出!