Construindo Flappy Bird com C

CBeginner
Pratique Agora

Introdução

Flappy Bird é um jogo mobile popular e viciante que ganhou imensa popularidade por sua jogabilidade simples, mas desafiadora. Neste projeto, aprenderemos como implementar nossa própria versão do Flappy Bird usando a linguagem de programação C.

Ao seguir este projeto, você irá:

  • Aprender como usar a biblioteca ncurses para desenho de tela baseado em texto.
  • Adquirir conhecimento sobre estruturas de dados e chamadas de sistema no Linux.
  • Ganhar experiência no tratamento de eventos de teclado e atualizações em tempo real em um programa C.

👀 Pré-visualização

Flappy Bird Preview

🎯 Tarefas

Neste projeto, você aprenderá:

  • Como implementar a versão baseada em caracteres do Flappy Bird usando C.
  • Como lidar com eventos de teclado para controlar o movimento do pássaro.
  • Como criar a ilusão de movimento para frente movendo os obstáculos da direita para a esquerda.
  • Como usar a biblioteca ncurses para desenhar a interface de caracteres.

🏆 Conquistas

Após concluir este projeto, você será capaz de:

  • Demonstrar proficiência na linguagem de programação C.
  • Desenvolver habilidades no tratamento de eventos de teclado.
  • Implementar atualizações em tempo real em um programa C.
  • Utilizar a biblioteca ncurses para desenho de tela baseado em texto.
  • Compreender estruturas de dados e chamadas de sistema no Linux.

Conhecimentos Básicos

Nosso projeto requer algum conhecimento de estruturas de dados e envolve algumas chamadas de sistema no Linux.

Além disso, também usamos uma biblioteca de desenho de tela baseada em texto chamada ncurses. Portanto, ao compilar, a opção -lcurses precisa ser adicionada.

Ideias de Design

Para implementar a versão baseada em caracteres do Flappy Bird, começamos com a implementação dos três pontos-chave a seguir:

  1. O programa deve ser capaz de responder a eventos de teclado.
  2. A interface de caracteres deve ser capaz de atualizar em tempo real.
  3. O pássaro deve dar a ilusão visual de voar para frente.

Para os três problemas acima, nossas soluções são as seguintes:

  1. Use as interfaces do sistema fornecidas pelo Linux para capturar eventos de teclado.
  2. Use as funções da biblioteca ncurses para desenhar a interface de caracteres.
  3. Para criar a ilusão do pássaro voando para frente:

A abordagem mais direta é fazer o pássaro se mover da esquerda para a direita na direção horizontal, mas isso faria com que o pássaro excedesse o limite direito em algum momento.

Em vez disso, vamos pensar ao contrário: quando uma pessoa vê a paisagem fora do carro enquanto viaja para frente, ela parece estar se movendo para trás (o movimento é relativo). Então, deixamos os obstáculos se moverem da direita para a esquerda, o que atinge o mesmo efeito visual e evita o problema do pássaro exceder o limite.

Definir Constantes

Primeiramente, abra o terminal Xfce e execute o seguinte comando para instalar a biblioteca ncurses:

sudo apt update
sudo apt-get install libncurses5-dev

Navegue até o diretório ~/project e crie o arquivo do projeto flappy_bird.c:

cd ~/project
touch flappy_bird.c

Em seguida, precisamos escrever o código C. O primeiro passo é incluir os arquivos de cabeçalho:

## include <curses.h>
## include <stdlib.h>
## include <signal.h>
## include <sys/time.h>

Antes de escrever a função main(), vamos completar algumas tarefas básicas. Como estamos trabalhando com uma interface de caracteres do terminal, os caracteres ASCII são essenciais. Portanto, precisamos definir algumas constantes.

Usaremos * para representar os pilares no fundo e O para representar o pássaro. O código é o seguinte:

## define CHAR_BIRD 'O'  // Define o caractere do pássaro
## define CHAR_STONE '*'  // Define as pedras que compõem os pilares
## define CHAR_BLANK ' '  // Define o caractere vazio

Os pilares no fundo serão armazenados usando uma lista encadeada simples. A struct é definida da seguinte forma:

typedef struct node {
    int x, y;
    struct node *next;
}node, *Node;

Vamos também definir algumas variáveis globais:

Node head, tail;
int bird_x, bird_y;
int ticker;

Agora, declararemos as funções que criaremos:

void init();  // Função de inicialização que gerencia as tarefas de inicialização do jogo
void init_bird();  // Inicializa as coordenadas de posição do pássaro
void init_draw();  // Inicializa o fundo
void init_head();  // Inicializa a cabeça da lista encadeada que armazena os pilares
void init_wall();  // Inicializa a lista encadeada que armazena os pilares
void drop(int sig);  // Função de recebimento de sinal para receber sinais do sistema e mover os pilares da direita para a esquerda
int set_ticker(int n_msec);  // Define o intervalo de tick do temporizador do kernel

Problema de Tempo (Timing)

Agora, vamos resolver o problema de como fazer o fundo se mover em um intervalo regular. Usaremos a funcionalidade fornecida pelo sistema Linux, nomeadamente sinais.

Não tem certeza do que é um sinal? Sem problemas, você pode pensar nisso como um temporizador no kernel do Linux que envia um sinal para o nosso programa a cada certo período de tempo. Nossa função de tratamento de sinal drop(int sig) será executada automaticamente quando o sinal for recebido. Só precisamos mover o pilar na função drop(int sig). Além disso, como o sinal é enviado pelo kernel do Linux, não haverá nenhum bloqueio da nossa recepção de sinal do teclado devido ao recebimento do sinal.

Agora, vamos implementar nosso código e definir o período do temporizador do kernel usando a função 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);
}

Função de tratamento de sinal drop(int sig):

void drop(int sig)
{
    int j;
    Node tmp, p;

    // Clear the symbol at the original bird position
    move(bird_y, bird_x);
    addch(CHAR_BLANK);
    refresh();

    // Update the position of the bird and refresh the screen
    bird_y++;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();

    // End the game if the bird collides with the pillar
    if((char)inch() == CHAR_STONE)
    {
        set_ticker(0);
        sleep(1);
        endwin();
        exit(0);
    }

    // Check if the first wall goes beyond the boundary
    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;  // Accelerate
        set_ticker(ticker);
    }
    // Draw a new pillar
    for(p = head->next; p->next != NULL; p->x--, p = p->next)
    {
        // Replace CHAR_STONE with 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--;
}

Na função de tratamento de sinal, movemos o fundo para frente por uma coluna e, ao mesmo tempo, deixamos o pássaro cair por uma linha. Também verificamos se o pássaro colide com um pilar. Se isso acontecer, o jogo termina.

Função main()

Na função main(), primeiro chamamos a função de inicialização init(), e então entramos no loop while(). O loop consiste principalmente em três partes:

  1. Verificar a entrada do usuário: Se a tecla "w" ou a barra de espaço for pressionada, o pássaro se moverá para cima duas linhas. Se a tecla "q" for pressionada, o jogo será encerrado. Se a tecla "z" for pressionada, o jogo será pausado.
  2. Mover o pássaro e redesenhá-lo.
  3. Verificar se o pássaro atinge os canos.

Vamos dar uma olhada no código:

int main()
{
    char ch;

    init();
    while(1)
    {
        ch = getch();  // Get keyboard input
        if(ch == ' ' || ch == 'w' || ch == 'W')  // If spacebar or "w" key is pressed
        {
            // Move the bird and redraw it
            move(bird_y, bird_x);
            addch(CHAR_BLANK);
            refresh();
            bird_y--;
            move(bird_y, bird_x);
            addch(CHAR_BIRD);
            refresh();

            // If the bird hits the pipes, end the game
            if((char)inch() == CHAR_STONE)
            {
                set_ticker(0);
                sleep(1);
                endwin();
                exit(0);
            }
        }
        else if(ch == 'z' || ch == 'Z')  // Pause
        {
            set_ticker(0);
            do
            {
                ch = getch();
            } while(ch != 'z' && ch != 'Z');
            set_ticker(ticker);
        }
        else if(ch == 'q' || ch == 'Q')  // Quit
        {
            sleep(1);
            endwin();
            exit(0);
        }
    }
    return 0;
}

Na função main(), primeiro inicializamos a tela e, em seguida, recebemos a entrada do teclado em um loop. Se a tecla "w" ou a barra de espaço for pressionada, o pássaro se moverá para cima duas linhas. Se a tecla "q" for pressionada, o jogo será encerrado. Se a tecla "z" for pressionada, o jogo será pausado.

Agora, vamos dar uma olhada na função 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);
}

A função init() primeiro inicializa a tela usando funções fornecidas por ncurses. Em seguida, ela chama várias sub-funções para realizar inicializações específicas. Observe que instalamos uma função de tratamento de sinal drop() e definimos o intervalo do temporizador.

Vamos analisar cada sub-função de inicialização.

A função init_bird() inicializa a posição do pássaro:

void init_bird()
{
    bird_x = 5;
    bird_y = 15;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();
    sleep(1);
}

As funções init_head() e init_wall() inicializam uma lista encadeada para armazenar os canos:

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;
}

A função init_draw() inicializa a tela:

void init_draw()
{
    Node p;
    int i, j;

    // Traverse the linked list
    for(p = head->next; p->next != NULL; p = p->next)
    {
        // Draw the pipes
        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);
    }
}

Com isso, nosso jogo flappy_bird está completo.

Compilação e Execução

Execute o comando gcc para compilar:

cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird

Compiling Flappy Bird code

Resumo

Neste projeto, usamos a linguagem de programação C para implementar um jogo Flappy Bird baseado em texto. Os alunos podem aprimorar ainda mais o jogo com base neste curso, como adicionar cores aos canos ou fazer com que as larguras dos canos mudem aleatoriamente.

✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar✨ Verificar Solução e Praticar