在某些时候禁止输入

时间:2016-08-13 10:41:59

标签: c input scanf

我在c中有一个使用scanf的基于文本的游戏。

有几次玩家应该输入内容,但是,当他没有输入时,光标停留在游戏中,让用户键入他想要的任何东西,这会破坏未来的扫描和故事。

除非有scanf等待响应,否则有没有办法禁止输入?

2 个答案:

答案 0 :(得分:8)

我认为退一步思考程序执行环境中存在的所有移动部件会很有帮助。

执行时,程序将成为在OS的多任务环境中运行的独特进程。终端是一个单独的进程,具有相关的GUI窗口,可以在本地或远程运行(例如,某人理论上可以通过ssh通过网络连接从远程位置运行您的游戏)。用户通过键盘和屏幕与终端程序进行交互。

现在,它实际上是终端进程(与OS内核密切合作),它负责用户输入的大部分细微差别。终端在接收到它们时立即将刚打字的字符打印到其GUI窗口,并且终端维护已输入但尚未被前台进程读取的字符的输入缓冲区。 / p>

方便地,终端允许其行为由一组配置设置控制,并且这些设置可以在连接程序的运行时间内以编程方式更改。我们可以用来读取和写入这些设置的C级API称为termios

我强烈推荐有关于终端的精彩文章:The TTY demystified。出于此问题的目的,配置TTY设备部分最有用。它没有直接演示termios库,但展示了如何使用内部使用termios库的stty实用程序。

(请注意,尽管到目前为止我所提供的链接都集中在Linux上,但它们适用于所有类Unix系统,包括Mac OS X.)

不幸的是,没有办法完全禁止"使用单个开关输入,但我们可以通过切换几个终端设置并在适当的时间手动丢弃缓冲输入来实现相同的效果。

我们需要关注的两个终端设置是ECHOICANON。默认情况下,这两个设置通常都处于打开状态。

通过关闭ECHO,我们可以防止终端在收到终端窗口时将刚输入的字符打印到终端窗口。因此,在程序运行时,用户输入的任何字符似乎都会被完全忽略,尽管它们仍然会被终端内部缓冲。

通过关闭ICANON,我们确保终端不会等待输入按键提交完整的输入行,然后再将输入返回到程序,例如当程序进行read()通话时。相反,它将返回它当前在其内部输入缓冲区中缓冲的任何字符,从而使我们可以立即丢弃它们并继续执行。

完整过程如下所示:

1:禁用输入,意味着关闭ECHOICANON

2:使用输出运行一些游戏,不需要任何用户输入。

3:启用输入,意味着丢弃任何缓冲的终端输入,然后打开ECHOICANON

4:读取用户输入。

5:从步骤1开始重复。后续的游戏玩法现在可以使用最新的用户输入。

步骤3中存在与丢弃缓冲输入相关的复杂情况。我们可以通过简单地通过read()读取来自stdin的输入和固定长度的缓冲区来实现这种丢弃操作,直到不再需要读取输入。但是,如果没有输入准备好在中读取以进行丢弃操作,那么第一次调用将阻塞,直到用户键入内容为止。我们需要阻止这种阻止。

我相信有两种方法可以做到。这样的事情称为非阻塞读取,可以使用termios或fcntl()设置(或通过使用O_NONBLOCK标志打开第二个文件描述符到同一端点,I think)哪会导致read()立即返回,errno设置为EAGAIN,如果它会阻止。第二种方法是使用poll()select()轮询文件描述符,以确定是否准备好读取数据;如果没有,我们可以完全避免read()电话。

这是一个使用select()来避免阻止的工作解决方案:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <unistd.h>
#include <termios.h>

struct termios g_terminalSettings; // global to track and change terminal settings

void disableInput(void);
void enableInput(void);

void discardInputBuffer(void);
void discardInputLine(void);

void setTermiosBit(int fd, tcflag_t bit, int onElseOff );
void turnEchoOff(void);
void turnEchoOn(void);
void turnCanonOff(void);
void turnCanonOn(void);

int main(void) {

    // prevent input immediately
    disableInput();

    printf("welcome to the game\n");

    // infinite game loop
    int line = 1;
    int quit = 0;
    while (1) {

        // print dialogue
        for (int i = 0; i < 3; ++i) {
            printf("line of dialogue %d\n",line++);
            sleep(1);
        } // end for

        // input loop
        enableInput();
        int input;
        while (1) {
            printf("choose a number in 1:3 (-1 to quit)\n");
            int ret = scanf("%d",&input);
            discardInputLine(); // clear any trailing garbage (can do this immediately for all cases)
            if (ret == EOF) {
                if (ferror(stdin)) { fprintf(stderr, "[error] scanf() failed: %s", strerror(errno) ); exit(1); }
                printf("end of input\n");
                quit = 1;
                break;
            } else if (ret == 0) { // invalid syntax
                printf("invalid input\n");
            } else if (input == -1) { // quit code
                quit = 1;
                break;
            } else if (!(input >= 1 && input <= 3)) { // invalid value
                printf("number is out-of-range\n");
            } else { // valid
                printf("you entered %d\n",input);
                break;
            } // end if
        } // end while
        if (quit) break;
        disableInput();

    } // end while

    printf("goodbye\n");

    return 0;

} // end main()

void disableInput(void) {
    turnEchoOff(); // so the terminal won't display all the crap the user decides to type during gameplay
    turnCanonOff(); // so the terminal will return crap characters immediately, so we can clear them later without waiting for a LF
} // end disableInput()

void enableInput(void) {
    discardInputBuffer(); // clear all crap characters before enabling input
    turnCanonOn(); // so the user can type and edit a full line of input before submitting it
    turnEchoOn(); // so the user can see what he's doing as he's typing
} // end enableInput()

void turnEchoOff(void) { setTermiosBit(0,ECHO,0); }
void turnEchoOn(void) { setTermiosBit(0,ECHO,1); }

void turnCanonOff(void) { setTermiosBit(0,ICANON,0); }
void turnCanonOn(void) { setTermiosBit(0,ICANON,1); }

void setTermiosBit(int fd, tcflag_t bit, int onElseOff ) {
    static int first = 1;
    if (first) {
        first = 0;
        tcgetattr(fd,&g_terminalSettings);
    } // end if
    if (onElseOff)
        g_terminalSettings.c_lflag |= bit;
    else
        g_terminalSettings.c_lflag &= ~bit;
    tcsetattr(fd,TCSANOW,&g_terminalSettings);
} // end setTermiosBit()

void discardInputBuffer(void) {
    struct timeval tv;
    fd_set rfds;
    while (1) {
        // poll stdin to see if there's anything on it
        FD_ZERO(&rfds);
        FD_SET(0,&rfds);
        tv.tv_sec = 0;
        tv.tv_usec = 0;
        if (select(1,&rfds,0,0,&tv) == -1) { fprintf(stderr, "[error] select() failed: %s", strerror(errno) ); exit(1); }
        if (!FD_ISSET(0,&rfds)) break; // can break if the input buffer is clean
        // select() doesn't tell us how many characters are ready to be read; just grab a big chunk of whatever is there
        char buf[500];
        ssize_t numRead = read(0,buf,500);
        if (numRead == -1) { fprintf(stderr, "[error] read() failed: %s", strerror(errno) ); exit(1); }
        printf("[debug] cleared %d chars\n",numRead);
    } // end while
} // end discardInputBuffer()

void discardInputLine(void) {
    // assumes the input line has already been submitted and is sitting in the input buffer
    int c;
    while ((c = getchar()) != EOF && c != '\n');
} // end discardInputLine()

我应该澄清一下,我所包含的discardInputLine()功能与丢弃输入缓冲区完全分开,输入缓冲区在discardInputBuffer()中实现并由enableInput()调用。丢弃输入缓冲区是暂时不允许用户输入的解决方案中必不可少的步骤,而丢弃scanf()未读取的输入行的其余部分并不是必需的。但我认为在输入循环的后续迭代中防止残余线输入被扫描是有意义的。如果用户输入了无效输入,那么防止无限循环也是必要的,因此我们可以将其称为必不可少。

这是我演绎输入的演示:

welcome to the game
line of dialogue 1
line of dialogue 2
line of dialogue 3
[debug] cleared 12 chars
choose a number in 1:3 (-1 to quit)
0
number is out-of-range
choose a number in 1:3 (-1 to quit)
4
number is out-of-range
choose a number in 1:3 (-1 to quit)
asdf
invalid input
choose a number in 1:3 (-1 to quit)
asdf 1 2 3
invalid input
choose a number in 1:3 (-1 to quit)
0 1
number is out-of-range
choose a number in 1:3 (-1 to quit)
1 4
you entered 1
line of dialogue 4
line of dialogue 5
line of dialogue 6
choose a number in 1:3 (-1 to quit)
2
you entered 2
line of dialogue 7
line of dialogue 8
line of dialogue 9
[debug] cleared 256 chars
[debug] cleared 256 chars
[debug] cleared 256 chars
[debug] cleared 256 chars
[debug] cleared 256 chars
[debug] cleared 256 chars
[debug] cleared 256 chars
[debug] cleared 238 chars
choose a number in 1:3 (-1 to quit)
-1
goodbye

在对话的第一个三元组中,我输入了12个随机字符,然后丢弃。然后我展示了各种类型的无效输入以及程序如何响应它们。在第二个三重对话期间,我没有输入任何内容,因此没有丢弃任何字符。在对话的最后三重奏中,我很快将一大块文本粘贴到我的终端上(使用鼠标右键单击,这是粘贴到我的特定终端的快捷方便的快捷方式),你可以看到它丢弃了所有的它正确,需要对select() / read()循环进行多次迭代才能完成。

答案 1 :(得分:1)

在Linux和HP-UX计算机上,使用

禁用终端

上键盘输入的显示
stty -echo 

启用从终端上的键盘输入

stty echo