C语言游戏 双缓存解决闪屏问题 详细总结[通俗易懂]

2022-07-25 21:23:53 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

最近,应学校课程要求,要完成一个C语言课程设计。可以是写一个小游戏,或是写管理系统等。

所以,准备做一个改版贪吃蛇:消灭小虫虫(瞎起的名字 :D)。

之前学过Java,所以学C语言也就比较顺利。而在刚学完C语言刚着手准备做C语言的小游戏时,却发现了一个问题——闪屏

(我在网上查找了很多关于双缓存,有关的解答很少,更少能够让一个完全不了解的小白一个明白的解释。下面我想和大家分享我使用双缓存完成了小游戏后的总结体会。希望能够一目了然。)

编辑器 —— Dev-C 5.11


先说一下,C语言来做游戏的原理: 就是在控制台打印图案,然后使用 system(“cls”); 来擦除界面,然后再打印图案的循环过程。

闪屏现象

我们正常打印输出内容的时候,是按顺序输出的。从第一个一直打印的最后一个。

当我们输出的内容十分庞大的时候,第一个和最后一个会存在输出时间差。

也就是前面先输出了,而后面你还没看到。所以会有闪屏的现象。

如何解决闪屏?

治标须治本——双缓存技术

何为双缓存?

我希望大家去看看这个网站:猛击这里

这个网站是我理解双缓存的主要网站,何为双缓存,这位作者写得还是比较易懂的。

不过怎么用?怎么能够用在我的C语言小游戏上?还是会让人一头雾水。

(下面只针对双缓存的实现分享我的总结,不对这个游戏的原理做详解。如果有同学想了解贪吃蛇的实现原理可以去看这位笔者:猛击这里 我的消灭小虫虫以及双缓存的学习也有借鉴他。)


Win32 API

#include<windows.h> 头文件引用

双缓存技术主要使用到了Win32 API

用到的函数有:CreateConsoleScreenBuffer、WriteConsoleOutputCharacter、ReadConsoleOutputCharacter、SetConsoleActiveScreenBuffer、SetConsoleCursorInfo

官方API文档:猛击这里

CreateConsoleScreenBuffer

简单来说就是 初始化新缓存,并配置新缓存参数。

代码语言:javascript复制
HANDLE WINAPI CreateConsoleScreenBuffer(
  _In_             DWORD               dwDesiredAccess,
  _In_             DWORD               dwShareMode,
  _In_opt_   const SECURITY_ATTRIBUTES *lpSecurityAttributes,
  _In_             DWORD               dwFlags,
  _Reserved_       LPVOID              lpScreenBufferData
);
代码语言:javascript复制
dwDesiredAccess:控制台缓冲安全与访问权限,可取值:
    GENERIC_READ (0x80000000L),读权限
    GENERIC_WRITE (0x40000000L),写权限
dwShareMode:共享模式,可取值:
    FILE_SHARE_READ:读共享
    FILE_SHARE_WRITE:写共享
lpSecurityAttributes:安全属性,NULL
dwFlags:缓冲区类型,仅可选:
    CONSOLE_TEXTMODE_BUFFER,控制台文本模式缓冲
lpScreenBufferData:保留,NULL

范例:

代码语言:javascript复制
//具体使用范例
hOutBuf = CreateConsoleScreenBuffer(
	GENERIC_WRITE,  //对控制台屏幕缓冲区的访问
	FILE_SHARE_WRITE, //定义缓冲区可共享写权限
	NULL,//安全属性默认为NULL 
	CONSOLE_TEXTMODE_BUFFER,//缓冲区类型,固定参数 
	NULL
);

//第一个缓存区赋值为hOutBuf,一般是创建两个缓存区(我这命名第二缓存区为:hOutput)

hOutput  = CreateConsoleScreenBuffer(
	GENERIC_WRITE,  //对控制台屏幕缓冲区的访问
	FILE_SHARE_WRITE, //定义缓冲区可共享写权限
	NULL,//安全属性默认为NULL 
	CONSOLE_TEXTMODE_BUFFER,//缓冲区类型,固定参数 
	NULL
);

WriteConsoleOutputCharacter

指定一个缓存区,将需要输出的内容(这规定的类型是字符数组)输出到控制台。

代码语言:javascript复制
BOOL WINAPI WriteConsoleOutputCharacter(
  _In_  HANDLE  hConsoleOutput,   //控制台屏幕缓冲区的句柄。句柄必须具有GENERIC_WRITE访问权限。
  _In_  LPCTSTR lpCharacter,      //写入的字符数组指针
  _In_  DWORD   nLength,          //写入的长度
  _In_  COORD   dwWriteCoord,     //写入起始坐标,  一个COORD结构(后面讲)
  _Out_ LPDWORD lpNumberOfCharsWritten  //指向变量的指针,该变量接收实际写入的字符数。
);

范例:

代码语言:javascript复制
char score_char1[] = "012345678901234567890123456789";
coord.Y = 1;//第一行位置输出
WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );
//之前全局变量定义了: COORD coord = {0,0};  DWORD bytes = 0;

COORD:

代码语言:javascript复制
typedef struct _COORD {
SHORT X; // 横坐标
SHORT Y; // 纵坐标
} COORD;
//使用范例
COORD coord = {0,0};

ReadConsoleOutputCharacter

指定缓存区,读取控制台内容输出到字符数组。

用法和WriteConsoleOutputCharacterA相同,不做范例。

SetConsoleActiveScreenBuffer

双缓存,顾名思义就是有两个缓存。那么这个函数就是用来切换两个缓存的。

代码语言:javascript复制
//设置控制台活动显示缓冲
BOOL WINAPI SetConsoleActiveScreenBuffer(
  _In_ HANDLE hConsoleOutput //hConsoleOutput:控制台输出设备句柄
);

范例:

代码语言:javascript复制
SetConsoleActiveScreenBuffer(hOutBuf);//设置hOutBuf为活动显示的缓冲区

//*...这里是设置不同缓存区的内容等操作的代码...*//

SetConsoleActiveScreenBuffer(hOutput);//设置hOutput为活动显示的缓冲区,即实现了切换缓冲区

SetConsoleCursorInfo

这是一个设置光标的函数:大小,可见度。

代码语言:javascript复制
BOOL WINAPI SetConsoleCursorInfo(
  _In_       HANDLE              hConsoleOutput,  //控制台输出设备句柄
  _In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo //光标信息(大小、可见性)
);

范例:

代码语言:javascript复制
//隐藏两个缓冲区的光标
CONSOLE_CURSOR_INFO cci;
cci.bVisible = 0; // 可见度
cci.dwSize =1;// 大小
SetConsoleCursorInfo(hOutput, &cci);
SetConsoleCursorInfo(hOutBuf, &cci);

/*注: 这里的CONSOLE_CURSOR_INFO结构体如下:
typedef struct _CONSOLE_CURSOR_INFO {
  DWORD dwSize;// 光标百分比厚度(1~100) 
  BOOL  bVisible;// 可见性 FALSE,0,不可见;TRUE,1,可见
  } CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO; */

总体代码:

代码语言:javascript复制
#include<stdio.h>
#include<windows.h>

HANDLE hOutput,hOutBuf;  //控制台屏幕缓冲区句柄
HANDLE houtpoint;
COORD coord = {5,0};//初始输出位置 
DWORD bytes = 0;
int hop_flag = 0;  //通过指针轮流指向两个缓冲区,实现双缓冲 

void printPic();

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
    );
    
    while(1){
		printPic();
		Sleep(600);
	}
} 

void printPic(){
	hop_flag = !hop_flag; 
	if(!hop_flag){
		char score_char1[] = "这是一个缓存区显示内容!11111111";
		coord.Y = 1;
	        WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );
		SetConsoleActiveScreenBuffer(hOutBuf);
	}else{
		char score_char2[] = "这是另一个缓存区显示内容!22222222";
		coord.Y = 1;
    	        WriteConsoleOutputCharacter( hOutput, score_char2, strlen(score_char2), coord, &bytes );
		SetConsoleActiveScreenBuffer(hOutput);
	}
	
	
}

运行结果:

代码语言:javascript复制
WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );

在这里,输出的是字符数组score_char1,用strlen()获得字符数组长度。当然这个要看你想要输出的长度。如果我改成:strlen(score_char1)-10

代码语言:javascript复制
WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1)-10, coord, &bytes );

那结果是这样的:

还有这里我定义了COORD coord = {5,0};也就是初始输出点是<5,0>,又因为coord.Y = 1;所以最后coord = {5,1}

在上面输出结果中,我们还能看到有光标在闪动,如果是做游戏的话,这个光标是很碍眼的。所以就可以用我上面提到过的SetConsoleCursorInfo来隐藏光标。


以上我们用的还是一维数组,只输出一行内容。当然我们可以使用二维数组,直接循环输出以二维数组横坐标和纵坐标大小的面。如下图:

主要代码:

代码语言:javascript复制
……
#define _Y 15  //15行 
#define _X 20  // 20列 

char data[_Y][_X];//这是全局变量定义的字符数组
……


int main(){
    ……//这里的代码不变,和上面一样
}

void printPic(){
	int i,j;
	hop_flag = !hop_flag;
	if(!hop_flag){    //这里是每次交替,直接把hOutput或hOutBuf赋给houtpoint 
		houtpoint = hOutput;
	}else{
		houtpoint = hOutBuf;
	}
    
	for(i = 0;i < _Y;i  ){    //打印你需要的二维数组图案
		for(j = 0;j < _X;j  ){
			if(i == 0|| i == _Y-1 || j == 0 || j == _X-1){
				data[i][j] = '*';
			}else{
				data[i][j] = ' ';
			}
		}
	}
	coord.Y = 1;
	for(i = 0;i < _Y;i  ){    //循环打印每一行
		coord.Y  ;    //每次都打印到下一行
    	WriteConsoleOutputCharacter( houtpoint, data[i], _X, coord, &bytes );
	}    //data[i]:每行的地址。 _X: 每行的长度

	SetConsoleActiveScreenBuffer(houtpoint);
}

动态更新数值:

主要代码:

代码语言:javascript复制
int key = 0;//计数器
……

int main(){
    ……//这里的代码不变,和上面一样
}

void printPic(){
	hop_flag = !hop_flag; 
	if(!hop_flag){
		houtpoint = hOutBuf;
	}else{
		houtpoint = hOutput;
	}
	
	key  ;
	char score_char1[] = "Score:";
	char score_char2[10];
	itoa(key,score_char2,10);//将整型key转换成字符串,存入score_char2,10为十进制转换
	strcat(score_char1,score_char2);//合并两个字符数组
	
	coord.Y = 1;
	for(int i=0;i<20;i  ){//这里循环只是为让大家能看出真的不闪屏
		coord.Y  ;
    	WriteConsoleOutputCharacter( houtpoint, score_char1, strlen(score_char1), coord, &bytes );
	}	
	SetConsoleActiveScreenBuffer(houtpoint);
	
}

看了这么多我相信你们也可以使用C语言写出一个小游戏咯~

在这也感谢其他博主的经验,希望大家一起加油~

如有错误之处,虚心接受~

0 人点赞