目录
文前声明:
最近在做很多简单代码的小游戏,所以需要从开发小游戏专栏的开头就介绍清屏效果的有效实现方法,途径与原理。本人被困惑了一段时间,找遍了绝大多数c站以及其他网站的资料以此来解释清屏效果的实现。c站目前没有解释清屏太过详细的文章,可能是由于内容易懂而本人过于笨拙吧?但是由于我在黑暗中踩到过水洼,所以我要在黑暗中引燃火把。
在文章开头,我们需要知道:我们所谓的清屏只是一个手段,而我们真正想要实现的理想效果是通过用户操作来实现画面的更新
system("cls")
system函数有很多功能,system("cls")函数是实现清屏功能最直接,也是从真正意义上对“清屏”两字做出实质性践行的。举个栗子:
#include <stdio.h>
int main(){
int a,b,c;
c=a+b;
printf("%d",c);
system("cls");
return 0;
}
对于这段代码中,大家可以很明确的看到system("cls")函数被放在最后一行,我们在计算c并输出c的结果后对当前屏幕上显示的内容进行了清屏处理;
但实际上,c已经被赋值为a+b。
也就是说,system("cls")只是对当前显示器上的内容进行清空处理,对于已经做出计算或赋值的变量等保存方式不会进行清除,我们后来的几个清屏函数也是这样单纯字面意义上的清除,并不会对其他变量的赋值或定义情况做出更改。
对于system("cls")函数的使用,我们不需要引入其他头文件,它是一种较为简单的使用,它的实现原理也很简单:
清空当前显示屏上的所有内容,并将清空后将需要打印在显示屏上的内容与缓冲区同步打印。
以我自己更为口头阐述的表达:就是在清空屏幕后,计算机会将需要打印的内容打印在缓冲区中,在缓冲区打印的同时,显示屏与缓冲区联通,显示屏直播缓冲区正在加载的内容。
但我们都知道凡事都不太好做出事半功倍的效果,也正是因此我们的system("cls")函数有了一定的限制:
1.只可以进行小部分内容的瞬间清屏
2.倘若输出内容较大,譬如一般以c语言做游戏都需要存放在循环体中,那时每当进行一次画面输出,都需要在循环体头部存放一个system("cls")函数,在编译后的运行阶段,你会看到画面不断的闪烁,这就体现出了这个函数的弊端!
system("cls")闪烁的原因解释:
在输出内容较为庞大的时候,由于这个函数的原理是同时清屏并从头开始打印,所以会存在“第一个”内容和“最后一个”内容输出的时间差,也就是在最后一个内容开始进行打印结束后循环体执行了一轮下面程序的编译,再次开始了新一轮循环,即再次使用了清屏函数,由于内容的输出和清空不断更替,所以会出现屏幕内容闪烁的问题!
那我们也能从原因中找到两个解决方法:
解决方案
1.使用sleep/Sleep函数
【在本人的实践过程中(可能还是因为不细心观察s大小写的区别吧),发现s大写和小写的刷新频率是不一样的,Sleep的单位是ms,sleep的单位是s,而且经过测试,自我感觉用这种方法最舒服的是Sleep(100)。只在这里对S和s做出区分,在后文中统一用sleep来作为标识符表达这个函数】
首先介绍下sleep函数,sleep函数的作用效果其实就是将屏幕短暂的暂停,暂停的秒数是你在括号中输入的时间,例如:
Sleep(100);
它的意义就是在程序编译到此处时,程序的编译时间暂停0.1s。
当然,这种解决方法同样也是不适合使用的,因为sleep虽然解决了屏幕多次闪烁的问题,可是这仍然没有解决根本上的原因,并且阻断了与玩家的实时交互,会在一定程度上遏制玩家继续进行游戏的兴趣,这种卡顿感是游戏制作中所避讳的,我们也只是在这里提供一种解决方法,培养思维。
顺便介绍下前面所缺少的知识,因为部分游戏的制作对于短暂的视觉暂停效果是不可或缺的。
2.再次引入一种新的具有清屏效果的工具
这种方法肯定是直接解决的最佳方案了,也是我们要继续介绍的下面两种从不同层面上所实现的清屏效果。
gotoxy函数
第一种实现方式,我们是通过再次引入一种新的函数以近似同样的效果来实现对屏幕的二次输出。
需要引入头文件
#include <windows.h>
首先,需要介绍的是gotoxy函数是需要自己定义的,一般的编译器比如我第一篇文章所推荐的Dev c++中没有函数库包含gotoxy函数的功能,所以我们需要在主函数前进行如下输入:
void gotoxy(int x,int y)
{
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(handle,pos);
}
(*以下内容对新手不做详细要求,我在这里声明只是有助于深度理解:
HANDLE是一种句柄类型,句柄是一种获得关键信息的类似于c语言程序设计中标识符的东西,可以从计算机内部获取信息作为每个操控的专属识别传递到索取的地方。
STD_OUTPUT_HANDLE是nstdHandle的一种输出设备的取值,所取的值还有另外两种我们不会在这里展开讲。
COORD是windows API中定义的结构,它表示了字符在控制台屏幕上的坐标,(x,y)与我们gotoxy(x,y)函数中输入的值相匹配。)
它的作用效果并不难,就是将光标移动到所输入的位置(x,y)中。
它的作用效果并不是清屏,很奇怪,对吗?为什么它可以进行清屏效果的实现?
主观上的,我将有着清屏作用的这两种函数分为了整体清屏和局部清屏。
字面意思来看,整体清屏也就是全部一次清除后再次输出,即前文介绍的system("cls")函数;而局部清屏就是我们所要讲到的如何利用gotoxy函数实现清屏效果:
在我们已经执行了一次循环体的前提下,我们的显示屏中已经有了输出界面,而这时我们再次执行循环体,gotoxy函数会将光标移动到(x,y)的位置,而我们会在循环体头部写入
gotoxy (0,0);
这样,光标会移动到(0,0)的位置,然后进行二次绘图,类似于“重画”的效果,重新进行打印。
那为什么可以防止闪烁呢?
我们在进行光标移动的时候,只是在单纯的进行循环体中内容的输出,而我们是在上一次循环体已输出内容的基础上进行的二次输出,即在一个已经绘画的模板上再次进行一次相似的创作。
在这里,注意我们提到的例子中用词是“相似的创作”,这例子也不是空穴来风,因为我们只是重绘,所以我们不可以有太大地方的改动(假如我们可以实现动画的制作,在每一帧的情况下迟早都会有大幅度的画面改动,那么这时我们就会看到一个画面中上半部分出现这一帧的内容,下半部分出现前一帧的情况)。因此,这种解决方案也被称为“不完美的解决闪烁的方案”。
当然,在运行的时候,会出现光标在屏幕中“随处移动”的现象,所以我们需要再次应用另一种函数,如下:
void HideCursor()
{
CONSOLE_CURSOR_INFO cursor_info={1,0};
SetConsorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);
}
int main(){
HideCursor();
return 0;
}
这个函数当然也是需要自己写入,它的作用是隐藏光标。只是为了防止光标乱窜,辅佐gotoxy函数更好的实现清屏功能,并无直接起到清除内容的作用。
那么我们到底有没有完全完美的解决方案呢?
双缓冲技术
这是最最最完美的解决方案,对于普通的接触小型游戏或其他方向的部分人可能不会接触到双缓冲的实现。在图形处理编程过程中,双缓冲是基本技术之一。正因为能十分“无痕”的处理图像问题,所以它可以在编译游戏领域解决闪烁问题,也由此使得它得到了广泛的应用。
(由于本人学识有限,对双缓冲技术做出较为口头的阐述中倘若出现错误还请在评论区指出错误,我会及时进行更正)
首先,我需要给出我们计算机较为基层的执行次序:
为了我们解释更加方便,所以我们引入上面这张流程图,显示缓冲区有标准输入输出流的支持,这样我们的显示屏会将显示缓冲区的输出内容实时获取。正因为如此,所以我们无法完全意义上的阻止屏幕闪烁。
因为我们没有时间去存储先前的内容就清空了屏幕,打印了更新后的内容。
而双进程是这样实现的:
#include <stdio.h>
#include <Windows.h>
int main(){
//获取默认标准显示缓冲区句柄
HANDLE hOutput;
COORD coord={0,0};
hOutput=GetStdHandle(STD_OUTPUT_HANDLE);
//创建新的缓冲区
HANDLE hOutBuf = CreateConsoleScreenBuffer(
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CONSOLE_TEXTMODE_BUFFER,
NULL
);
//设置新的缓冲区为活动显示缓冲
SetConsoleActiveScreenBuffer(hOutBuf);
//隐藏两个缓冲区的光标
CONSOLE_CURSOR_INFO cci;
cci.bVisible=0;
cci.dwSize=1;
SetConsoleCursorInfo(hOutput, &cci);
SetConsoleCursorInfo(hOutBuf, &cci);
//双缓冲处理显示
DWORD bytes=0;
char data[800];
while (1)
{
for (char c='a'; c<'z'; c++)
{
system("cls");
for (int i=0; i<800; i++)
{
printf("%c",c);
}
ReadConsoleOutputCharacterA(hOutput, data, 800, coord, &bytes);
WriteConsoleOutputCharacterA(hOutBuf, data, 800, coord, &bytes);
}
}
return 0;
}
为了实现这个过程,我们调用了几个win32 API函数,但是这些是不需要进行记忆的,只要在调用时查找内容即可。
主要的部分已经在后面进行了批注声明,可以参考注释进行大致的理解。
代码看不懂没有关系,我们直接走流程图,看他的原理和底层逻辑是如何实现的:
它基于先前输入的内容创造了一个空间,它可以实现真正意义上的“无痕”清屏,因为我们需要足够的时间去准备输出内容的存储空间。
我找了部分大佬解释的双缓冲文章,其中解释最为明白的是唯梦永恒大佬的文章:
由于默认的缓冲区有标准输入输出流的支持,所以为了输入输出的方便,我们将默认的显示缓冲区作为后台缓冲区,而将新建的显示缓冲区作为活动的屏幕显示。基本过程是,先将要显示的数据传输到默认缓冲区,等到数据全部写入后,再一次性填充到新建的显示缓存区。
上面是引用和大佬的文章内容,可能难以理解,以本人的见解来看是这么解释的:
对比刚才单缓冲区的情况,我们人为性的再次增加一个显示缓冲区(这也是双缓冲名字的由来),让这个新加入的缓冲区(图中的第二缓冲区)作为一个目前屏幕显示所引用的缓冲区,而之前的缓冲区用于准备下一帧需要打印的内容,给第二缓冲区用于数据准备,在第一个缓冲区中进行打印准备,而当第一个缓冲区中打印内容加载完备后,第二缓冲区瞬间更替第一缓冲区的内容,此时第一缓冲区仍然在准备下一次所需要打印的内容...如此反复,就真正意义上的实现了一种“换像”,甚至不需要我们之前所提及的清屏工作,因为我们的清屏工作也是为了实现本文的开头所述,也是我们使用清屏函数的最终目的:画面的更新 而做准备。
如此,我们就真正意义上的达成了我们的最终目的!
至此,我很兴奋地告诉各位,今天要阐述清屏的有关知识到此结束。
计算机的底层无比奇妙,请坚信我们永远都在学习这一理念!我们不止可以利用c语言操纵软件,还可以实现与硬件的联通,即便现在的我们被现有的知识所束缚,但我坚信未来的高阶是需要累累硕果铺垫而成就的高堂,在此次解决清屏问题中我更加肯定了这一想法,我会为了我的名字,我的目标:励志做大佬 而奋斗。
那么,我今天的博客到此为止,感谢您认真看完了本篇小白编写的博客,以上均为本人理解,如有错误欢迎在评论区指出。
虽然基础薄弱,但今天仍是在幻想成为大佬的一天。
读书,是为了在落幕无光的黑暗中找到方向