Разработка Flappy Bird на C

CCBeginner
Практиковаться сейчас

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Flappy Bird - это популярная и увлекательная мобильная игра, которая приобрела огромную популярность из-за простого, но вызывающего сложности игрового процесса. В этом проекте мы научимся реализовывать свою версию Flappy Bird с использованием языка программирования C.

Следуя этому проекту, вы:

  • научитесь использовать библиотеку ncurses для текстовой отрисовки экрана;
  • приобретете знания о структурах данных и системных вызовах в Linux;
  • получите опыт работы с событиями клавиатуры и реальными обновлениями в программе на C.

👀 Предпросмотр

Предпросмотр Flappy Bird

🎯 Задачи

В этом проекте вы научитесь:

  • реализовывать текстовую версию Flappy Bird на C;
  • обрабатывать события клавиатуры для управления движением птицы;
  • создавать иллюзию движения вперед путем перемещения препятствий с права налево;
  • использовать библиотеку ncurses для отрисовки текстового интерфейса.

🏆 Достижения

После завершения этого проекта вы сможете:

  • продемонстрировать мастерство в языке программирования C;
  • развить навыки обработки событий клавиатуры;
  • реализовать реальные обновления в программе на C;
  • использовать библиотеку ncurses для текстовой отрисовки экрана;
  • понять структуры данных и системные вызовы в Linux.

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("C")) -.-> c/FunctionsGroup(["Functions"]) c(("C")) -.-> c/BasicsGroup(["Basics"]) c(("C")) -.-> c/ControlFlowGroup(["Control Flow"]) c(("C")) -.-> c/CompoundTypesGroup(["Compound Types"]) c(("C")) -.-> c/UserInteractionGroup(["User Interaction"]) c/BasicsGroup -.-> c/variables("Variables") c/BasicsGroup -.-> c/constants("Constants") c/ControlFlowGroup -.-> c/if_else("If...Else") c/ControlFlowGroup -.-> c/while_loop("While Loop") c/CompoundTypesGroup -.-> c/structures("Structures") c/FunctionsGroup -.-> c/function_declaration("Function Declaration") c/FunctionsGroup -.-> c/function_parameters("Function Parameters") c/UserInteractionGroup -.-> c/user_input("User Input") c/UserInteractionGroup -.-> c/output("Output") subgraph Lab Skills c/variables -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/constants -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/if_else -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/while_loop -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/structures -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/function_declaration -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/function_parameters -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/user_input -.-> lab-298823{{"Разработка Flappy Bird на C"}} c/output -.-> lab-298823{{"Разработка Flappy Bird на C"}} end

Основные знания

Наше проектирование требует некоторых знаний о структурах данных и включает в себя некоторые системные вызовы в Linux.

Кроме того, мы также используем библиотеку для текстовой отрисовки экрана под названием ncurses. Поэтому при компиляции необходимо добавить опцию -lcurses.

Идеи дизайна

Для реализации текстовой версии Flappy Bird мы начинаем с реализации следующих трех ключевых моментов:

  1. Программа должна быть способна реагировать на события клавиатуры.
  2. Текстовый интерфейс должен быть способен обновляться в режиме реального времени.
  3. Птица должна создавать визуальную иллюзию полета вперед.

Для решения вышеперечисленных трех задач наши решения следующие:

  1. Используем системные интерфейсы, предоставляемые Linux, для захвата событий клавиатуры.
  2. Используем функции библиотеки ncurses для отрисовки текстового интерфейса.
  3. Чтобы создать иллюзию полета птицы вперед:

Самый простой подход - заставить птицу двигаться слева направо по горизонтали, но в таком случае птица в какой-то момент выйдет за правую границу.

Вместо этого давайте подумаем наоборот: когда человек едет вперед и видит пейзаж снаружи машины, это кажется, что движется назад (движение относительное). Поэтому мы заставляем препятствия двигаться справа налево, что дает тот же визуальный эффект и избавляется от проблемы выхода птицы за границу.

Определение констант

Сначала откройте терминал 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, &timeset, 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(). Цикл主要 состоит из трех частей:

  1. Проверяем ввод пользователя: Если нажата клавиша "w" или пробел, птица поднимется на две строки. Если нажата клавиша "q", игра завершится. Если нажата клавиша "z", игра будет приостановлена.
  2. Перемещаем птицу и перерисовываем ее.
  3. Проверяем, столкнулась ли птица с трубами.

Посмотрим на код:

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
Compiling Flappy Bird code
✨ Проверить решение и практиковаться

Обзор

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