Aggiorna la console senza sfarfallio - c++

Aggiorna la console senza sfarfallio - c++

Ah, questo riporta i bei vecchi tempi. Ho fatto cose simili al liceo :-)

Incontrerai problemi di prestazioni. L'I/O della console, specialmente su Windows, è lento. Molto, molto lento (a volte anche più lento della scrittura su disco). In effetti, ti stupirai rapidamente di quanto altro lavoro puoi fare senza che influisca sulla latenza del tuo loop di gioco, poiché l'I/O tenderà a dominare tutto il resto. Quindi la regola d'oro è semplicemente ridurre al minimo la quantità di I/O che fai, soprattutto.

Per prima cosa, suggerisco di sbarazzarsi di system("cls") e sostituiscilo con le chiamate alle effettive funzioni del sottosistema della console Win32 che cls avvolge (documenti):

#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 volta, è molto meglio disegnare (o cancellare, sovrascrivendoli con uno spazio) 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

Nota che questo elimina anche lo sfarfallio, poiché non è più necessario cancellare completamente lo schermo prima di ridisegnare:puoi semplicemente modificare ciò che deve essere modificato senza eseguire una cancellazione intermedia, quindi il frame precedente viene aggiornato in modo incrementale, persistendo fino a quando non è completamente attivo ad oggi.

Suggerisco di utilizzare una tecnica di doppio buffering:avere un buffer in memoria che rappresenti lo stato "corrente" dello schermo della console, inizialmente popolato di 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 il tuo battleField array in questo momento). Quando arriva il momento di disegnare la cornice, non cancellare prima tutto. Invece, esamina entrambi i buffer in parallelo e scrivi solo le modifiche dallo stato precedente (il buffer "corrente" a quel punto contiene lo stato precedente). Quindi, copia il buffer "successivo" nel buffer "corrente" per configurare il frame 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);

Puoi anche fare un ulteriore passo avanti ed eseguire in batch le modifiche insieme in un'unica chiamata I/O (che è significativamente più economica di molte chiamate per scritture di singoli caratteri, ma comunque proporzionalmente più costosa quanto più caratteri vengono 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, funzionerà molto più velocemente del primo ciclo; tuttavia in pratica probabilmente non farà la differenza poiché std::cout sta già memorizzando nel buffer le scritture comunque. Ma è un buon esempio (e un modello comune che si manifesta molto quando non c'è buffer nel sistema sottostante), quindi l'ho incluso comunque.

Infine, nota che puoi ridurre il sonno a 1 millisecondo. Windows in realtà spesso dormirà più a lungo, in genere fino a 15 ms, ma impedirà al core della CPU di raggiungere il 100% di utilizzo con un minimo di latenza aggiuntiva.

Nota che questo non è affatto il modo in cui i giochi "reali" fanno le cose; quasi sempre cancellano il buffer e ridisegnano tutto ogni fotogramma. Loro non subiscono sfarfallio perché usano l'equivalente di un doppio buffer sulla GPU, dove il frame precedente rimane visibile fino a quando il nuovo frame non è completamente disegnato.

Bonus :puoi cambiare il colore in uno qualsiasi degli 8 diversi colori del sistema e anche lo sfondo:

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;

system("cls") è la causa del tuo problema. Per aggiornare il frame, il tuo programma deve generare un altro processo e quindi caricare ed eseguire un altro programma. Questo è piuttosto costoso.cls cancella lo schermo, il che significa che per una piccola quantità di tempo (fino a quando il controllo non torna al processo principale) non viene visualizzato completamente nulla. Ecco da dove viene lo sfarfallio. Dovresti usare una libreria come ncurses che ti consente di visualizzare la "scena", quindi sposta la posizione del cursore su <0,0> senza modificare nulla sullo schermo e mostra nuovamente la tua scena "sopra" quella vecchia. In questo modo eviterai lo sfarfallio, perché la scena visualizzerà sempre qualcosa, senza il passaggio "schermo completamente vuoto".


Un metodo consiste nel scrivere i dati formattati in una stringa (o buffer), quindi bloccare la scrittura del buffer nella console.

Ogni chiamata a una funzione ha un sovraccarico. Prova a fare di più in una funzione. Nel tuo output, questo potrebbe significare molto testo per richiesta di output.

Ad esempio:

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

Le operazioni di I/O sono costose (dal punto di vista dell'esecuzione), quindi l'uso migliore è massimizzare i dati per richiesta di output.