I am writing a snake game in C using the ncurses
library, where the screen updates itself every second. As those who have played the game will know, if the user enters various keys or holds down a key for long, there should be no 'buffered' key presses which get stored. In other words, if I hold down w
(the up key) and stdin
receives a sequence of 20 w
s, and subsequently enter a d
(the right key), I expect the snake to immediately move to the right, and ignore the buffered w
s.
I am trying to achieve this using the ncurses
function getch()
, but for some reason I am achieving the undesired effect which I have just described; namely that any long key presses are being stored and processed first, before considering the last key pressed, as this MWE will illustrate:
#include <stdio.h>
#include <unistd.h>
#include <ncurses.h>
int main(){
char c = 'a';
initscr();
cbreak();
noecho();
for(;;){
fflush(stdin);
timeout(500);
c = getch();
sleep(1);
printw("%c", c);
}
return 0;
}
Is there any way I can modify this code so that getch()
ignores any buffered text? The fflush()
right before doesn't seem to be helping.
First the answer to your immediate question: Even when you read from stdin
one character at a time, you normally don't want to miss one. If getch()
would just return whatever character was entered last, input routines using getch()
would become very unreliable.
Even in a snake game, you DO want to get all the characters entered. Imagine the snake moves to the right and the player wants to do a "u-turn" by hitting down and then immediately left. If your game would only pick up the last character, the snake would go directly to the left and thus kill itself. Quite a frustrating game experience ;)
The solution to your problem is simple: make your game loop poll for input much more often than the snake is moved and maintain a queue of requested direction changes. I've done that in my curses-based snake game.
Here is the relevant code from the game loop:
typedef enum
{
NONE,
LEFT,
DOWN,
RIGHT,
UP
} Dir;
// [...]
ticker_start(10);
while (1)
{
screen_refresh(screen);
ticker_wait();
key = getch();
if (key == 'q' || key == 'Q')
{
// quit game
}
switch (key)
{
case KEY_LEFT:
snake_setDir(snake, LEFT);
break;
case KEY_DOWN:
snake_setDir(snake, DOWN);
break;
case KEY_UP:
snake_setDir(snake, UP);
break;
case KEY_RIGHT:
snake_setDir(snake, RIGHT);
break;
case ' ':
// pause game
break;
}
if (!--nextStep)
{
step = snake_step(snake); // move the snake
// code to check for food, killing, ...
}
if (!--nextFood)
{
// add a new food item
}
}
ticker_stop();
And here is how the snake implements and uses the queue:
struct snake
{
// queue of requested directions:
Dir dir[4];
// more properties
};
void
snake_setDir(Snake *self, Dir dir)
{
int i;
Dir p;
p = self->dir[0];
for (i = 1; i < 4; ++i)
{
if (self->dir[i] == NONE)
{
if (dir != p) self->dir[i] = dir;
break;
}
p = self->dir[i];
}
}
static void dequeueDir(Snake *self)
{
int i;
if (self->dir[1] != NONE)
{
for (i=0; i<3; ++i) self->dir[i] = self->dir[i+1];
self->dir[3] = NONE;
}
}
Step
snake_step(Snake *self)
{
dequeueDir(self);
// [...]
switch (self->dir[0])
{
case LEFT:
--newHead->x;
break;
case DOWN:
++newHead->y;
break;
case RIGHT:
++newHead->x;
break;
case UP:
--newHead->y;
break;
default:
break;
}
// [...]
}
Good luck with your game!
I've done some more research and found out that:
The flushinp()
routine throws away any typeahead that has been typed by the user and has not yet been read by the program.
This solved my problem.
Source: https://linux.die.net/man/3/flushinp
the following proposed code shows how to ignore all but the last key entered.
#include <stdio.h>
#include <unistd.h>
#include <ncurses.h>
int main( void )
{
char c = ' ';
char lastKey;
initscr();
cbreak();
noecho();
for(;;)
{
timeout(500);
while( (c = getch()) != EOF )
{
lastKey = c;
}
sleep(1);
if( EOF == c )
{
printw("%c", lastKey);
}
}
return 0;
}