Update console without flickering - C ++

I'm trying to make a shooter for scrolling in the console, I know that this is not an ideal tool for this, but I posed several problems for myself.

The problem is that whenever it updates the frame, the whole console flickers. Is there any way around this?

I used an array to store all the necessary characters for output, here is my updateFrame function. Yes, I know that system("cls") lazy, but if this is not the problem, I did not fuss for this purpose.

 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; } } 
+7
c ++
source share
3 answers

Ah, that brings back the good old days. I did similar things in high school :-)

You will run into performance issues. Console I / O, especially on Windows, is slow. Very, very slow (sometimes slower than writing to disk, even). In fact, you will quickly be surprised how much more work you can do without affecting the latency of your game cycle, as I / O will dominate the rest. Thus, the golden rule simply minimizes the number of I / O operations that you do, above all.

First, I suggest getting rid of system("cls") and replacing it with calls to the actual Win32 console console functions that cls wraps ( 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); } 

In fact, instead of redrawing the entire “frame” each time, you are much better off drawing (or erasing, overwriting them with a space) individual characters at a time:

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

Please note that this also eliminates flickering, since there is no longer any need to completely clear the screen before redrawing - you can just change what you need to change without intermediate clearing, so the previous frame is gradually updated, remaining until it is completely updated .

I suggest using the double buffering method: to have one buffer in memory that represents the "current" state of the console screen, initially filled with spaces. Then add another buffer that represents the “next” state of the screen. The logic of updating your game will change the “next” state (just like with your battleField array). When the time comes to draw a frame, do not delete everything first. Instead, go through both buffers in parallel and write only the changes from the previous state (the “current” buffer at this point contains the previous state). Then copy the “next” buffer to the “current” buffer to configure for the next frame.

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

You can even take one more step and batch run the changes together into one I / O call (which is much cheaper than many calls for writing individual characters, but still proportionally more expensive, the more characters are written).

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

Theoretically, this will work much faster than the first cycle; however, in practice, this probably will not make any difference, since std::cout already buffers the record anyway. But this is a good example (and a generic pattern that shows a lot when there is no buffer in the base system), so I turned it on anyway.

Finally, note that you can reduce your sleep to 1 millisecond. In any case, Windows cannot sleep normally for less than 10-15 ms, but this will not allow your processor to achieve 100% utilization with minimal additional delay.

Note that this is not at all what the "real" games do; they almost always clear the buffer and redraw all frames. They do not flicker because they use the equivalent of a double buffer on the GPU, where the previous frame remains visible until the new frame is completely completed.

Bonus You can change the color to any of 8 different system colors , as well as the background:

 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; 
+14
source share

system("cls") is causing your problem. To update the frame, your program must start another process, and then download and execute another program. It is quite expensive. cls clears the screen, which means that for a short period of time (until control returns to your main process), it will not display anything. Where flicker comes from. You should use some library, such as ncurses , which allows you to display the "scene" and then move the cursor position to <0,0> without changing anything on the screen and re-display the scene "on top" of the Old. This way you avoid flickering because your scene will always display something without the “completely blank screen” step.

+6
source share

One way is to write formatted data to a string (or buffer), and then block writing the buffer to the console.

Each function call has overhead. Try to start the function. In your output, this can mean a lot of text for each exit request.

For example:

 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 operations are expensive (to execute), so it’s best to use the data for each exit request.

+1
source share

All Articles