3

I'm trying to build a simple snake game that runs in the console:

#include <iostream> #include <windows.h> #define WIDTH 30 #define HEIGHT 50 void board(){ // COORD coord; for(int i = 0; i < WIDTH; i++){ // Draw the right wall cout << "#"; for(int j = 0; j < HEIGHT; j++){ // coord.X = j; // coord.Y = i; // SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); // Draw the top && bottom wall if(i == 0 || i == WIDTH -1){ cout << "#"; } // Draw the the spaces in between if(i > 0 && i < WIDTH - 1 ){ cout << " "; } // Draw the left wall if(j == HEIGHT - 1){ cout << "#"; } } cout << "\n"; } } int main(){ while(true){ board(); } return 0; } 

While drawing the game board (walls and spaces) in an infinite loop, the entire console flickers continuously. I suspect this is because I'm re-drawing the whole board every time, but I'm not quite sure how to resolve the issue.

Why does this flickering happen and what strategies I might use to reduce or eliminate it?

Additional Information

  • Issue: The console flickers because the entire board is continuously redrawn in an infinite loop.
  • Environment: Windows Console using <windows.h>.
  • Goal: Improve the drawing method to reduce flickering, perhaps by only updating parts of the screen or using a different technique for drawing.
10
  • 5
    Firstly don't use cout. Secondly create a 'buffer' (which is invisble), do all your drawing to that buffer, then when you are finished swap the buffer you've just created with the buffer that is currently being displayed. That way the whole modifed screen gets displayed at once without any flickering. You can find some documentation here Commented Apr 14 at 11:52
  • 2
    You can redraw only portions of the board that change, e.g. the new head position of the snake and clearing the previous tail position. This should drastically improve flickering. Commented Apr 14 at 11:53
  • 3
    Sounds like a job for ncurses (if you're ok with using msys2) Commented Apr 14 at 12:10
  • 2
    Create an off-screen buffer (CreateConsoleScreenBuffer), perform all rendering to it, and then make it the active screen buffer (SetConsoleActiveScreenBuffer). Commented Apr 14 at 16:00
  • 1
    @MohamedMajilan: You may find this answer of mine to another Stack Overflow question helpful. You will probably want to use the functions WriteConsole or WriteConsoleOutput instead of cout, and use SetConsoleCursorPosition. Writing more than one character at once to the console will probably be better for performance. Commented Apr 15 at 13:15

5 Answers 5

4

Don't cout one character at a time. The cost of outputting one character to the console is almost the same as the cost of outputting 4,000 characters.

Create a string buffer, put all your content in it(and put \n at the end of each line and \0 at the end of the last line), and then output the entire buffer.

As long as the string buffer size is less than 4096, the console only needs to be refreshed once during the output process.

 char buffer[(WIDTH + 1) * HEIGHT] = {}; for (int i = 0; i < (WIDTH + 1) * HEIGHT-1;++i) { buffer[i] = ' '; } for (int i = 0; i < WIDTH; ++i) { buffer[i] = '#'; } for (int i = 0; i < WIDTH; ++i) { buffer[(WIDTH + 1) * (HEIGHT-1)+i] = '#'; } for (int i = 1; i < HEIGHT - 1; ++i) { buffer[i * (WIDTH + 1)] = '#'; } for (int i = 1; i < HEIGHT - 1; ++i) { buffer[i * (WIDTH + 1)+ WIDTH-1] = '#'; } for (int i = 0; i < HEIGHT-1; ++i) { buffer[i* (WIDTH + 1)+ WIDTH] = '\n'; } std::cout << buffer; 

Remember, IO operations are many times slower than accessing memory. In the absence of buffering, outputting a single character twice can cost more than outputting thousands of characters once.

Sign up to request clarification or add additional context in comments.

Comments

1

The only thing you can do here is draw only the parts that have changed. You should have two versions of the board: the actual version and the previous version.

Your actual algorithm is roughly this:

  1. compute the new state of board
  2. erase the screen
  3. draw board
  4. restart at 1

And this is what you should do:

  1. copy board to previous_board
  2. compute the new state of board
  3. draw the parts of board that are different from the parts in previous_board.
  4. restart at 1

Comments

0

Resizing the console window

The most complicated part of the problem is probably to resize the console screen buffer and scroll window to the desired dimensions. This is because the functions SetConsoleWindowInfo and SetConsoleScreenBufferSize both have strong restrictions on how they can be used. You

  • cannot set the dimensions of the scroll window to be larger than any of the dimensions of the scroll buffer, and
  • cannot you cannot set the dimensions of the scroll buffer to be smaller than any of the dimensions of the scroll window.

This means that you must first shrink the dimensions of the scroll window that are too large, then set the scroll buffer to the desired size, and then expand the dimensions of the scroll window that are too large. See the following questions for further information:

There does exist a function called SetConsoleScreenBufferInfoEx which appears to be able to set both the size of the scroll window and the size of the screen buffer in a single function call, so it should theoretically be able to simplify the complicated situation mentioned above. However, this function is not well documented and does not work as expected with me, so I am not using it in my program below.

In my program below, I have created my own function ResizeConsoleWindow which according to my tests, appears to resize the console window to the desired size reliably.

Solving the flickering problem

My guess is that your flickering problem is caused by scrolling, which happens automatically when you write past the last line that is visible on the screen.

To prevent this from happening, I recommend that you do not use the stream objects provided by the standard C++ library, but rather use low-level console output functions, which gives you full control of the area to which you write the output. That way, you can prevent scrolling from occurring.

For representing the content of the console, you can create a 2D array of CHAR_INFO elements. This array can be initialized with the function ReadConsoleOutput and you can write the content of this array to the console using the function WriteConsoleOutput.

I have rewritten your code as described above:

#define WIN32_LEAN_AND_MEAN #include <Windows.h> #include <iostream> #include <stdexcept> #include <cassert> #define WIDTH 50 #define HEIGHT 30 CHAR_INFO board[HEIGHT][WIDTH]; static void UpdateBoard(); static void DrawBoard( HANDLE hConsole ); static void InitializeBoard( HANDLE hConsole ); static void ResizeConsoleWindow( HANDLE hConsole, int width, int height ); static void DisableBlinkingCursor( HANDLE hConsole ); int main() try { // get handle to console HANDLE hConsole = GetStdHandle( STD_OUTPUT_HANDLE ); // resize console window and buffer to desired dimensions ResizeConsoleWindow( hConsole, WIDTH, HEIGHT ); // disable blinking of the cursor DisableBlinkingCursor( hConsole ); // initialize the 2D board array InitializeBoard( hConsole ); // update and draw board in infinite loop for (;;) { UpdateBoard(); DrawBoard( hConsole ); } } catch ( const std::runtime_error &e ) { std::cerr << "Runtime error: " << e.what() << '\n'; } static void UpdateBoard() { // draw the lines with the top wall for ( int i = 0; i < WIDTH; i++ ) { board[0][i].Char.AsciiChar = '#'; } // draw the left and right walls, clearing the space in between for ( int i = 1; i < HEIGHT - 1; i++ ) { board[i][0].Char.AsciiChar = '#'; for ( int j = 1; j < WIDTH - 1; j++ ) { board[i][j].Char.AsciiChar = ' '; } board[i][WIDTH-1].Char.AsciiChar = '#'; } // draw the bottom wall for ( int i = 0; i < WIDTH; i++ ) { board[HEIGHT-1][i].Char.AsciiChar = '#'; } } static void DrawBoard( HANDLE hConsole ) { SMALL_RECT rect = { 0, 0, WIDTH - 1, HEIGHT - 1 }; if ( ! WriteConsoleOutputA( hConsole, &board[0][0], COORD{ WIDTH, HEIGHT }, COORD{ 0, 0 }, &rect ) ) { throw std::runtime_error( "WriteConsoleOutput error!" ); } } static void InitializeBoard( HANDLE hConsole ) { SMALL_RECT rect = { 0, 0, WIDTH - 1, HEIGHT - 1 }; if ( ! ReadConsoleOutputA( hConsole, &board[0][0], COORD{ WIDTH, HEIGHT }, COORD{ 0, 0 }, &rect ) ) { throw std::runtime_error( "ReadConsoleOutput error!" ); } } static void ResizeConsoleWindow( HANDLE hConsole, int width, int height ) { CONSOLE_SCREEN_BUFFER_INFO sbi; // set console cursor position to 0, 0 if ( ! SetConsoleCursorPosition( hConsole, COORD{ 0, 0 } ) ) { throw std::runtime_error( "Error setting console cursor position!" ); } // get information about console screen buffer size and console window size if ( ! GetConsoleScreenBufferInfo( hConsole, &sbi ) ) { throw std::runtime_error( "Error getting console information!" ); } // the following calculations are based on the assumption that the top-left // corner of the scroll window is 0,0, so make sure that this assumption is // correct assert( sbi.srWindow.Top == 0 && sbi.srWindow.Left == 0 ); // shrink scroll window, if necessary bool bMustShrinkX = sbi.srWindow.Right > WIDTH - 1; bool bMustShrinkY = sbi.srWindow.Bottom > HEIGHT - 1; if ( bMustShrinkX || bMustShrinkY ) { if ( bMustShrinkX ) { sbi.srWindow.Right = WIDTH - 1; } if ( bMustShrinkY ) { sbi.srWindow.Bottom = HEIGHT - 1; } if ( ! SetConsoleWindowInfo( hConsole, TRUE, &sbi.srWindow ) ) { throw std::runtime_error( "SetConsoleWindowInfo error!" ); } } // set scroll buffer size sbi.dwSize.X = WIDTH; sbi.dwSize.Y = HEIGHT; if ( ! SetConsoleScreenBufferSize( hConsole, sbi.dwSize ) ) { throw std::runtime_error( "Error setting buffer size!" ); } // expand scroll window, if necessary bool bMustExpandX = sbi.srWindow.Right < WIDTH - 1; bool bMustExpandY = sbi.srWindow.Bottom < HEIGHT - 1; if ( bMustExpandX || bMustExpandY ) { if ( bMustExpandX ) { sbi.srWindow.Right = WIDTH - 1; } if ( bMustExpandY ) { sbi.srWindow.Bottom = HEIGHT - 1; } if ( ! SetConsoleWindowInfo( hConsole, TRUE, &sbi.srWindow ) ) { throw std::runtime_error( "SetConsoleWindowInfo error!" ); } } } static void DisableBlinkingCursor( HANDLE hConsole ) { CONSOLE_CURSOR_INFO cci; // hide cursor of console if ( ! GetConsoleCursorInfo( hConsole, &cci ) ) { throw std::runtime_error( "GetConsoleCursorInfo error!" ); } cci.bVisible = FALSE; if ( ! SetConsoleCursorInfo( hConsole, &cci ) ) { throw std::runtime_error( "SetConsoleCursorInfo error!" ); } } 

This program prints the following output in an infinite loop, without any flickering:

################################################## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ################################################## 

Comments

-1

If you just want to stop the flickering without modifying anything else, you could just hide the cursor using SetConsoleCursorInfo at the start of your application:

const CONSOLE_CURSOR_INFO info{ .dwSize = 1, .bVisible = false }; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); 

As the other answers and comments already mentioned, to remove flickering and as bonus even speed up the draw time you should use a buffer to which you write your characters first, and then output the whole buffer at once. This could be achieved by something like the following:

void board() { std::array<char, HEIGHT* (WIDTH + 1)> buffer; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), { 0, 0 }); for (int y = 0; y < HEIGHT; ++y) { for (int x = 0; x < WIDTH; ++x) { if (x == 0 || y == 0 || x == WIDTH - 1 || y == HEIGHT - 1) { buffer[y * (WIDTH + 1) + x] = '#'; } else { buffer[y * (WIDTH + 1) + x] = ' '; } } buffer[y * (WIDTH + 1) + WIDTH] = '\n'; } std::cout.write(buffer.data(), buffer.size()); } 

This sped up drawing by over 300 times on my machine.


If you would like to speed up your drawing even more, you could have two buffers and swap them back and forth with a second thread drawing them.

4 Comments

OP's problem isn't that output is too slow, its too fast.
@Caleth I honestly don't think that OP's problem has to do with speed at all. When testing the output, the only flickering I noticed was the cursor advancing by once cell after each std::cout. When writing a whole buffer at once, the cursor only ever jumps to the ending of the drawn board, which means that it doesn't flicker all over the screen. So this removed flickering and made it way faster.
Using the code they post, the console output is infinitely scrolling about as fast as it will go. They have commented out SetConsoleCursorPosition
@Caleth I don't think that the code in OP's questions is to be taken exactly as is (this wouldn't even work because of cout without std:: or using namespace std;). My guess is that OP actually intends to use SetConsoleCursorPosition but wanted to fix the flickering first without knowing where it comes from. However I am not going to discuss what OP might be intending or not under my answer anymore. If you want to be sure what his intentions are, just ask OP directly.
-3

The main reason you are seeing flickering is that you are drawing individual characters as fast as std::cout will let you.

By default on windows, std::cout will buffer the characters you stream to it until you supply a newline. Because there are many newlines in each board, this means that the console will draw line by line. Once it is done drawing once, you immediately start drawing again. This will scroll the lines of the console, so there isn't a consistent position of the top and bottom rows. When your screen goes to draw, those borders will be in an arbitrary position.

I would check if you still have flicker if there is a wait-time between draws, as a modern computer should be able to update an entire console window in a fraction your screen's refresh time. Adding a wait of approximately the time between redraws of your screen (~16ms for a 60Hz screen) will (mostly) ensure that your bottom row will be the bottom of the console output when the screen is drawn.

#include <thread> int main(){ while(true){ auto next_frame = std::chrono::steady_clock::now() + std::chrono::milliseconds(16); board(); std::this_thread::sleep_until(next_frame); } return 0; } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.