c++ - 不闪烁地更新控制台 - C++

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

最佳答案

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

您将遇到性能问题。控制台 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);
}

确实,与其每次都重新绘制整个“框架”,不如一次绘制(或删除,通过用空格覆盖它们)单个字符:

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

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

我建议使用双缓冲技术:在内存中有一个缓冲区代表控制台屏幕的“当前”状态,最初用空格填充。然后有另一个缓冲区代表屏幕的“下一个”状态。您的游戏更新逻辑将修改“下一个”状态(就像现在对您的 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);

您甚至可以更进一步,将更改的批量运行合并到单个 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 已经在缓冲写入了。但这是一个很好的例子(当底层系统中没有缓冲区时,它是一个常见的模式),所以我还是把它包括在内。

最后请注意,您可以将 sleep 时间减少到 1 毫秒。 Windows 实际上通常会休眠更长时间,通常长达 15 毫秒,但它会阻止您的 CPU 内核达到 100% 的使用率,并且额外延迟最少。

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

Bonus:您可以将颜色更改为 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;

关于c++ - 不闪烁地更新控制台 - C++,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34842526/

相关文章:

c++ - 指针可以指向一个值,指针值可以指向地址吗?

c++ - 从C++中的类声明继承

C++ 在函数中创建新对象

C++ fatal error C1001 : An internal error has occurred in the compiler with openMP

C++ 填充动态数组int

c++ - 检查共享库是否与二进制文件兼容?

c++ - 强制列表框更新

c++ - 显示未显示在 qt 中其他类的文本框中

c++ - 我如何使用类似 Java 的 C++ 枚举作为另一个类的成员变量?

c++ - 此类中的变量未命名类型 C++