2016-01-17 17 views
7

Sto tentando di creare uno sparatutto a scorrimento laterale console, so che questo non è il mezzo ideale per questo, ma mi pongo una piccola sfida.Console di aggiornamento senza sfarfallio - C++

Il problema è che ogni volta che si aggiorna il frame, l'intera console sfarfalla. C'è un modo per aggirarlo?

Ho usato un array per contenere tutti i caratteri necessari da stampare, ecco la mia funzione updateFrame. Sì, lo so system("cls") è pigro, ma a meno che non sia la causa del problema, non sono preoccupato per questo scopo.

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; 
} 
} 
+0

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

+2

dovresti provare a usare ncurses, ti permette di fare tutto ciò che vuoi nella shell. –

+0

@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

risposta

14

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; 
+0

Non sono riuscito a implementare tutto ciò che hai suggerito finora, tuttavia il metodo di doppio buffering sembra aver risolto lo sfarfallio (sono tentato di aggiornare anche quel buffer su un thread separato in modo che sia ulteriormente ottimizzato su macchine multicore - Penso che funzioni). Tuttavia, non sono del tutto sicuro di cosa stia: memcpy ((char *), campo di battaglia precedente, (char const *) battleField, MAX_X * MAX_Y); 'cosa, o cosa intendi per essere contigui in memoria, così ulteriormente chiarimento potrebbe aiutare. Grazie comunque per tutto! –

+1

@James: non aggiungere thread al mix se puoi aiutarlo - probabilmente non migliorerà le prestazioni, ma complicherà enormemente il tuo codice e potrebbe introdurre bug non deterministici sottili (condizioni di gara). La 'memcpy' copia solo tutti i byte da' battleField' a 'prevBattleField' in modo che i due contengano valori identici dopo (impostando' prevBattleField' per il fotogramma successivo). Una regione 'contigua' significa solo che i byte sono tutti l'uno accanto all'altro in memoria.Con 'a [x] [y]', tutti i byte per una data colonna sono contigui, ma si desidera che tutti i byte di una data riga siano contigui ('a [y] [x]'). – Cameron

+0

Questo è ... fantastico. Sto imparando molto da questa risposta – Ben

1

Un metodo consiste nel scrivere i dati formattati in una stringa (o nel buffer), quindi bloccare il buffer di scrittura 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])); 

I/O operazioni sono costose (esecuzione-saggio), quindi l'uso migliore è quello di massimizzare i dati per richiesta di uscita.

6

system("cls")è la causa del problema. Per l'aggiornamento del frame, il 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 (finché il controllo non ritorna al processo principale) non visualizza completamente nulla. Ecco da dove viene lo sfarfallio. Dovresti usare qualche libreria come ncurses che ti permette di visualizzare la "scena", quindi spostare la posizione del cursore su < 0,0>senza modificare nulla sullo schermo e visualizzare nuovamente la scena "sopra" la vecchia. In questo modo eviterai lo sfarfallio, perché la tua scena mostrerà sempre qualcosa, senza passaggio "schermo completamente vuoto".

+1

Penso tu voglia dire costoso? –

+0

@JeffDavenport: certo che hai ragione. Grazie, risolto. – nsilent22

Problemi correlati