啊,这让我回想起好旧的日子。我在高中时也做过类似的事情 :-)
你将遇到性能问题。控制台 I/O,在 Windows 上特别慢。非常、非常慢(有时甚至比写入磁盘还要慢)。实际上,你会很快惊奇于其他工作对游戏循环延迟的影响如此之小,因为 I/O 倾向于支配其他所有操作。所以黄金法则就是尽可能地减少 I/O 操作。
首先,建议放弃 system("cls")
并改用调用实际的 Win32 控制台子系统函数来代替 cls
(docs):
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
void cls()
{
static const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;
COORD topLeft = { 0, 0 };
std::cout.flush();
if (!GetConsoleScreenBufferInfo(hOut, &csbi)) {
abort();
}
DWORD length = csbi.dwSize.X * csbi.dwSize.Y;
DWORD written;
FillConsoleOutputCharacter(hOut, TEXT(' '), length, topLeft, &written);
FillConsoleOutputAttribute(hOut, csbi.wAttributes, length, topLeft, &written);
SetConsoleCursorPosition(hOut, topLeft);
}
实际上,与其每次重新绘制整个“框架”,你最好一次只绘制(或通过用空格覆盖来擦除)单个字符:
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);
}
setCursorPosition(10, 5);
std::cout << "CHEESE";
setCursorPosition(10, 5);
std::cout 'W';
setCursorPosition(10, 9);
std::cout << 'Z';
setCursorPosition(10, 5);
std::cout << " ";
std::cout.flush();
请注意,这也消除了闪烁,因为在重新绘制之前不再需要完全清除屏幕 - 您可以仅更改需要更改的内容而无需进行中间清除,因此先前的帧会逐步更新并持久存在,直到完全更新为止。
我建议使用双缓冲技术:在内存中有一个缓冲区表示控制台屏幕的“当前”状态,最初填充为空格。然后有另一个缓冲区表示屏幕的“下一个”状态。您的游戏更新逻辑将修改“下一个”状态(就像现在对您的数组所做的那样)。在绘制帧时,请勿先擦除所有内容。相反,同时遍历两个缓冲区,并仅写出从上一个状态(此时的“当前”缓冲区包含上一个状态)发生的更改。然后,将“下一个”缓冲区复制到“当前”缓冲区中以准备下一帧。
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调用(这比许多单个字符写入的调用要便宜得多,但仍然与写入的字符数成比例地更昂贵)。
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 实际上通常会休眠更长时间,通常高达15毫秒,但它会防止 CPU 核心达到100% 的使用率并带来最小的额外延迟。
请注意,这不是“真正”的游戏处理方式;它们几乎总是在每一帧清除缓冲区并重新绘制所有内容。他们不会出现闪烁,因为他们在 GPU 上使用双缓冲的等效物,其中前一帧保持可见状态,直到新帧完全完成绘制。
奖励:您可以将颜色更改为
8 种不同的系统颜色 中的任何一种,背景也可以。
void setConsoleColour(unsigned short colour)
{
static const HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
std::cout.flush();
SetConsoleTextAttribute(hOut, colour);
}
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;