Building Flappy Bird Using C

CCBeginner
Practice Now

Introduction

Flappy Bird is a popular and addictive mobile game that gained immense popularity for its simple yet challenging gameplay. In this project, we will learn how to implement our own version of Flappy Bird using the C programming language.

By following this project, you will:

  • Learn how to use the ncurses library for text-based screen drawing.
  • Acquire knowledge of data structures and system calls in Linux.
  • Gain experience in handling keyboard events and real-time updates in a C program.

๐Ÿ‘€ Preview

Flappy Bird Preview

๐ŸŽฏ Tasks

In this project, you will learn:

  • How to implement the character-based version of Flappy Bird using C.
  • How to handle keyboard events to control the bird's movement.
  • How to create the illusion of forward motion by moving obstacles from right to left.
  • How to use the ncurses library to draw the character interface.

๐Ÿ† Achievements

After completing this project, you will be able to:

  • Demonstrate proficiency in C programming language.
  • Develop skills in handling keyboard events.
  • Implement real-time updates in a C program.
  • Utilize the ncurses library for text-based screen drawing.
  • Understand data structures and system calls in Linux.

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("`C`")) -.-> c/BasicsGroup(["`Basics`"]) c(("`C`")) -.-> c/ControlFlowGroup(["`Control Flow`"]) c(("`C`")) -.-> c/PointersandMemoryGroup(["`Pointers and Memory`"]) c(("`C`")) -.-> c/CompoundTypesGroup(["`Compound Types`"]) c(("`C`")) -.-> c/FunctionsGroup(["`Functions`"]) c/BasicsGroup -.-> c/comments("`Comments`") c/BasicsGroup -.-> c/variables("`Variables`") c/BasicsGroup -.-> c/data_types("`Data Types`") c/BasicsGroup -.-> c/operators("`Operators`") c/ControlFlowGroup -.-> c/if_else("`If...Else`") c/ControlFlowGroup -.-> c/while_loop("`While Loop`") c/ControlFlowGroup -.-> c/for_loop("`For Loop`") c/PointersandMemoryGroup -.-> c/memory_address("`Memory Address`") c/PointersandMemoryGroup -.-> c/pointers("`Pointers`") c/CompoundTypesGroup -.-> c/structures("`Structures`") c/FunctionsGroup -.-> c/function_parameters("`Function Parameters`") c/FunctionsGroup -.-> c/function_declaration("`Function Declaration`") c/FunctionsGroup -.-> c/recursion("`Recursion`") subgraph Lab Skills c/comments -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/variables -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/data_types -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/operators -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/if_else -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/while_loop -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/for_loop -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/memory_address -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/pointers -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/structures -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/function_parameters -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/function_declaration -.-> lab-298823{{"`Building Flappy Bird Using C`"}} c/recursion -.-> lab-298823{{"`Building Flappy Bird Using C`"}} end

Basic Knowledge

Our project requires some knowledge of data structures and involves some system calls in Linux.

In addition, we also use a text-based screen drawing library called ncurses. So, when compiling, the -lcurses option needs to be added.

Design Ideas

To implement the character-based version of Flappy Bird, we start with the implementation of the following three key points:

  1. The program should be able to respond to keyboard events.
  2. The character interface should be able to update in real-time.
  3. The bird should give the visual illusion of flying forward.

For the above three problems, our solutions are as follows:

  1. Use the system interfaces provided by Linux to capture keyboard events.
  2. Use ncurses library functions to draw the character interface.
  3. To create the illusion of the bird flying forward:

The most straightforward approach is to make the bird move from left to right in the horizontal direction, but this would cause the bird to exceed the right boundary at some point.

Instead, let's think in reverse: when a person sees the scenery outside the car while traveling forward, it appears to be moving backward (motion is relative). So, we let the obstacles move from right to left, which achieves the same visual effect and avoids the issue of the bird exceeding the boundary.

Define Constants

First, open the Xfce terminal and execute the following command to install the ncurses library:

sudo apt-get install libncurses5-dev

Navigate to the ~/project directory and create the project file flappy_bird.c:

cd ~/project
touch flappy_bird.c

Next, we need to write the C code. The first step is to include the header files:

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

Before writing the main() function, let's complete some basic tasks. Since we are working with a terminal character interface, ASCII characters are essential. Therefore, we need to define some constants.

We will use * to represent the pillars in the background and O to represent the bird. The code is as follows:

## define CHAR_BIRD 'O'  // Define the bird character
## define CHAR_STONE '*'  // Define the stones that make up the pillars
## define CHAR_BLANK ' '  // Define the empty character

The pillars in the background will be stored using a singly linked list. The struct is defined as follows:

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

Let's also define some global variables:

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

We will now declare the functions that we will create:

void init();  // Initialization function that manages the initialization tasks of the game
void init_bird();  // Initialize the bird's position coordinates
void init_draw();  // Initialize the background
void init_head();  // Initialize the linked list head that stores the pillars
void init_wall();  // Initialize the linked list that stores the pillars
void drop(int sig);  // Signal reception function to receive system signals and move the pillars from right to left
int set_ticker(int n_msec);  // Set the kernel's timer tick interval

Timing Issue

Now let's solve the problem of how to make the background move at a regular interval. We will use the functionality provided by the Linux system, namely signals.

Not sure what a signal is? No worries, you can think of it as a timer in the Linux kernel that sends a signal to our program every certain period of time. Our signal handler function drop(int sig) will be automatically executed when the signal is received. We just need to move the pillar in the drop(int sig) function. Additionally, since the signal is sent by the Linux kernel, there won't be any blocking of our keyboard signal reception due to receiving the signal.

Now let's implement our code and set the kernel's timer period using the set_ticker(int n_msec) function:

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

Signal handler function 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--;
}

In the signal handler function, we move the background forward by one column, and at the same time, we let the bird fall down by one row. We also check if the bird collides with a pillar. If it does, it's game over.

main() Function

In the main() function, we first call the initialization function init(), and then enter the while() loop. The loop mainly consists of three parts:

  1. Check the user's input: If the "w" key or the spacebar is pressed, the bird will move up two rows. If the "q" key is pressed, the game will exit. If the "z" key is pressed, the game will pause.
  2. Move the bird and redraw it.
  3. Check if the bird hits the pipes.

Let's take a look at the code:

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

In the main() function, we first initialize the screen, and then receive keyboard input in a loop. If the "w" key or spacebar is pressed, the bird will move up two rows. If the "q" key is pressed, the game will exit. If the "z" key is pressed, the game will pause.

Now let's take a look at the init() function:

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

The init() function first initializes the screen using functions provided by ncurses. Then it calls several sub-functions to perform specific initializations. Note that we install a signal handler function drop(), and set the timer interval.

Let's look at each initialization sub-function.

The init_bird() function initialized the bird's position:

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

The init_head() and init_wall() functions initialize a linked list to store the pipes:

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

The init_draw() function initializes the screen:

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

With this, our flappy_bird game is complete.

Compilation and Running

Execute the gcc command to compile:

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

Summary

In this project, we used the C programming language to implement a text-based Flappy Bird game. Students can further enhance the game based on this course, such as adding colors to the pipes or making the pipe widths randomly change.

Other C Tutorials you may like