Введение
Flappy Bird - это популярная и увлекательная мобильная игра, которая приобрела огромную популярность из-за простого, но вызывающего сложности игрового процесса. В этом проекте мы научимся реализовывать свою версию Flappy Bird с использованием языка программирования C.
Следуя этому проекту, вы:
- научитесь использовать библиотеку
ncursesдля текстовой отрисовки экрана; - приобретете знания о структурах данных и системных вызовах в Linux;
- получите опыт работы с событиями клавиатуры и реальными обновлениями в программе на C.
👀 Предпросмотр

🎯 Задачи
В этом проекте вы научитесь:
- реализовывать текстовую версию Flappy Bird на C;
- обрабатывать события клавиатуры для управления движением птицы;
- создавать иллюзию движения вперед путем перемещения препятствий с права налево;
- использовать библиотеку
ncursesдля отрисовки текстового интерфейса.
🏆 Достижения
После завершения этого проекта вы сможете:
- продемонстрировать мастерство в языке программирования C;
- развить навыки обработки событий клавиатуры;
- реализовать реальные обновления в программе на C;
- использовать библиотеку
ncursesдля текстовой отрисовки экрана; - понять структуры данных и системные вызовы в Linux.
Основные знания
Наше проектирование требует некоторых знаний о структурах данных и включает в себя некоторые системные вызовы в Linux.
Кроме того, мы также используем библиотеку для текстовой отрисовки экрана под названием ncurses. Поэтому при компиляции необходимо добавить опцию -lcurses.
Идеи дизайна
Для реализации текстовой версии Flappy Bird мы начинаем с реализации следующих трех ключевых моментов:
- Программа должна быть способна реагировать на события клавиатуры.
- Текстовый интерфейс должен быть способен обновляться в режиме реального времени.
- Птица должна создавать визуальную иллюзию полета вперед.
Для решения вышеперечисленных трех задач наши решения следующие:
- Используем системные интерфейсы, предоставляемые Linux, для захвата событий клавиатуры.
- Используем функции библиотеки ncurses для отрисовки текстового интерфейса.
- Чтобы создать иллюзию полета птицы вперед:
Самый простой подход - заставить птицу двигаться слева направо по горизонтали, но в таком случае птица в какой-то момент выйдет за правую границу.
Вместо этого давайте подумаем наоборот: когда человек едет вперед и видит пейзаж снаружи машины, это кажется, что движется назад (движение относительное). Поэтому мы заставляем препятствия двигаться справа налево, что дает тот же визуальный эффект и избавляется от проблемы выхода птицы за границу.
Определить константы
Сначала откройте терминал Xfce и выполните следующую команду для установки библиотеки ncurses:
sudo apt update
sudo apt-get install libncurses5-dev
Перейдите в каталог ~/project и создайте файл проекта flappy_bird.c:
cd ~/project
touch flappy_bird.c
Далее необходимо написать код на C. Первым шагом является включение заголовочных файлов:
## include <curses.h>
## include <stdlib.h>
## include <signal.h>
## include <sys/time.h>
Прежде чем писать функцию main(), давайте выполним некоторые базовые задачи. Поскольку мы работаем с текстовым интерфейсом терминала, ASCII-символы являются обязательными. Поэтому необходимо определить некоторые константы.
Мы будем использовать * для представления столбов в фоне и O для представления птицы. Код выглядит следующим образом:
## define CHAR_BIRD 'O' // Определить символ птицы
## define CHAR_STONE '*' // Определить камни, составляющие столбы
## define CHAR_BLANK ' ' // Определить пустой символ
Столбы в фоне будут храниться с использованием односвязного списка. Структура определяется следующим образом:
typedef struct node {
int x, y;
struct node *next;
}node, *Node;
Также определим некоторые глобальные переменные:
Node head, tail;
int bird_x, bird_y;
int ticker;
Теперь объявим функции, которые мы создадим:
void init(); // Функция инициализации, которая управляет задачами инициализации игры
void init_bird(); // Инициализировать координаты положения птицы
void init_draw(); // Инициализировать фон
void init_head(); // Инициализировать голову связанного списка, хранящего столбы
void init_wall(); // Инициализировать связанный список, хранящий столбы
void drop(int sig); // Функция приема сигнала для приема системных сигналов и перемещения столбов с права налево
int set_ticker(int n_msec); // Установить интервал тактования таймера ядра
Проблема с таймингом
Теперь решим проблему того, как заставить фон двигаться с регулярным интервалом. Мы будем использовать функциональность, предоставляемую системой Linux, а именно сигналы.
Не знаете, что такое сигнал? Не переживайте, вы можете представить его как таймер в ядре Linux, который посылает сигнал в нашу программу каждые определенные промежутки времени. Наша функция обработчика сигнала drop(int sig) будет автоматически выполняться при получении сигнала. Мы просто нужно переместить столб в функции drop(int sig). Кроме того, поскольку сигнал посылается ядром Linux, приема сигнала не будет блокировать прием сигнала с клавиатуры.
Теперь реализуем наш код и установим период таймера ядра с использованием функции set_ticker(int n_msec):
int set_ticker(int n_msec)
{
struct itimerval timeset;
long n_sec, n_usec;
n_sec = n_msec / 1000;
n_usec = (n_msec % 1000) * 1000L;
timeset.it_interval.tv_sec = n_sec;
timeset.it_interval.tv_usec = n_usec;
timeset.it_value.tv_sec = n_sec;
timeset.it_value.tv_usec = n_usec;
return setitimer(ITIMER_REAL, ×et, NULL);
}
Функция обработчика сигнала drop(int sig):
void drop(int sig)
{
int j;
Node tmp, p;
// Очистить символ в исходном положении птицы
move(bird_y, bird_x);
addch(CHAR_BLANK);
refresh();
// Обновить позицию птицы и обновить экран
bird_y++;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
// Закончить игру, если птица столкнется с столбом
if((char)inch() == CHAR_STONE)
{
set_ticker(0);
sleep(1);
endwin();
exit(0);
}
// Проверить, выходит ли первая стена за границу
p = head->next;
if(p->x < 0)
{
head->next = p->next;
free(p);
tmp = (node *)malloc(sizeof(node));
tmp->x = 99;
tmp->y = rand() % 11 + 5;
tail->next = tmp;
tmp->next = NULL;
tail = tmp;
ticker -= 10; // Ускорить
set_ticker(ticker);
}
// Нарисовать новый столб
for(p = head->next; p->next!= NULL; p->x--, p = p->next)
{
// Заменить CHAR_STONE на CHAR_BLANK
for(j = 0; j < p->y; j++)
{
move(j, p->x);
addch(CHAR_BLANK);
refresh();
}
for(j = p->y+5; j <= 23; j++)
{
move(j, p->x);
addch(CHAR_BLANK);
refresh();
}
if(p->x-10 >= 0 && p->x < 80)
{
for(j = 0; j < p->y; j++)
{
move(j, p->x-10);
addch(CHAR_STONE);
refresh();
}
for(j = p->y + 5; j <= 23; j++)
{
move(j, p->x-10);
addch(CHAR_STONE);
refresh();
}
}
}
tail->x--;
}
В функции обработчика сигнала мы сдвигаем фон вперед на одну колонку, а одновременно заставляем птицу спуститься на одну строку. Также проверяем, столкнулась ли птица с столбом. Если да, то игра окончена.
Функция main()
В функции main() мы сначала вызываем функцию инициализации init(), а затем заходим в цикл while(). Цикл主要 состоит из трех частей:
- Проверяем ввод пользователя: Если нажата клавиша "w" или пробел, птица поднимется на две строки. Если нажата клавиша "q", игра завершится. Если нажата клавиша "z", игра будет приостановлена.
- Перемещаем птицу и перерисовываем ее.
- Проверяем, столкнулась ли птица с трубами.
Посмотрим на код:
int main()
{
char ch;
init();
while(1)
{
ch = getch(); // Получаем ввод с клавиатуры
if(ch == ' ' || ch == 'w' || ch == 'W') // Если нажата пробел или клавиша "w"
{
// Перемещаем птицу и перерисовываем ее
move(bird_y, bird_x);
addch(CHAR_BLANK);
refresh();
bird_y--;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
// Если птица столкнулась с трубами, завершаем игру
if((char)inch() == CHAR_STONE)
{
set_ticker(0);
sleep(1);
endwin();
exit(0);
}
}
else if(ch == 'z' || ch == 'Z') // Приостановка
{
set_ticker(0);
do
{
ch = getch();
} while(ch!= 'z' && ch!= 'Z');
set_ticker(ticker);
}
else if(ch == 'q' || ch == 'Q') // Завершение
{
sleep(1);
endwin();
exit(0);
}
}
return 0;
}
В функции main() мы сначала инициализируем экран, а затем в цикле получаем ввод с клавиатуры. Если нажата клавиша "w" или пробел, птица поднимется на две строки. Если нажата клавиша "q", игра завершится. Если нажата клавиша "z", игра будет приостановлена.
Теперь посмотрим на функцию init():
void init()
{
initscr();
cbreak();
noecho();
curs_set(0);
srand(time(0));
signal(SIGALRM, drop);
init_bird();
init_head();
init_wall();
init_draw();
sleep(1);
ticker = 500;
set_ticker(ticker);
}
Функция init() сначала инициализирует экран с использованием функций, предоставляемых ncurses. Затем она вызывает несколько вспомогательных функций для выполнения конкретной инициализации. Обратите внимание, что мы устанавливаем функцию обработчика сигнала drop() и настраиваем интервал таймера.
Посмотрим на каждую вспомогательную функцию инициализации.
Функция init_bird() инициализирует позицию птицы:
void init_bird()
{
bird_x = 5;
bird_y = 15;
move(bird_y, bird_x);
addch(CHAR_BIRD);
refresh();
sleep(1);
}
Функции init_head() и init_wall() инициализируют связанный список для хранения труб:
void init_head()
{
Node tmp;
tmp = (node *)malloc(sizeof(node));
tmp->next = NULL;
head = tmp;
tail = head;
}
void init_wall()
{
int i;
Node tmp, p;
p = head;
for(i = 0; i < 5; i++)
{
tmp = (node *)malloc(sizeof(node));
tmp->x = (i + 1) * 19;
tmp->y = rand() % 11 + 5;
p->next = tmp;
tmp->next = NULL;
p = tmp;
}
tail = p;
}
Функция init_draw() инициализирует экран:
void init_draw()
{
Node p;
int i, j;
// Пробегаемся по связанному списку
for(p = head->next; p->next!= NULL; p = p->next)
{
// Рисуем трубы
for(i = p->x; i > p->x-10; i--)
{
for(j = 0; j < p->y; j++)
{
move(j, i);
addch(CHAR_STONE);
}
for(j = p->y+5; j <= 23; j++)
{
move(j, i);
addch(CHAR_STONE);
}
}
refresh();
sleep(1);
}
}
Таким образом, наша игра Flappy Bird завершена.
Компиляция и запуск
Выполните команду gcc для компиляции:
cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird

Резюме
В этом проекте мы использовали язык программирования C для реализации текстовой игры Flappy Bird. Студенты могут дальнейшим образом улучшить игру на основе этого курса, например, добавить цвета в трубы или сделать так, чтобы ширина труб случайным образом изменялась.



