Construyendo Flappy Bird con C

CCBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Flappy Bird es un juego móvil popular y adictivo que ganó una gran popularidad por su juego simple pero desafiante. En este proyecto, aprenderemos cómo implementar nuestra propia versión de Flappy Bird utilizando el lenguaje de programación C.

Siguiendo este proyecto, usted:

  • Aprenderá a usar la biblioteca ncurses para dibujos de pantalla basados en texto.
  • Adquirirá conocimientos sobre estructuras de datos y llamadas de sistema en Linux.
  • Ganará experiencia en el manejo de eventos de teclado y actualizaciones en tiempo real en un programa C.

👀 Vista previa

Vista previa de Flappy Bird

🎯 Tareas

En este proyecto, aprenderá:

  • Cómo implementar la versión basada en caracteres de Flappy Bird utilizando C.
  • Cómo manejar eventos de teclado para controlar el movimiento del pájaro.
  • Cómo crear la ilusión de movimiento hacia adelante al mover los obstáculos de derecha a izquierda.
  • Cómo utilizar la biblioteca ncurses para dibujar la interfaz de caracteres.

🏆 Logros

Después de completar este proyecto, podrá:

  • Demostrar habilidad en el lenguaje de programación C.
  • Desarrollar habilidades en el manejo de eventos de teclado.
  • Implementar actualizaciones en tiempo real en un programa C.
  • Utilizar la biblioteca ncurses para dibujos de pantalla basados en texto.
  • Comprender estructuras de datos y llamadas de sistema en Linux.

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("C")) -.-> c/ControlFlowGroup(["Control Flow"]) c(("C")) -.-> c/FunctionsGroup(["Functions"]) c(("C")) -.-> c/UserInteractionGroup(["User Interaction"]) c(("C")) -.-> c/BasicsGroup(["Basics"]) c(("C")) -.-> c/CompoundTypesGroup(["Compound Types"]) 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{{"Construyendo Flappy Bird con C"}} c/constants -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/if_else -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/while_loop -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/structures -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/function_declaration -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/function_parameters -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/user_input -.-> lab-298823{{"Construyendo Flappy Bird con C"}} c/output -.-> lab-298823{{"Construyendo Flappy Bird con C"}} end

Conocimientos básicos

Nuestro proyecto requiere algunos conocimientos de estructuras de datos y implica algunas llamadas de sistema en Linux.

Además, también utilizamos una biblioteca de dibujo de pantalla basada en texto llamada ncurses. Por lo tanto, cuando se compila, se debe agregar la opción -lcurses.

Ideas de diseño

Para implementar la versión basada en caracteres de Flappy Bird, comenzamos con la implementación de los siguientes tres puntos clave:

  1. El programa debe ser capaz de responder a eventos de teclado.
  2. La interfaz de caracteres debe ser capaz de actualizarse en tiempo real.
  3. El pájaro debe dar la ilusión visual de volar hacia adelante.

Para los tres problemas anteriores, nuestras soluciones son las siguientes:

  1. Utilizar las interfaces del sistema proporcionadas por Linux para capturar eventos de teclado.
  2. Utilizar funciones de la biblioteca ncurses para dibujar la interfaz de caracteres.
  3. Para crear la ilusión de que el pájaro vuela hacia adelante:

El enfoque más directo sería hacer que el pájaro se mueva de izquierda a derecha en la dirección horizontal, pero esto haría que el pájaro excediera el límite derecho en algún momento.

En cambio, pensemos al revés: cuando una persona ve la escena exterior del automóvil mientras viaja hacia adelante, parece que está moviéndose hacia atrás (el movimiento es relativo). Entonces, hacemos que los obstáculos se muevan de derecha a izquierda, lo que logra el mismo efecto visual y evita el problema de que el pájaro exceda el límite.

Definir constantes

Primero, abre la terminal Xfce y ejecuta el siguiente comando para instalar la biblioteca ncurses:

sudo apt update
sudo apt-get install libncurses5-dev

Navega hasta el directorio ~/project y crea el archivo del proyecto flappy_bird.c:

cd ~/project
touch flappy_bird.c

A continuación, necesitamos escribir el código C. El primer paso es incluir los archivos de encabezado:

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

Antes de escribir la función main(), completemos algunas tareas básicas. Dado que estamos trabajando con una interfaz de caracteres de terminal, los caracteres ASCII son esenciales. Por lo tanto, necesitamos definir algunas constantes.

Usaremos * para representar las columnas en el fondo y O para representar el pájaro. El código es el siguiente:

## define CHAR_BIRD 'O'  // Define el carácter del pájaro
## define CHAR_STONE '*'  // Define las piedras que forman las columnas
## define CHAR_BLANK ' '  // Define el carácter vacío

Las columnas en el fondo se almacenarán usando una lista enlazada simple. La estructura se define como sigue:

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

Definamos también algunas variables globales:

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

Ahora declararemos las funciones que crearemos:

void init();  // Función de inicialización que gestiona las tareas de inicialización del juego
void init_bird();  // Inicializa las coordenadas de posición del pájaro
void init_draw();  // Inicializa el fondo
void init_head();  // Inicializa la cabeza de la lista enlazada que almacena las columnas
void init_wall();  // Inicializa la lista enlazada que almacena las columnas
void drop(int sig);  // Función de recepción de señales para recibir señales del sistema y mover las columnas de derecha a izquierda
int set_ticker(int n_msec);  // Establece el intervalo de tic del temporizador del kernel
✨ Revisar Solución y Practicar

Problema de temporización

Ahora resolvamos el problema de cómo hacer que el fondo se mueva a intervalos regulares. Utilizaremos la funcionalidad proporcionada por el sistema Linux, es decir, señales.

¿No estás seguro de qué es una señal? No te preocupes, puedes pensar en ella como un temporizador en el kernel de Linux que envía una señal a nuestro programa cada cierto período de tiempo. Nuestra función manejadora de señales drop(int sig) se ejecutará automáticamente cuando se reciba la señal. Simplemente debemos mover la columna en la función drop(int sig). Además, dado que la señal es enviada por el kernel de Linux, no habrá ningún bloqueo en la recepción de señales de teclado debido a la recepción de la señal.

Ahora implementemos nuestro código y establezcamos el período del temporizador del kernel utilizando la función 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);
}

Función manejadora de señales drop(int sig):

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

    // Borra el símbolo en la posición original del pájaro
    move(bird_y, bird_x);
    addch(CHAR_BLANK);
    refresh();

    // Actualiza la posición del pájaro y actualiza la pantalla
    bird_y++;
    move(bird_y, bird_x);
    addch(CHAR_BIRD);
    refresh();

    // Termina el juego si el pájaro choca con la columna
    if((char)inch() == CHAR_STONE)
    {
        set_ticker(0);
        sleep(1);
        endwin();
        exit(0);
    }

    // Verifica si la primera pared sale del límite
    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;  // Acelera
        set_ticker(ticker);
    }
    // Dibuja una nueva columna
    for(p = head->next; p->next!= NULL; p->x--, p = p->next)
    {
        // Reemplaza CHAR_STONE con 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--;
}

En la función manejadora de señales, movemos el fondo hacia adelante una columna, y al mismo tiempo, hacemos que el pájaro baje una fila. También verificamos si el pájaro choca con una columna. Si es así, el juego ha terminado.

✨ Revisar Solución y Practicar

Función main()

En la función main(), primero llamamos a la función de inicialización init(), y luego entramos en el bucle while(). El bucle se compone principalmente de tres partes:

  1. Verificar la entrada del usuario: Si se presiona la tecla "w" o la barra espaciadora, el pájaro se moverá dos filas hacia arriba. Si se presiona la tecla "q", el juego se cerrará. Si se presiona la tecla "z", el juego se pausará.
  2. Mover el pájaro y redibujarlo.
  3. Verificar si el pájaro choca con las tuberías.

Echemos un vistazo al código:

int main()
{
    char ch;

    init();
    while(1)
    {
        ch = getch();  // Obtener la entrada del teclado
        if(ch == ' ' || ch == 'w' || ch == 'W')  // Si se presiona la barra espaciadora o la tecla "w"
        {
            // Mover el pájaro y redibujarlo
            move(bird_y, bird_x);
            addch(CHAR_BLANK);
            refresh();
            bird_y--;
            move(bird_y, bird_x);
            addch(CHAR_BIRD);
            refresh();

            // Si el pájaro choca con las tuberías, terminar el juego
            if((char)inch() == CHAR_STONE)
            {
                set_ticker(0);
                sleep(1);
                endwin();
                exit(0);
            }
        }
        else if(ch == 'z' || ch == 'Z')  // Pausa
        {
            set_ticker(0);
            do
            {
                ch = getch();
            } while(ch!= 'z' && ch!= 'Z');
            set_ticker(ticker);
        }
        else if(ch == 'q' || ch == 'Q')  // Salir
        {
            sleep(1);
            endwin();
            exit(0);
        }
    }
    return 0;
}

En la función main(), primero inicializamos la pantalla, y luego recibimos la entrada del teclado en un bucle. Si se presiona la tecla "w" o la barra espaciadora, el pájaro se moverá dos filas hacia arriba. Si se presiona la tecla "q", el juego se cerrará. Si se presiona la tecla "z", el juego se pausará.

Ahora echemos un vistazo a la función 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);
}

La función init() primero inicializa la pantalla utilizando funciones proporcionadas por ncurses. Luego llama a varias sub-funciones para realizar inicializaciones específicas. Tenga en cuenta que instalamos una función manejadora de señales drop(), y establecemos el intervalo del temporizador.

Echemos un vistazo a cada sub-función de inicialización.

La función init_bird() inicializó la posición del pájaro:

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

Las funciones init_head() e init_wall() inicializan una lista enlazada para almacenar las tuberías:

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

La función init_draw() inicializa la pantalla:

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

    // Recorrer la lista enlazada
    for(p = head->next; p->next!= NULL; p = p->next)
    {
        // Dibujar las tuberías
        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);
    }
}

Con esto, nuestro juego flappy_bird está completo.

✨ Revisar Solución y Practicar

Compilación y ejecución

Ejecute el comando gcc para compilar:

cd ~/project
gcc -o flappy_bird flappy_bird.c -lcurses
./flappy_bird
Compiling Flappy Bird code
✨ Revisar Solución y Practicar

Resumen

En este proyecto, utilizamos el lenguaje de programación C para implementar un juego de Flappy Bird basado en texto. Los estudiantes pueden mejorar aún más el juego sobre la base de este curso, por ejemplo, agregando colores a las tuberías o haciendo que los anchos de las tuberías cambien al azar.