Недавно у меня появилось немного свободного времени, поэтому я решил улучшить свои знания языка С. Пока что они ограничиваются уровнем простых институтских лабораторных работ и книгой С. Прата «Язык программирования C», поэтому для начала я поставил себе простую задачу — написать консольную змейку. К тому моменту, когда отображение игрового поля и самой змейки на экране и часть, отвечающая за передвижение ее по полю была написана, появилось две проблемы:
Можно было решить их при помощи использования getch() из curses, но это было скучно и не интересно.
О том как были решены эти проблемы под катом.
Для начала нужно было справиться со считыванием нажатия клавиш пользователя. Реализовано это у меня через вызов функции getc(), но при вызове этой функции программа начинала считывать символы, только после того, как был нажат Enter. В ходе недолго поиска было обнаружено, что это не проблема getc(), а проблема терминала Linux, точнее драйвера терминала, который по-умолчанию работает в каноническом режиме, т.е. это драйвер терминала ожидает окончания ввода строки нажатием Enter, после чего отправляет данную строку в stdin программы. Так же было найдено решение данной проблемы: перевод драйвера в неканонический режим. Реализуется это очень просто — подключается заголовочный файл termios.h и пишутся две функции:
Первая функция для перевода в неканонический режим:
Вторая функция для возвращения в первоначальное состояние:
Переменная stored_settings должна быть объявлена как глобальная:
Затем в начале программы вызывается первая функция, а в конце, соответственно, вызывается вторая.
В готовом виде тестовый пример выглядит так:
Теперь моя змейка умела считывать посимвольно пользовательский ввод. Но появилась вторая проблема: программа все равно останавливалась и ждала пока пользователь нажмет какую-либо клавишу. Снова начался поиск в интернете, в процессе которого я часто натыкался на «решение» этой проблеммы в виде того, что было описано выше. Результатом поиска стал совет использовать функцию select().
Из параметров, принимаемых этой функцией нас интересуют только n, readfds и utimeout:
Поэтому вызов select() будет выглядеть слудеющим образом:
Для указания, что события будут рассматриваться только для stdin нужно вызвать
Структура struct timeval определена следующим образом:
В итоге у нас получается вот такой тестовый пример (не забудем перевести терминал в неканонический режим, что уже было рассмотрено выше):
Использованный материал:
- данные отправлялись программе только после нажатия Enter
- программа останавливалась на считывании пользовательского ввода
Можно было решить их при помощи использования getch() из curses, но это было скучно и не интересно.
О том как были решены эти проблемы под катом.
Для начала нужно было справиться со считыванием нажатия клавиш пользователя. Реализовано это у меня через вызов функции getc(), но при вызове этой функции программа начинала считывать символы, только после того, как был нажат Enter. В ходе недолго поиска было обнаружено, что это не проблема getc(), а проблема терминала Linux, точнее драйвера терминала, который по-умолчанию работает в каноническом режиме, т.е. это драйвер терминала ожидает окончания ввода строки нажатием Enter, после чего отправляет данную строку в stdin программы. Так же было найдено решение данной проблемы: перевод драйвера в неканонический режим. Реализуется это очень просто — подключается заголовочный файл termios.h и пишутся две функции:
Первая функция для перевода в неканонический режим:
void set_keypress(void) { struct termios new_settings; tcgetattr(0,&stored_settings); new_settings = stored_settings; /* Отключение канонического режима и вывода на экран и установка буфера ввода размером в 1 байт */ new_settings.c_lflag &= (~ICANON); new_settings.c_lflag &= (~ECHO); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); return; }
Вторая функция для возвращения в первоначальное состояние:
void reset_keypress(void) { tcsetattr(0,TCSANOW,&stored_settings); return; }
Переменная stored_settings должна быть объявлена как глобальная:
static struct termios stored_settings;
Затем в начале программы вызывается первая функция, а в конце, соответственно, вызывается вторая.
В готовом виде тестовый пример выглядит так:
#include <stdlib.h> #include <stdio.h> #include <termios.h> #include <string.h> static struct termios stored_settings; void set_keypress(void) { struct termios new_settings; tcgetattr(0,&stored_settings); new_settings = stored_settings; new_settings.c_lflag &= (~ICANON & ~ECHO); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); return; } void reset_keypress(void) { tcsetattr(0,TCSANOW,&stored_settings); return; } int main(void) { set_keypress(); printf("Test: "); while(1) { // putchar здесь вызывается для того, чтобы проверить работоспособность putchar(getchar()); } return 0; }
Теперь моя змейка умела считывать посимвольно пользовательский ввод. Но появилась вторая проблема: программа все равно останавливалась и ждала пока пользователь нажмет какую-либо клавишу. Снова начался поиск в интернете, в процессе которого я часто натыкался на «решение» этой проблеммы в виде того, что было описано выше. Результатом поиска стал совет использовать функцию select().
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *utimeout);
Из параметров, принимаемых этой функцией нас интересуют только n, readfds и utimeout:
- n — этот параметр должен превышать не единицу максимальный файловый дескриптор в любом из наборов. Другими словами, вы должны определить максимальное целое значение для всех ваших дескрипторов, увеличить его на 1, и передать результат в качестве параметра n.
- readfds — этот набор мониторится на наличие данных для чтения в одном или нескольких дескрипторах. После возврата из select набор readfs будет очищен от всех дескрипторов, кроме тех, в которых есть данные, доступные для немедленного чтения функциями recv() (для сокетов) или read() (для каналов pipe, файлов и сокетов).
- utimeout — максимальное время, в течение которого select будет ожидать смены статуса. Если этот параметр установлен в NULL, select будет заблокирован бесконечно, ожидая событий в дескрипторах. При установке параметра в 0 секунд select возвратится немедленно.
Поэтому вызов select() будет выглядеть слудеющим образом:
select(1, &rfds, NULL, NULL, &tv);
Для указания, что события будут рассматриваться только для stdin нужно вызвать
FD_SET(0, &rfds);
Структура struct timeval определена следующим образом:
struct timeval { time_t tv_sec; /* секунды */ long tv_usec; /* микросекунды */ };
В итоге у нас получается вот такой тестовый пример (не забудем перевести терминал в неканонический режим, что уже было рассмотрено выше):
#include <stdio.h> #include <stdlib.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <termios.h> static struct termios stored_settings; void set_keypress(void) { struct termios new_settings; tcgetattr(0,&stored_settings); new_settings = stored_settings; new_settings.c_lflag &= (~ICANON & ~ECHO); new_settings.c_cc[VTIME] = 0; new_settings.c_cc[VMIN] = 1; tcsetattr(0,TCSANOW,&new_settings); return; } void reset_keypress(void) { tcsetattr(0,TCSANOW,&stored_settings); return; } int main(void) { fd_set rfds; struct timeval tv; int retval; set_keypress(); while(1) { FD_ZERO(&rfds); FD_SET(0, &rfds); tv.tv_sec = 0; tv.tv_usec = 0; retval = select(2, &rfds, NULL, NULL, &tv); if (retval) { printf("Data is available now.\n"); getc(stdin); } else { printf("No data available.\n"); } usleep(100000); } reset_keypress(); exit(0); }
Использованный материал:
