在某些时候禁止输入

cra*_*sfh 4 c input scanf

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

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

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

bgo*_*dst 8

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

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

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

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

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

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


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

我们需要关注的两个终端设置是ECHO和ICANON.这两个设置通常默认打开.

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

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

完整过程如下所示:

1:禁用输入,意味着关闭ECHO和ICANON.

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

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

4:读取用户输入.

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

步骤3中存在与丢弃缓冲输入有关的复杂情况.我们可以通过read()使用固定长度的缓冲区读取来自stdin via的输入来实现这种丢弃操作,直到不再需要读取输入.但是如果没有任何输入可以完全读取以进行丢弃操作,那么第一个调用将阻塞,直到用户输入内容为止.我们需要阻止这种阻塞.

我相信有两种方法可以做到.有一种称为非阻塞读取的东西,它可以用termios设置或fcntl()(或者通过打开第二个文件描述符到带有O_NONBLOCK标志的同一端点),如果它会导致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()
Run Code Online (Sandbox Code Playgroud)

我应该澄清一下,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
Run Code Online (Sandbox Code Playgroud)

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