c - 在某些时间禁止输入

标签 c input scanf

我有一个c语言的文本游戏,它使用scanf。
有些时候玩家应该输入一些东西,但是,当他不输入时,光标停留在游戏中,让用户输入他想要的任何东西,这会破坏将来的扫描和故事。
是否有方法禁止输入,除非有扫描等待响应?

最佳答案

我认为,回退一下并思考程序中的执行环境中存在的所有活动部件将是有益的。
执行时,您的程序将成为一个运行在操作系统多任务环境中的不同进程。终端是一个单独的进程,有一个相关的gui窗口,可以在本地或远程运行(例如,理论上有人可以通过ssh通过网络连接从远程位置运行游戏)。用户通过键盘和屏幕与终端程序交互。
现在,实际上是终端进程(与操作系统内核密切合作)负责用户输入的大部分细微差别。终端在接收到键入的字符后立即将其打印到其gui窗口中,终端维护已键入但尚未被前台进程读取的字符的输入缓冲区。
方便地,终端允许其行为由一组配置设置控制,并且这些设置可以在连接的程序运行时以编程方式更改。我们可以用来读写这些设置的c级api称为termios
有一篇关于终端的文章我强烈推荐:The TTY demystified。在本问题中,配置tty设备部分最有用。它不直接演示termios库,而是演示如何使用stty实用程序,该实用程序在内部使用termios库。
(请注意,尽管到目前为止我提供的链接都集中在Linux上,但它们适用于所有类Unix系统,包括MacOSX。)
不幸的是,没有办法用一个开关完全“禁止”输入,但是我们可以通过切换几个终端设置和在正确的时间手动丢弃缓冲输入来达到同样的效果。
我们需要关注的两个终端设置是ECHOICANON。默认情况下,这两个设置都处于打开状态。
通过关闭ECHO,我们可以防止终端在接收到键入的字符时将其打印到终端窗口。因此,当程序运行时,用户类型的任何字符似乎都会被完全忽略,尽管终端仍会在内部缓冲这些字符。
通过关闭ICANON,我们确保终端不会等待enter键提交完整的输入行,然后再将输入返回到程序,例如当程序进行read()调用时。相反,它将返回当前缓冲在其内部输入缓冲区中的任何字符,从而使我们能够立即丢弃它们并继续执行。
整个过程如下:
1:禁用输入,表示关闭ECHOICANON
2:运行一些有输出的游戏,不需要任何用户输入。
3:启用输入,意味着放弃任何缓冲终端输入,然后打开ECHOICANON
4:读取用户输入。
5:重复步骤1。接下来的游戏可以使用最新的用户输入。
在步骤3中有一个与丢弃缓冲输入相关的复杂问题。我们可以通过使用固定长度的缓冲区通过read()从stdin读取输入,直到没有要读取的输入为止,来实现这个丢弃操作。但是如果对于丢弃操作根本没有准备好读取的输入,那么第一个调用将被阻塞,直到用户键入某些内容。我们需要防止这种阻塞。
我相信有两种方法可以做到。有一个叫做非阻塞读取的东西,可以用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()

我应该澄清,我包含的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()循环才能完成。

关于c - 在某些时间禁止输入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38931856/

相关文章:

c - 编写 C 函数 sprod (n,x,y) 返回 2 个一维浮点型数组的标量积

c - 为什么在以下结构定义中需要显式转换

c - OpenGL glutWireCube 可以工作,但 glutWireCylinder 不能。我究竟做错了什么?

c - strcmp 在 c 中的 while 循环中不起作用

c++ - sscanf 的精度问题

c - 如何用C语言在另一个文件中搜索一个文件的单词?

JavaScript 不会返回输入元素值

c - Stdin 、 stdout 、 stderr ,如何让我的程序从文本文件获取输入并输出到文本文件

java - 获取键盘输入

c - 如何从用户输入中获取数组的每个元素,然后将其传递给 C 中另一个文件中的函数