Why doesn't getch() read the last character en

2019-07-12 11:35发布

问题:

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 ws, and subsequently enter a d (the right key), I expect the snake to immediately move to the right, and ignore the buffered ws.

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.

回答1:

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!



回答2:

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



回答3:

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