更新控制台而不会闪烁-C ++ [英] Update console without flickering - c++

查看:85
本文介绍了更新控制台而不会闪烁-C ++的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试制作一个控制台侧滚动射击游戏,我知道这不是理想的选择,但是我给自己带来了一些挑战。

I'm attempting to make a console side scrolling shooter, I know this isn't the ideal medium for it but I set myself a bit of a challenge.

问题在于,每当更新框架时,整个控制台都会闪烁。有什么办法解决这个问题?

The problem is that whenever it updates the frame, the entire console is flickering. Is there any way to get around this?

我使用了一个数组来保存所有需要输出的字符,这是我的 updateFrame 函数。是的,我知道 system( cls)是懒惰的,但是除非是引起问题的原因,否则我不会为此而大惊小怪。

I have used an array to hold all of the necessary characters to be output, here is my updateFrame function. Yes, I know system("cls") is lazy, but unless that's the cause of problem I'm not fussed for this purpose.

void updateFrame()
{
system("cls");
updateBattleField();
std::this_thread::sleep_for(std::chrono::milliseconds(33));
for (int y = 0; y < MAX_Y; y++)
{
    for (int x = 0; x < MAX_X; x++)
    {
        std::cout << battleField[x][y];
    }
    std::cout << std::endl;
}
}


推荐答案

Ah ,这带回了过去的美好时光。我在高中时做过类似的事情:-)

Ah, this brings back the good old days. I did similar things in high school :-)

您将遇到性能问题。控制台I / O速度很慢,尤其是在Windows上。非常非常慢(有时甚至比写入磁盘还要慢)。实际上,由于I / O往往会主导其他所有事情,因此您很快会惊讶于在不影响游戏循环延迟的情况下还可以做多少其他工作。因此,黄金法则仅仅是将您要做的I / O数量降到最低,而不是其他。

You're going to run into performance problems. Console I/O, especially on Windows, is slow. Very, very slow (sometimes slower than writing to disk, even). In fact, you'll quickly become amazed how much other work you can do without it affecting the latency of your game loop, since the I/O will tend to dominate everything else. So the golden rule is simply to minimize the amount of I/O you do, above all else.

首先,我建议摆脱 system( cls)并将其替换为对 cls 包装的实际Win32控制台子系统函数的调用(文档):

First, I suggest getting rid of the system("cls") and replace it with calls to the actual Win32 console subsystem functions that cls wraps (docs):

#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

void cls()
{
    // Get the Win32 handle representing standard output.
    // This generally only has to be done once, so we make it static.
    static const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);

    CONSOLE_SCREEN_BUFFER_INFO csbi;
    COORD topLeft = { 0, 0 };

    // std::cout uses a buffer to batch writes to the underlying console.
    // We need to flush that to the console because we're circumventing
    // std::cout entirely; after we clear the console, we don't want
    // stale buffered text to randomly be written out.
    std::cout.flush();

    // Figure out the current width and height of the console window
    if (!GetConsoleScreenBufferInfo(hOut, &csbi)) {
        // TODO: Handle failure!
        abort();
    }
    DWORD length = csbi.dwSize.X * csbi.dwSize.Y;

    DWORD written;

    // Flood-fill the console with spaces to clear it
    FillConsoleOutputCharacter(hOut, TEXT(' '), length, topLeft, &written);

    // Reset the attributes of every character to the default.
    // This clears all background colour formatting, if any.
    FillConsoleOutputAttribute(hOut, csbi.wAttributes, length, topLeft, &written);

    // Move the cursor back to the top left for the next sequence of writes
    SetConsoleCursorPosition(hOut, topLeft);
}

实际上,您不必每次都重新绘制整个框架,一次绘制(或通过用空格覆盖来擦除)单个字符会更好:

Indeed, instead of redrawing the entire "frame" every time, you're much better off drawing (or erasing, by overwriting them with a space) individual characters at a time:

// x is the column, y is the row. The origin (0,0) is top-left.
void setCursorPosition(int x, int y)
{
    static const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    std::cout.flush();
    COORD coord = { (SHORT)x, (SHORT)y };
    SetConsoleCursorPosition(hOut, coord);
}

// Step through with a debugger, or insert sleeps, to see the effect.
setCursorPosition(10, 5);
std::cout << "CHEESE";
setCursorPosition(10, 5);
std::cout 'W';
setCursorPosition(10, 9);
std::cout << 'Z';
setCursorPosition(10, 5);
std::cout << "     ";  // Overwrite characters with spaces to "erase" them
std::cout.flush();
// Voilà, 'CHEESE' converted to 'WHEEZE', then all but the last 'E' erased

请注意,这也消除了闪烁,因为不再需要在重新绘制之前完全清除屏幕-您可以简单地更改需要更改的内容而无需进行中间清除,因此前一帧将进行增量更新,

Note that this eliminates the flicker, too, since there's no longer any need to clear the screen completely before redrawing -- you can simply change what needs changing without doing an intermediate clear, so the previous frame is incrementally updated, persisting until it's completely up to date.

我建议使用双缓冲技术:在内存中有一个缓冲区代表控制台屏幕的当前状态,最初到处都是空格。然后有另一个缓冲区代表屏幕的下一个状态。您的游戏更新逻辑将修改下一个状态(就像现在对您的 battleField 数组所做的一样)。当需要绘制框架时,请不要先擦除所有内容。取而代之的是,并行浏览两个缓冲区,只写出先前状态的更改 (此时的当前缓冲区包含先前状态)。然后,将下一个缓冲区复制到当前缓冲区中,以设置下一个帧。

I suggest using a double-buffering technique: Have one buffer in memory that represents the "current" state of the console screen, initially populated with spaces. Then have another buffer that represents the "next" state of the screen. Your game update logic will modify the "next" state (exactly like it does with your battleField array right now). When it comes time to draw the frame, don't erase everything first. Instead, go through both buffers in parallel, and write out only the changes from the previous state (the "current" buffer at that point contains the previous state). Then, copy the "next" buffer into the "current" buffer to set up for your next frame.

char prevBattleField[MAX_X][MAX_Y];
std::memset((char*)prevBattleField, 0, MAX_X * MAX_Y);

// ...

for (int y = 0; y != MAX_Y; ++y)
{
    for (int x = 0; x != MAX_X; ++x)
    {
        if (battleField[x][y] == prevBattleField[x][y]) {
            continue;
        }
        setCursorPosition(x, y);
        std::cout << battleField[x][y];
    }
}
std::cout.flush();
std::memcpy((char*)prevBattleField, (char const*)battleField, MAX_X * MAX_Y);

您甚至可以更进一步,将更改的批处理操作合并成一个I / O调用(这比许多调用单个字符的调用要便宜得多,但写的字符越多,调用成本就成比例地增加。)

You can even go one step further and batch runs of changes together into a single I/O call (which is significantly cheaper than many calls for individual character writes, but still proportionally more expensive the more characters are written).

// Note: This requires you to invert the dimensions of `battleField` (and
// `prevBattleField`) in order for rows of characters to be contiguous in memory.
for (int y = 0; y != MAX_Y; ++y)
{
    int runStart = -1;
    for (int x = 0; x != MAX_X; ++x)
    {
        if (battleField[y][x] == prevBattleField[y][x]) {
            if (runStart != -1) {
                setCursorPosition(runStart, y);
                std::cout.write(&battleField[y][runStart], x - runStart);
                runStart = -1;
            }
        }
        else if (runStart == -1) {
            runStart = x;
        }
    }
    if (runStart != -1) {
        setCursorPosition(runStart, y);
        std::cout.write(&battleField[y][runStart], MAX_X - runStart);
    }
}
std::cout.flush();
std::memcpy((char*)prevBattleField, (char const*)battleField, MAX_X * MAX_Y);

理论上,这比第一个循环快得多;但是实际上,由于 std :: cout 已经在缓冲写操作了,所以它可能不会起作用。但这是一个很好的例子(当基础系统中没有缓冲区时,一个常见的模式会显示很多),因此无论如何我都将其包括在内。

In theory, that will run a lot faster than the first loop; however in practice it probably won't make a difference since std::cout is already buffering writes anyway. But it's a good example (and a common pattern that shows up a lot when there is no buffer in the underlying system), so I included it anyway.

最后,请注意您可以将睡眠时间减少到1毫秒。 Windows不能真正睡眠少于10-15ms,但是它将阻止您的CPU核心使用率达到100%,同时又将等待时间降到最低。

Finally, note that you can reduce your sleep to 1 millisecond. Windows cannot really sleep for less than 10-15ms anyway, but it will prevent your CPU core from reaching 100% usage with a minimum of additional latency.

请注意,从根本上说,真实游戏是在做事;他们几乎总是清除缓冲区并在每一帧重绘所有内容。 它们不会闪烁,因为它们在GPU上使用了等同于双缓冲区的功能,在该位置上一帧保持可见,直到新帧完全绘制完毕。

Note that this not at all the way "real" games do things; they almost always clear the buffer and redraw everything every frame. They don't get flickering because they use the equivalent of a double-buffer on the GPU, where the previous frame stays visible until the new frame is completely finished being drawn.

奖金:您可以将颜色更改为 8种不同的系统颜色,背景也一样:

Bonus: You can change the colour to any of 8 different system colours, and the background too:

void setConsoleColour(unsigned short colour)
{
    static const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    std::cout.flush();
    SetConsoleTextAttribute(hOut, colour);
}

// Example:
const unsigned short DARK_BLUE = FOREGROUND_BLUE;
const unsigned short BRIGHT_BLUE = FOREGROUND_BLUE | FOREGROUND_INTENSITY;

std::cout << "Hello ";
setConsoleColour(BRIGHT_BLUE);
std::cout << "world";
setConsoleColour(DARK_BLUE);
std::cout << "!" << std::endl;

这篇关于更新控制台而不会闪烁-C ++的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆