问题引入
基于C语言的游戏开发的动态画面往往是不断的刷新显示区来实现的,即不断地输入和清空。因为计算机的计算速度很快,所以在画面较小的情况下我不会觉得有闪屏或不流畅的体验。 但一旦要刷新的画面较大,是会闪瞎开发者的眼的! 比如下面这段代码
int main()
{
while(1){
for(int i=1;i<=50;i++)
{
for(int j=1;j<=50;j++)
{
printf("*");
}
printf("\n");
}
system("cls");}
return 0;
}
如果这样做游戏的,用户的游戏体验可谓极差无比!
是什么原因使屏幕闪烁呢?
我们在code中可以看出,我们的printf是一个一个打印符号的,他不能像easyx中整体打印一个图形。这也就导致我们每个位置出现的时间会有差异,所以打印得越多,闪烁就会越厉害。
解决方式一:局部改动(不推荐)
既然我们将一个图形不断刷新会闪烁,那我们就改变局部把。有一些贪吃蛇程序是用到这一招,因为贪吃蛇会每一帧画面需要改动的地方相对较少,这样可以大大减弱我们感受到的闪烁。这种方法可以用改变光标的位置来实现。
但是!
这也只是治标不治本的方法,如果需要改动的地方过多,也同样会出现闪烁。所以这种办法仅限于贪吃蛇这类画面改动量较小的游戏。
解决方式二:双缓存(推荐)
这也是本篇终点介绍的,这种方式的画面,不管有多少改变的地方,都只需要刷新一次画面即可。
我们先看一下我们控制台显示内容的过程
我们知道我们运行程序那个黑框就是控制台,在默认情况下他显示的内容就是默认的显示缓冲区的内容。我们之前提到了显示出现闪烁是因为缓冲区的内容不断清除和打印,那如果我们能让打印的过程不显示出来,只显示打印完后的显示缓冲区不就行了吗。 当然,打印的过程既然不显示缓冲区的内容也总得有东西让控制台来显示吧!那么我们就需要有两个缓冲区,打印一个的同时显示另一个,不断重复这个过程。
考虑到操作的一些方便,我们不继续用默认的缓冲区,我们新建两个缓冲区A和B,示意如下
其中第一组红黄箭头表示控制台在改变哪一个缓冲区的内容(可以理解为打印),第二组红黄箭头表示显示的是哪一个缓冲区的内容,颜色需要对应起来。
(听不懂没关系,我们直接看一下代码)
- 我们先做一些准备
char date[35][65]; //存放画面的二维数组
int fxx[5]={0,1,-1,0,0}; //方向数组,移动方向的时候使用
int fxy[5]={0,0,0,-1,1};
HANDLE hOutput, hOutBuf;
//两个句柄,这里可以就理解为我们等会需要用的两个缓冲区
COORD coord = { 0,0 }; //位置坐标,用到的时候解释
DWORD bytes = 0; //这个主要是要配合下面的一个函数
void initconsoleScreenBuffer(); //初始化我们的缓存区
int show(char input); //交换显示缓存区
- 重点(模板)
这一步主要是模板,模板主要部分来自作者_寒潭雁影
void initconsoleScreenBuffer()
{
hOutBuf = CreateConsoleScreenBuffer( //创建缓冲区
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
hOutput = CreateConsoleScreenBuffer( //创建缓冲区
GENERIC_WRITE,
FILE_SHARE_WRITE,
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
//以下5行是设置这两个缓冲区的光标不可见
CONSOLE_CURSOR_INFO cci; //创建光标
cci.bVisible = 0; //光标可见度为0
cci.dwSize = 1; //光标大小为1,可省略
SetConsoleCursorInfo(hOutput, &cci); //设置第一个缓冲区的光标参数
SetConsoleCursorInfo(hOutBuf, &cci); //设置第二个缓冲区的光标参数
//这里我们初始化我们的地图
for(int i=0;i<30;i++)
{
for(int j=0;j<60;j++)
{
date[i][j]='-';
if(i==15&&j==15)
date[i][j]='W';
}
}
}
- 实现双缓冲区切换显示
注:我们在进入show前已经接收了键盘输入的方向
说一个小知识,我们的printf是标准输出,即是输出在默认的显示缓冲区,如果我们将显示的缓冲区设置为我们自己i新建的缓冲区,我们输出的内容是看不到的。
int show(char input)
{
int toward;
switch(input)//确定方向
{
case 's' : toward=1;break;
case 'w' : toward=2;break;
case 'a' : toward=3;break;
case 'd' : toward=4;break;
default : toward=0;break;
}
for(int i=0;i<30;i++)
{
for(int j=0;j<60;j++)
{
if(date[i][j]=='W'&&i+fxx[toward]>=0&&i+fxx[toward]<30&&j+fxy[toward]>=0&&j+fxy[toward]<60)//如果这次移动有效
{
date[i][j]='-';
date[i+fxx[toward]][j+fxy[toward]]='W';
goto loop1;
}
}
}
loop1:
for(int i=0;i<30;i++)
{
coord.Y=i;
//坐标位置设定为第i行第0列,下面的函数需要这个坐标位置
WriteConsoleOutputCharacterA(hOutBuf,date[i],60,coord,&bytes);
//向缓冲区写入内容(不管这个缓冲区需要需要显示都可以写入)
//五个参数分别是
/*缓冲区名、
*需要写入的内容首地址、
*需要写入内容的长度、
*开始写入的位置、
*记录成功写入了多少(这里没用,只是为了配合这个函数)
*/
}
//将这个缓冲区显示在控制台上
SetConsoleActiveScreenBuffer(hOutBuf);
//继续输入,此时我们始终显示的hOutBuf,只有再次设置显示另一个缓冲区或程序结束才会改变显示的缓冲区
input=getch();
if(input=='0')
return 0;
switch(input)
{
case 's' : toward=1;break;
case 'w' : toward=2;break;
case 'a' : toward=3;break;
case 'd' : toward=4;break;
default : toward=0;break;
}
for(int i=0;i<30;i++)
{
for(int j=0;j<60;j++)
{
if(date[i][j]=='W'&&i+fxx[toward]>=0&&i+fxx[toward]<30&&j+fxy[toward]>=0&&j+fxy[toward]<60)
{
date[i][j]='-';
date[i+fxx[toward]][j+fxy[toward]]='W';
goto loop2;
}
}
}
loop2:
for(int i=0;i<30;i++)
{
coord.Y=i;
WriteConsoleOutputCharacterA(hOutput,date[i],60,coord,&bytes);
}
SetConsoleActiveScreenBuffer(hOutput);
return 1;
}
最终效果
在任何时候可以按0结束程序
/*
* @Description:
* @Autor: Kadia
* @Date: 2020-05-18 22:03:18
* @LastEditors: Kadia
* @connect: vx:ccz1354 qq:544692713
* @LastEditTime: 2020-05-19 12:22:14
*/
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>
#include <windows.h>
char date[35][65];
int fxx[5]={0,1,-1,0,0};
int fxy[5]={0,0,0,-1,1};
HANDLE hOutput, hOutBuf;
COORD coord = { 0,0 };
DWORD bytes = 0;
void initconsoleScreenBuffer();
int show(char input);
int main()
{
char input;
initconsoleScreenBuffer();
while(1)
{
input=='0';
if(!show(input))
break;
input=getch();
if(input=='0')
break;
}
return 0;
}
void initconsoleScreenBuffer()
{
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);
for(int i=0;i<30;i++)
{
for(int j=0;j<60;j++)
{
date[i][j]='-';
if(i==15&&j==15)
date[i][j]='W';
}
}
}
int show(char input)
{
int toward;
switch(input)
{
case 's' : toward=1;break;
case 'w' : toward=2;break;
case 'a' : toward=3;break;
case 'd' : toward=4;break;
default : toward=0;break;
}
for(int i=0;i<30;i++)
{
for(int j=0;j<60;j++)
{
if(date[i][j]=='W'&&i+fxx[toward]>=0&&i+fxx[toward]<30&&j+fxy[toward]>=0&&j+fxy[toward]<60)
{
date[i][j]='-';
date[i+fxx[toward]][j+fxy[toward]]='W';
goto loop1;
}
}
}
loop1:
for(int i=0;i<30;i++)
{
coord.Y=i;
WriteConsoleOutputCharacterA(hOutBuf,date[i],60,coord,&bytes);
}
SetConsoleActiveScreenBuffer(hOutBuf);
input=getch();
if(input=='0')
return 0;
switch(input)
{
case 's' : toward=1;break;
case 'w' : toward=2;break;
case 'a' : toward=3;break;
case 'd' : toward=4;break;
default : toward=0;break;
}
for(int i=0;i<30;i++)
{
for(int j=0;j<60;j++)
{
if(date[i][j]=='W'&&i+fxx[toward]>=0&&i+fxx[toward]<30&&j+fxy[toward]>=0&&j+fxy[toward]<60)
{
date[i][j]='-';
date[i+fxx[toward]][j+fxy[toward]]='W';
goto loop2;
}
}
}
loop2:
for(int i=0;i<30;i++)
{
coord.Y=i;
WriteConsoleOutputCharacterA(hOutput,date[i],60,coord,&bytes);
}
SetConsoleActiveScreenBuffer(hOutput);
return 1;
}