Search code examples
c++ncurses

How to reduce flickering/lag on curses?


Currently trying to solve an issue with flickering in a very small game I'm making in ncurses with C++. The flickering isn't abhorrent but I could imagine it being far more annoying when more entities are being rendered/moving at once. Code below. There's also a header file but I don't believe it's relevant here.

#include "game.h"


// only real note is that the sprite char** must be NULL terminated
class Entity {
  public:
    int x;
    int y;
    //these are mainly here for bounds checking
    int width;
    int height;

    //position and sprite are needed by outside methods, but nothing else should be
    // this needs to be stored as an array of chars* in order to properly render the sprite
    // the sprite array also needs to be null terminated
    const char** sprite;
    Entity(int y, int x, const char** sprite) {
      this->y = y;
      this->x = x;
      this->sprite = sprite;
      this->width = getWidth();
      this->height = getHeight();

    };


    int getWidth() {
      int w_max = 0, i = 0;
      while (this->sprite[i] != NULL) {
        int line_width = strlen(sprite[i]);
        if (line_width > w_max) {
          w_max = width;

        }
        i++;
      }

      return w_max;

    }

    int getHeight() {
      int current_height = 0, i = 0;
      while (this->sprite[i] != NULL) {
        current_height++;
        i++;
      }
      return current_height;

    }

};

class Player: public Entity {
  public:
    Player(int y, int x, const char** sprite) : Entity (y, x, sprite) {}

    int move(int proposed_direction) {
      int right = 0, down = 0;

      switch(proposed_direction) {
        case KEY_LEFT:
          right--;
          break;
        case KEY_RIGHT:
          right++;
          break;
        case KEY_UP:
          down--;
          break;
        case KEY_DOWN:
          down++;
          break;
        case 'q':
          endwin();
          exit(0);
          break;
        default:
          down++;
          break;

      }
      this->y += down;
      this->x += right;

      return -1;

    }


//    int check(int proposed_direction) {
//      return -1;
//
//    }
};


void screenStart() {
  initscr();            
  noecho();
  curs_set(FALSE);
  //honestly not sure why this is required
  nodelay(stdscr, TRUE);
  //timeout(-1);
  keypad(stdscr, TRUE);

}

void drawEntity(Entity* entity) {
  
  //this is to print out every line of the sprite in order at the right place
  for (int i = 0; entity->sprite[i] != NULL; i++) {
    // the + i is there because it draws horizontally line by line
    mvprintw(entity->y + i, entity->x, entity->sprite[i]);

  }
  

}

int main() {
  screenStart();
  const char* player_sprite[] = {"XX", "XX", NULL};
  Player* player = new Player(15, 15, player_sprite);

  int ch;
  for (;;) {
    erase();
    if ((ch = getch()) == ERR) {
      drawEntity(player);

    }

    else {
      player->move(ch);
      drawEntity(player);

    }
    wnoutrefresh(stdscr);
    doupdate();
    napms(16);

  }

    endwin();

  return 0;

};

I've looked into reducing terminal size, calculating the timeout better, etc, but wanted to make sure there was nothing I was doing majorly wrong before I committed to doing something. Redrawing only part of the window might help, but I don't know if it would scale all that well? I'm open to any advice on curses in general as well.

EDIT: New code below, after changing the order of redrawing/clearing/calls which has no flickering:

for (;;) {
    werase(win);
    auto startTime = std::chrono::steady_clock::now();
    box(win, 0, 0); 
    player->drawEntity();
    int ch = wgetch(win);
    if (ch != ERR) {
      player->move(ch);
    }   

    auto diff = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - startTime);
    napms(WAIT_TIME - diff.count());

  }

Solution

  • This chunk is the main contributor to flicker:

    erase();
    if ((ch = getch()) == ERR) {
      drawEntity(player);
    
    }
    

    The erase modifies the whole screen, and the following getch does a refresh (actually clearing the screen) before your code follows up by repainting the screen. If you change the way it's organized so that the erase is followed by the repainting, then that eliminates most of the screen updates, i.e., little activity on the actual terminal.