更新控制台没有闪烁 - c ++

时间:2016-01-17 19:16:45

标签: c++

我试图让控制台侧面滚动射手,我知道这不是理想的媒介,但我给自己设置了一些挑战。

问题在于,无论何时更新帧,整个控制台都会闪烁。有没有办法解决这个问题?

我使用了一个数组来保存要输出的所有必要字符,这是我的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;
}
}

3 个答案:

答案 0 :(得分:23)

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

您将遇到性能问题。控制台I / O,特别是在Windows上,速度很慢。非常非常慢(有时甚至比写入磁盘慢)。实际上,如果不影响游戏循环的延迟,你很快就会惊讶于你可以做多少其他工作,因为I / O将倾向于支配其他所有东西。因此,黄金法则只是最大限度地减少您所做的I / O数量,最重要的是。

首先,我建议删除system("cls")并将其替换为cls包装的实际Win32控制台子系统函数(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);
}

确实,而不是重新绘制整个框架&#34;每次,你最好不要一次性绘制(或用空格覆盖它们)个别角色:

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

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

我建议使用双缓冲技术:在内存中有一个缓冲区代表&#34;当前&#34;控制台屏幕的状态,最初填充空格。然后有另一个缓冲区代表&#34; next&#34;屏幕状态。您的游戏更新逻辑将修改&#34; next&#34;状态(与现在的battleField数组完全一样)。在绘制框架时,不要先擦除所有内容。相反,并行浏览两个缓冲区,并仅从前一个状态写出更改(&#34;当前&#34;此时缓冲区包含先前的状态)。然后,复制&#34; next&#34;缓冲到&#34;当前&#34;缓冲区以设置下一帧。

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调用(这比单个字符写入的许多调用要便宜得多,但编写的字符越多,成本也越高)。

// 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已经缓冲了写入。但它是一个很好的例子(当底层系统中没有缓冲区时,会出现很多常见模式),所以无论如何我都把它包括在内。

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

请注意,这完全没有&#34;真实&#34;游戏做事;他们几乎总是清除缓冲区并重新绘制每一帧的所有内容。 他们没有闪烁,因为他们在GPU上使用相当于双缓冲区的地方,前一帧保持可见,直到新帧完全被绘制完毕。

奖金:您可以将颜色更改为8 different system colours中的任何一种,以及背景颜色:

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;

答案 1 :(得分:6)

system("cls") 是导致问题的原因。对于更新框架,您的程序必须生成另一个进程,然后加载并执行另一个程序。这非常昂贵。 cls清除您的屏幕,这意味着在很短的时间内(直到控制权返回主进程)它完全没有显示任何内容。这就是闪烁的来源。 您应该使用像ncurses这样的库,它允许您显示“场景”,然后将光标位置移动到&lt; 0,0&gt; 不修改屏幕上的任何内容,并将旧场景“重叠”过来。这样你就可以避免闪烁,因为你的场景总会显示一些东西,没有“完全空白的屏幕”步骤。

答案 2 :(得分:1)

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

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

例如:

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 = '\0'; // "Insurance" for C-Style strings.
cout.write(&buffer[0], std::distance(p_buffer - &buffer[0]));

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