Ah, questo riporta indietro i bei vecchi tempi. Ho fatto cose simili alle superiori :-)
Hai problemi di prestazioni. L'I/O della console, specialmente su Windows, è lento. Molto, molto lento (a volte più lento della scrittura su disco, anche). In effetti, ti stupirai rapidamente di quanto altro lavoro puoi fare senza influire sulla latenza del tuo ciclo di gioco, dal momento che l'I/O tenderà a dominare tutto il resto. Quindi la regola d'oro è semplicemente quella di minimizzare la quantità di I/O che fai, sopra ogni altra cosa.
In primo luogo, vi suggerisco di sbarazzarsi del system("cls")
e sostituirlo con chiamate alle effettive funzioni di sottosistema console Win32 che cls
avvolge (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);
}
Infatti, invece di ridisegnare l'intero "frame" ogni tempo, si sta molto meglio disegno (o la cancellazione, da loro sovrascrivendo con uno spazio) i singoli caratteri alla volta:
// 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
si noti che questo elimina il tremolio, troppo, dal momento che ci Non è più necessario cancellare completamente lo schermo prima di ridisegnare - puoi semplicemente cambiare ciò che è necessario cambiare senza fare una cancellazione intermedia, quindi il frame precedente viene aggiornato in modo incrementale, persistendo finché non è completamente aggiornato.
Suggerisco di utilizzare una tecnica di doppio buffering: avere un buffer in memoria che rappresenta lo stato "corrente" dello schermo della console, inizialmente popolato con spazi. Quindi avere un altro buffer che rappresenta lo stato "successivo" dello schermo. La logica di aggiornamento del gioco modificherà lo stato "successivo" (esattamente come fa con l'array battleField
in questo momento). Quando arriva il momento di disegnare la cornice, non cancellare prima tutto. Invece, passare attraverso entrambi i buffer in parallelo e scrivere solo le modifiche dallo stato precedente (il buffer "corrente" in quel punto contiene lo stato precedente). Quindi, copia il buffer "successivo" nel buffer "corrente" per impostare il fotogramma successivo.
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);
Si può anche fare un passo avanti e corre batch di modifiche insieme in una singola chiamata di I/O (che è molto più economico rispetto a molti inviti a carattere individuale, scrive, ma ancora proporzionalmente più costosi i più caratteri sono scritti).
// 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 teoria, sarà eseguito molto più velocemente del primo ciclo; tuttavia, in pratica, probabilmente non farà alcuna differenza dal momento che lo std::cout
sta già eseguendo il buffering delle scritture comunque. Ma è un buon esempio (e un modello comune che appare molto quando non c'è un buffer nel sistema sottostante), quindi l'ho incluso comunque.
Infine, si noti che è possibile ridurre il sonno a 1 millisecondo. Windows non può in realtà dormire per meno di 10-15 ms, ma impedirà al core della CPU di raggiungere il 100% di utilizzo con un minimo di latenza aggiuntiva.
Si noti che questo non è affatto il modo in cui i "veri" giochi fanno le cose; quasi sempre cancellano il buffer e ridisegnano tutto ogni frame. Gli non hanno lo sfarfallio perché utilizzano l'equivalente di un doppio buffer sulla GPU, in cui il fotogramma precedente rimane visibile fino a quando il nuovo fotogramma non viene completamente completato.
Bonus: È possibile cambiare il colore a qualsiasi di 8 different system colours, e lo sfondo troppo:
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;
ci sono duplicati per questo. Se nulla, dovresti interrompere la discussione ** dopo ** stampi qualcosa, non quando lo schermo è stato cancellato, ma suppongo che lo sfarfallio sarà ancora visibile su Windows. – LogicStuff
dovresti provare a usare ncurses, ti permette di fare tutto ciò che vuoi nella shell. –
@LogicStuff: duplicati o meno, questa è una domanda a cui ho voluto rispondere per un tempo molto lungo, poiché ho passato molto tempo a trafficare con cose simili. Ci sono modi per evitare lo sfarfallio. – Cameron