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