C语言游戏开发闪屏解决办法--双缓冲技术

问题引入

基于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;
}

完结(欢迎补充)

  • 21
    点赞
  • 183
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
在C语言中,如果使用Windows API函数编写小游戏,可能会出现闪屏问题。这是因为在每次重绘窗口时,窗口背景会被擦除,然后重新绘制。为了解决这个问题,可以使用双缓冲技术。 双缓冲技术的基本思路是:在内存中创建一个与屏幕大小相同的位图,先将所有的绘制操作都在位图上完成,然后再一次性将整个位图绘制到屏幕上,从而避免了闪屏问题。 以下是使用双缓冲技术解决闪屏问题的示例代码: ```c #include <Windows.h> #define WIDTH 80 #define HEIGHT 25 int main() { // 获取控制台句柄和屏幕缓冲区句柄 HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE hBuffer = CreateConsoleScreenBuffer( GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL ); // 设置控制台窗口大小和缓冲区大小 COORD size = { WIDTH, HEIGHT }; SetConsoleScreenBufferSize(hBuffer, size); SMALL_RECT rc = { 0, 0, WIDTH - 1, HEIGHT - 1 }; SetConsoleWindowInfo(hBuffer, TRUE, &rc); // 将缓冲区设置为活动缓冲区 SetConsoleActiveScreenBuffer(hBuffer); // 隐藏光标 CONSOLE_CURSOR_INFO cci; GetConsoleCursorInfo(hOut, &cci); cci.bVisible = FALSE; SetConsoleCursorInfo(hOut, &cci); // 在缓冲区中绘制游戏界面 while (1) { // 创建位图并获取设备上下文 HBITMAP hBitmap = CreateCompatibleBitmap(hOut, WIDTH, HEIGHT); HDC hdcBitmap = CreateCompatibleDC(hOut); SelectObject(hdcBitmap, hBitmap); // 绘制游戏界面到位图上 // ... // 将位图绘制到缓冲区上 BitBlt( GetStdHandle(STD_OUTPUT_HANDLE), 0, 0, WIDTH, HEIGHT, hdcBitmap, 0, 0, SRCCOPY ); // 释放设备上下文和位图资源 DeleteDC(hdcBitmap); DeleteObject(hBitmap); } return 0; } ``` 在这个示例代码中,我们首先创建了一个与屏幕大小相同的缓冲区,然后将其设置为活动缓冲区。在游戏循环中,我们先在内存中创建一个位图,并将游戏界面绘制到位图上,最后再将位图绘制到缓冲区上。这样就避免了每次重绘窗口时的闪屏问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值