C语言游戏 双缓存解决闪屏问题 详细总结

本文详细介绍了在C语言控制台环境下利用Win32API实现双缓存技术,有效解决了游戏开发中常见的闪屏问题。通过具体实例演示了如何创建控制台屏幕缓冲区,切换缓存区及隐藏光标,适用于初学者掌握双缓存原理。

最近,应学校课程要求,要完成一个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

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

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

范例:

//具体使用范例
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

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

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

范例:

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

COORD:

typedef struct _COORD {
SHORT X; // 横坐标
SHORT Y; // 纵坐标
} COORD;
//使用范例
COORD coord = {0,0};

ReadConsoleOutputCharacter

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

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

SetConsoleActiveScreenBuffer

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

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

范例:

SetConsoleActiveScreenBuffer(hOutBuf);//设置hOutBuf为活动显示的缓冲区

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

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

SetConsoleCursorInfo

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

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

范例:

//隐藏两个缓冲区的光标
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; */

总体代码:

#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);
	}
	
	
}

运行结果:

WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1), coord, &bytes );

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

WriteConsoleOutputCharacter( hOutBuf, score_char1, strlen(score_char1)-10, coord, &bytes );

那结果是这样的:

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

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


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

主要代码:

……
#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);
}

动态更新数值:

主要代码:

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语言写出一个小游戏咯~

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

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

🙂🙂🙂

MFC调用win32窗口显示调试信息,使用AllocConsole 函数(2010-11-16 15:33:25)转载标签: 调试win32mfc杂谈 分类: 编译器 AllocConsole Function 为主调进程分配一个新的控制台。 语法 C++ : BOOL WINAPI AllocConsole(void); 参数: 无 返回值:如果函数成功,返回值是非零值;如果函数失败,返回值是零值。 备注: 一个进程仅能关联一个控制台,所以该函数在主调进程已经具有控制台时将会失败。 一个进程可以使用 FreeConsole 函数来释放与之关联的控制台,之后它就可以调用该函数来创建一个新的控制台或使用 AttachConsole 函数来关联另一个控制台。 如果主调进程创建了一个子进程,则子进程也将继承这个新创建的控制台。 该函数为新的控制台初始化标准输入、输出、错误句柄等。 标准输入句柄是一个控制台输入缓冲的句柄,标准输出和标准错误句柄则是控制台屏幕缓冲的句柄。为了获得这些句柄,可以使用 GetStdHandle 函数。 该函数主要用于GUI应用程序来创建一个控制台窗口。 GUI应用程序初始化时时没有控制台的,而控制台应用程序则以控制台来初始化的。 要求 : Minimum supported client Windows 2000 Professional Minimum supported server Windows 2000 Server Header: Wincon.h (include Windows.h) Library: Kernel32.lib DLL Kernel32.dll ---------------------------------------------------------------------------------------- 虽然WIN32时代是图形界面时代,但偶尔程序中还需要用到命令行模式,比如批处理,这时再搞个图形界面出来显得似乎就不那么专业了。但客户还需要在正常状态下(对于命令行模式,我认为用户是非正常状态,比如脑子进水。)使用图形界面,这决定我们不能开一个控制台工程,而需要使用MFC exe程序。 OK,理所当然的,通过条件控制,命令行下我关掉对话框界面的调用代码,再使用 cout << "Hello world!" << endl; 来向这个友好的世界打个招呼,并坚持认为这句问候应该显示在CMD那个漆黑的窗口里。 很沮丧地说,事实给我与痛击。cmd窗口里仍然漆黑一片,系统完全不理会我的友好。 邓爷爷说,改革开放好!也许,我也需要个改革。 在同事mr. zhang的指导下,我找到一组API:Console Functions!正是这组API,最终让我的友好得以正当地表达。 一。创建一个Console,AllocConsole 直接使用 AllocConsole(); 马上,若是进程内第一次调用这个函数,一个空的cmd窗口会蹦出来。需要注意,一个进程只能创建一个console,多次调用会返回FALSE;而且,这个窗口是个独立的控制台窗口。 MSDN的解释:A process can be associated with only one console, so the AllocConsole function fails if the calling process already has a console. 还有段:If the calling process creates a child process, the child inherits the new console. 二。显示Hello World,WriteConsole 有了console,我们还需要获取它的句柄HANDLE,然后才能在上面显示。方法是 GetStdHandle,它会获取前面我们AllocConsole得到的cmd窗口的句柄;若未调用AllocConsole,将获取标准的输入输出窗口句柄。 MSDN的解释:The GetStdHandle function returns a handle for the standard input, standard output, or standard error device. HANDLE hdlWrite = GetStdHandle(STD_OUTPUT_HANDLE); //这里也可以使用STD_ERROR_HANDLE TCHAR c[] = {"Hello world!"}; WriteConsole(hdlWrite, c, sizeof(c), NULL, NULL); 得到Console的句柄后直接使用 WriteConsole 函数即可在屏幕上显示啦。这个函数有5个参数:第一个是console的句柄,第二个是写出内容的地址,第三个参数是预计写出长度,第四个参数是实际写出长度,可为NULL,但不建议,若使用NULL,boundchecker会在这提示错误用法,第五参数系统保留,必须使用NULL。 MSDN里说也可以使用WriteFile来向console的handle输出。 三。获取用户输入, ReadConsole 光显示是不够的,用户还需要操作啊,当然,是键盘输入。 前面我们已经创建了一个Console,输入也需要使用这个console,但若还用上面的OUTPUT句柄就不行了,我们会发现,程序在下面这句会直接跳过。 ReadConsole(hdlWrite, c, 2, NULL, NULL); 这里,我们还需要一个STD_INPUT_HANDLE;如下: TCHAR Buffer[100]; //开缓存 memset(Buffer, 0, 100); DWORD dwCount = 0;//已输入数 HANDLE hdlRead = GetStdHandle(STD_INPUT_HANDLE); ReadConsole(hdlRead, Buffer, 100, &dwCount;, NULL); 这个函数和WriteConsole类似,但需要注意的是,第四个参数必须指定,否则会无法读取。 MSDN里还提到,若需要获取其它键盘外的输入信息,如鼠标信息,只能使用ReadConsoleInput函数: If the input buffer contains input events other than keyboard events (such as mouse events or window-resizing events), they are discarded. Those events can only be read by using the ReadConsoleInput function. 四。关闭Console 最后用完记得使用CloseHandle释放句柄,如: CloseHandle(hdlRead); CloseHandle(hdlWrite); 若想关闭Console,可以使用FreeConsole(); 需要注意的是,若用户直接点击cmd窗口的关闭按钮,整个程序(Application)将退出! 五。其他函数 这组函数还提供了其他有趣的功能,比如修改console的背景色,前景色等。更多函数请参见MSDN。 AllocConsole(); SetConsoleTitle("Lonefox love China"); //修改Console的标题 HANDLE hConsole = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE, //权限 FILE_SHARE_READ | FILE_SHARE_WRITE, //console的共享方式 NULL,//安全性设置,NULL默认即可 CONSOLE_TEXTMODE_BUFFER,//唯一值 NULL//保留 );//创建console的屏幕内容缓存,一个进程可以有多个ScreenBuffer SetConsoleActiveScreenBuffer(hConsole); //显示该buffer的内容 SetConsoleTextAttribute(hConsole, FOREGROUND_RED | BACKGROUND_GREEN); TCHAR c[] = {"Hello world!\n"}; WriteConsole(hConsole, c, sizeof(c), NULL, NULL); //在屏幕显示 CloseHandle(hConsole); FreeConsole(); 后记:本文仅学习笔记,非教科书。若有问题欢迎留言讨论!
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值