更新控制台而不闪烁 - C++

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/34842526/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 14:26:34  来源:igfitidea点击:

Update console without flickering - c++

c++

提问by Persistence

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?

问题是每当它更新框架时,整个控制台都在闪烁。有没有办法解决这个问题?

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

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

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

回答by Cameron

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

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

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.

你会遇到性能问题。控制台 I/O,尤其是在 Windows 上,速度很慢。非常非常慢(有时甚至比写入磁盘还慢)。事实上,您很快就会惊奇地发现,在不影响游戏循环延迟的情况下还能做多少其他工作,因为 I/O 往往会主导其他一切。所以黄金法则就是尽量减少你做的 I/O 数量,最重要的是。

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

首先,我建议去掉system("cls")并将其替换为对实际的 Win32 控制台子系统函数的调用,这些函数cls包含 ( 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.

请注意,这也消除了闪烁,因为在重绘之前不再需要完全清除屏幕——您可以简单地更改需要更改的内容,而无需进行中间清除,因此前一帧会逐步更新,一直持续到完全清除迄今为止。

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 battleFieldarray 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 changesfrom 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.

我建议使用双缓冲技术:在内存中有一个缓冲区代表控制台屏幕的“当前”状态,最初用空格填充。然后有另一个缓冲区代表屏幕的“下一个”状态。您的游戏更新逻辑将修改“下一个”状态(就像现在对您的battleField阵列所做的一样)。当需要绘制框架时,不要先擦除所有内容。相反,并行通过两个缓冲区,写出前一个状态的变化(此时的“当前”缓冲区包含前一个状态)。然后,将“下一个”缓冲区复制到“当前”缓冲区中以设置下一帧。

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

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).

您甚至可以更进一步,将批量更改一起运行到单个 I/O 调用中(这比许多单个字符写入调用便宜得多,但写入的字符越多,成本仍成比例增加)。

// 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);

In theory, that will run a lot faster than the first loop; however in practice it probably won't make a difference since std::coutis 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.

理论上,这将比第一个循环运行得快得多;然而实际上它可能不会有什么不同,因为std::cout无论如何已经在缓冲写入。但这是一个很好的例子(并且当底层系统中没有缓冲区时会出现很多常见的模式),所以我还是把它包括在内。

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.

最后,请注意,您可以将睡眠时间减少到 1 毫秒。无论如何,Windows 无法真正休眠少于 10-15 毫秒,但它会阻止您的 CPU 核心达到 100% 使用率,同时将额外的延迟降至最低。

Note that this not at all the way "real" games do things; they almost always clear the buffer and redraw everything every frame. Theydon'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.

请注意,这完全不是“真正的”游戏做事的方式;他们几乎总是清除缓冲区并每帧重绘所有内容。它们不会闪烁,因为它们在 GPU 上使用等效的双缓冲区,其中前一帧保持可见,直到新帧完全绘制完成。

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

奖励:您可以将颜色更改为8 种不同系统颜色中的任何一种,也可以将背景更改为:

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;

回答by nsilent22

system("cls")isthe cause of your problem. For updating frame your program has to spawn another process and then load and execute another program. This is quite expensive. clsclears your screen, which means for a small amount of the time (until control returns to your main process) it displays completely nothing. That's where flickering comes from. You should use some library like ncurseswhich allows you to display the "scene", then move your cursor position to <0,0> without modifying anything on the screenand redisplay your scene "over" the old one. This way you'll avoid flickering, because your scene will always display something, without 'completely blank screen' step.

system("cls")你的问题的原因。为了更新框架,您的程序必须生成另一个进程,然后加载并执行另一个程序。这是相当昂贵的。 cls清除您的屏幕,这意味着在一小段时间内(直到控制权返回到您的主进程)它完全不显示任何内容。这就是闪烁的来源。您应该使用一些类似的库ncurses,它允许您显示“场景”,然后将光标位置移动到 <0,0>而不修改屏幕上的任何内容,然后在旧场景“上方”重新显示您的场景。这样您就可以避免闪烁,因为您的场景将始终显示某些内容,而无需“完全空白屏幕”步骤。

回答by Thomas Matthews

One method is to write the formatted data to a string (or buffer) then block write the buffer to the console.

一种方法是将格式化数据写入字符串(或缓冲区),然后将缓冲区写入控制台。

Every call to a function has an overhead. Try go get more done in a function. In your Output, this could mean a lot of text per output request.

对函数的每次调用都有开销。尝试在函数中完成更多工作。在您的输出中,这可能意味着每个输出请求有很多文本。

For example:

例如:

static char buffer[2048];
char * p_next_write = &buffer[0];
for (int y = 0; y < MAX_Y; y++)
{
    for (int x = 0; x < MAX_X; x++)
    {
        *p_next_write++ = battleField[x][y];
    }
    *p_next_write++ = '\n';
}
*p_next_write = '##代码##'; // "Insurance" for C-Style strings.
cout.write(&buffer[0], std::distance(p_buffer - &buffer[0]));

I/O operations are expensive (execution-wise), so the best use is to maximize the data per output request.

I/O 操作很昂贵(执行方面),因此最好的用途是最大化每个输出请求的数据。