How to Make a Flappy Bird Game in Python

Learn how to build a flappy bird game from scratch using the Pygame library in Python.
  · 16 min read · Updated aug 2023 · Game Development

Welcome! Meet our Python Code Assistant, your new coding buddy. Why wait? Start exploring now!

Flappy Bird is a classic and addictive game that has captured the hearts of millions with its simple yet challenging gameplay. In this tutorial, we will guide you through the process of building your very own Flappy Bird game from scratch, using the Pygame module, a popular Python library for game development.

With just a basic understanding of Python and a dash of creativity, you can harness Pygame's capabilities to create a fun and engaging gaming experience that can be customized to your liking.

Game Setup

Let's start by making sure Pygame is installed in your computer, head to your terminal and install pygame module using pip:

$ pip install pygame

After that, create a directory for the game and create the following .py files inside it: settings.py, main.py, world.py, game.py, pipe.py, bird.py. Create also another folder inside the game directory and name it assets, which we'll use to store game media files. Here is the file structure of our code:

Now we can start coding. Let's define our game variables and functions in settings.py:

# settings.py
from os import walk
import pygame

WIDTH, HEIGHT = 600, 650

pipe_pair_sizes = [
    (1, 7),
    (2, 6),
    (3, 5),
    (4, 4),
    (5, 3),
    (6, 2),
    (7, 1)
]
pipe_size = HEIGHT // 10
pipe_gap = (pipe_size * 2) + (pipe_size // 2)
ground_space = 50

def import_sprite(path):
    surface_list = []
    for _, __, img_file in walk(path):
        for image in img_file:
            full_path = f"{path}/{image}"
            img_surface = pygame.image.load(full_path).convert_alpha()
            surface_list.append(img_surface)
    return surface_list

Next, let's create the main class of our game in main.py which also contains the game's main loop:

# main.py
import pygame, sys
from settings import WIDTH, HEIGHT, ground_space
from world import World

pygame.init()

screen = pygame.display.set_mode((WIDTH, HEIGHT + ground_space))
pygame.display.set_caption("Flappy Bird")

class Main:
    def __init__(self, screen):
        self.screen = screen
        self.bg_img = pygame.image.load('assets/terrain/bg.png')
        self.bg_img = pygame.transform.scale(self.bg_img, (WIDTH, HEIGHT))
        self.ground_img = pygame.image.load('assets/terrain/ground.png')
        self.ground_scroll = 0
        self.scroll_speed = -6
        self.FPS = pygame.time.Clock()
        self.stop_ground_scroll = False

    def main(self):
        world = World(screen)
        while True:
            self.stop_ground_scroll = world.game_over
            self.screen.blit(self.bg_img, (0, 0))
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == pygame.KEYDOWN:
                    if not world.playing and not world.game_over:
                        world.playing = True
                    if event.key == pygame.K_SPACE:
                        world.update("jump")
                    if event.key == pygame.K_r:
                        world.update("restart")
            world.update()
            self.screen.blit(self.ground_img, (self.ground_scroll, HEIGHT))
            if not self.stop_ground_scroll:
                self.ground_scroll += self.scroll_speed
                if abs(self.ground_scroll) > 35:
                    self.ground_scroll = 0
            pygame.display.update()
            self.FPS.tick(60)

if __name__ == "__main__":
    play = Main(screen)
    play.main()

The main() function serves as the entry point of the game and is responsible for running the game loop. The game loop is designed to keep the game running continuously until it is exited or closed by the player.

At the beginning of the loop, a World instance called world is created, which represents the game world and holds information about the game's state. The World class handles the gameplay mechanics, such as the bird's movement and collision detection with obstacles. Inside the loop, the first step is to set the self.stop_ground_scroll to the current value of world.game_over. This controls whether the scrolling of the ground should stop when the game is over, providing a smooth transition between game sessions. Next, the background image (self.bg_img) is drawn onto the game's screen at the coordinates (0, 0) using self.screen.blit() method. This ensures that the screen is refreshed with the updated background image before other elements are drawn.

The game then checks for events using pygame.event.get() to detect any user input, such as clicking the close button to quit the game. If the user presses the space key, the world.update("jump") method is called to handle the bird's jump action. If the 'r' key is pressed, the world.update("restart") method is triggered, allowing the player to restart the game after a game-over.

After handling events, the world.update() method updates the game state and applies physics to the game objects. This includes moving the bird and the obstacles, checking for collisions, and updating the score.

Following the world update, the ground image (self.ground_img) is drawn onto the screen at the self.ground_scroll position, creating the illusion of the ground scrolling. If self.stop_ground_scroll is False, the ground will scroll horizontally based on the self.scroll_speed value. The scrolling creates the impression of continuous movement in the game world.

Finally, pygame.display.update() is called to update the game display with the latest changes made in this iteration of the game loop. The self.FPS.tick(60) line ensures that the game runs at a maximum frame rate of 60 frames per second, controlling the speed of the game loop and making it consistent across different devices.

[here]

Creating the Game World

Now that we have our game loop and main class, let's move on to creating our class for the game world:

# world.py
import pygame
from pipe import Pipe
from bird import Bird
from game import GameIndicator
from settings import WIDTH, HEIGHT, pipe_size, pipe_gap, pipe_pair_sizes
import random

class World:
    def __init__(self, screen):
        self.screen = screen
        self.world_shift = 0
        self.current_x = 0
        self.gravity = 0.5
        self.current_pipe = None
        self.pipes = pygame.sprite.Group()
        self.player = pygame.sprite.GroupSingle()
        self._generate_world()
        self.playing = False
        self.game_over = False
        self.passed = True
        self.game = GameIndicator(screen)

    # adds pipe once the last pipe added reached the desired pipe horizontal spaces
    def _add_pipe(self):
        pipe_pair_size = random.choice(pipe_pair_sizes)
        top_pipe_height, bottom_pipe_height = pipe_pair_size[0] * pipe_size, pipe_pair_size[1] * pipe_size
        pipe_top = Pipe((WIDTH, 0 - (bottom_pipe_height + pipe_gap)), pipe_size, HEIGHT, True)
        pipe_bottom = Pipe((WIDTH, top_pipe_height + pipe_gap), pipe_size, HEIGHT, False)
        self.pipes.add(pipe_top)
        self.pipes.add(pipe_bottom)
        self.current_pipe = pipe_top

    # creates the player and the obstacle
    def _generate_world(self):
        self._add_pipe()
        bird = Bird((WIDTH//2 - pipe_size, HEIGHT//2 - pipe_size), 30)
        self.player.add(bird)

Once our main() function initializes a world using the World class, the _generate_world() function will be called which generates the game world by adding a pipe (using self._add_pipe()) and the bird player character to the game. With random.choice(pipe_pair_sizes) in _add_pipe() function, we can have a pair of pipe obstacles to the game with random sizes.

Next, let's add some world components for handling the game physics:

# world.py
    # for moving background/obstacle
    def _scroll_x(self):
        if self.playing:
            self.world_shift = -6
        else:
            self.world_shift = 0

    # add gravity to bird for falling
    def _apply_gravity(self, player):
        if self.playing or self.game_over:
            player.direction.y += self.gravity
            player.rect.y += player.direction.y

    # handles scoring and collision
    def _handle_collisions(self):
        bird = self.player.sprite
        # for collision checking
        if pygame.sprite.groupcollide(self.player, self.pipes, False, False) or bird.rect.bottom >= HEIGHT or bird.rect.top <= 0:
            self.playing = False
            self.game_over = True
        else:
            # if player pass through the pipe gaps
            bird = self.player.sprite
            if bird.rect.x >= self.current_pipe.rect.centerx:
                bird.score += 1
                self.passed = True

The _scroll_x() function is responsible for moving the game background and obstacles horizontally, creating the illusion of a scrolling effect. When the game is in the "playing" state, it sets the self.world_shift value to -6, causing the game world to move to the left. When the game is not in the "playing" state (e.g., game over or not started yet), self.world_shift is set to 0, halting the scrolling effect.

The _apply_gravity() function adds gravity to the game's bird character, causing it to fall gradually. When the game is in either the "playing" or "game over" state, the bird's direction.y (vertical movement) is increased by the gravity value, simulating a downward force. The bird's position is then updated accordingly by adding the direction.y value to its current vertical position.

The _handle_collisions() function manages the game's collision detection, scoring, and game-over conditions. It checks for collisions between the player (bird) and the pipes. If a collision occurs, or if the bird's rect (collision bounding box) goes above the screen or below the game's height, the game enters the "game over" state, setting self.playing to False and self.game_over to True. Otherwise, if the player successfully passes through the gap between the pipes, their score is incremented, and self.passed is set to True. This function effectively determines the core gameplay mechanics of scoring points and detecting game-ending collisions.

Let's create one more function in the world class which combines all of the world's characteristics:

# world.py
    # updates the bird's overall state
    def update(self, player_event = None):
        # new pipe adder
        if self.current_pipe.rect.centerx  <= (WIDTH // 2) - pipe_size:
            self._add_pipe()
        # updates, draws pipes
        self.pipes.update(self.world_shift)
        self.pipes.draw(self.screen)
        # applying game physics
        self._apply_gravity(self.player.sprite)
        self._scroll_x()
        self._handle_collisions()
        # configuring player actions
        if player_event == "jump" and not self.game_over:
            player_event = True
        elif player_event == "restart":
            self.game_over = False
            self.pipes.empty()
            self.player.empty()
            self.player.score = 0
            self._generate_world()
        else:
            player_event = False
        if not self.playing:
            self.game.instructions()
        # updates, draws pipes
        self.player.update(player_event)
        self.player.draw(self.screen)
        self.game.show_score(self.player.sprite.score)

The update() function is responsible for managing various aspects of the game, including scrolling the pipes, applying gravity to the bird, handling collisions, responding to player input, updating the player's score, and controlling the game's flow.

To start, the function keeps track of the current pipe's position, and when it reaches a certain point on the screen (the middle minus half the pipe size), it adds a new pipe to keep the gameplay challenging. The function then updates the positions of all the pipes and applies the scrolling effect to give the impression of the bird continuously flying through a dynamic environment. Next, gravity is applied to the bird's vertical movement, making it fall gradually. The function ensures that gravity is only applied when the game is in the "playing" or "game over" state.

To handle collisions and game-over conditions, the function checks for collisions between the bird and the pipes or the screen boundaries. If a collision occurs, the game enters the "game over" state, halting the gameplay and displaying the final score. The function also responds to player input events. If the player requests a jump (by pressing a key or tapping the screen), it triggers the bird's upward movement. Additionally, the function allows the player to restart the game after a game-over, resetting all game elements to their initial state.

When the game is not in the "playing" state, the function displays the game instructions, guiding the player on how to play. Finally, the function updates and displays the player's score on the screen during gameplay, incrementing the score whenever the bird successfully passes through the gaps between pipes.

For the handling game instructions and player score, let's create another class and name it GameIndicator:

# game.py
import pygame
from settings import WIDTH, HEIGHT

pygame.font.init()

class GameIndicator:
    def __init__(self, screen):
        self.screen = screen
        self.font = pygame.font.SysFont('Bauhaus 93', 60)
        self.inst_font = pygame.font.SysFont('Bauhaus 93', 30)
        self.color = pygame.Color("white")
        self.inst_color = pygame.Color("black")

    def show_score(self, int_score):
        bird_score = str(int_score)
        score = self.font.render(bird_score, True, self.color)
        self.screen.blit(score, (WIDTH // 2, 50))

    def instructions(self):
        inst_text1 = "Press SPACE button to Jump,"
        inst_text2 = "Press \"R\" Button to Restart Game."
        ins1 = self.inst_font.render(inst_text1, True, self.inst_color)
        ins2 = self.inst_font.render(inst_text2, True, self.inst_color)
        self.screen.blit(ins1, (95, 400))
        self.screen.blit(ins2, (70, 450))

In the show_score() function, the player's score is converted to a string, and then rendered using the specified font and color. The resulting score text is then placed on the screen at the horizontal center and 50 pixels from the top. As for the instructions() function, it renders the game instructions using two strings for the text. These instructions are displayed on the screen with a different font and color, appearing at specific positions (95, 400) and (70, 450) respectively, providing guidance to the player on how to jump and restart the game.

Adding Game Components

And for the game components, we have the bird which represents the player character, and the pipes as the game obstacles. Let's create the Bird class first in bird.py.

# bird.py
import pygame
from settings import import_sprite

class Bird(pygame.sprite.Sprite):
    def __init__(self, pos, size):
        super().__init__()
        # bird basic info
        self.frame_index = 0
        self.animation_delay = 3
        self.jump_move = -9
        # bird animation
        self.bird_img = import_sprite("assets/bird")
        self.image = self.bird_img[self.frame_index]
        self.image = pygame.transform.scale(self.image, (size, size))
        self.rect = self.image.get_rect(topleft = pos)
        self.mask = pygame.mask.from_surface(self.image)
        # bird status
        self.direction = pygame.math.Vector2(0, 0)
        self.score = 0

    # for bird's flying animation
    def _animate(self):
        sprites = self.bird_img
        sprite_index = (self.frame_index // self.animation_delay) % len(sprites)
        self.image = sprites[sprite_index]
        self.frame_index += 1
        self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y))
        self.mask = pygame.mask.from_surface(self.image)
        if self.frame_index // self.animation_delay > len(sprites):
            self.frame_index = 0

    # to make the bird fly higher
    def _jump(self):
        self.direction.y = self.jump_move

    # updates the bird's overall state
    def update(self, is_jump):
        if is_jump:
            self._jump()
        self._animate()

The Bird class inherits from pygame.sprite.Sprite, making it suitable for use with sprite groups in Pygame. It is initialized with a position pos and a  size, along with various attributes related to the bird's animation, movement, and status.

In the __init__() method, the bird's basic information, such as the current frame index for animation, the animation delay, and the jump movement (how high the bird jumps), is set. The bird's animation frames are loaded from the specified image file using the import_sprite() function from the settings module. The first frame of the animation is scaled to the desired size, and its position is set to pos. The bird's mask, used for collision detection, is also initialized based on the image's transparency. The bird's direction is represented by a 2D vector (pygame.math.Vector2) initially set to (0, 0), representing no initial movement. The score attribute is used to keep track of the player's score during gameplay.

The _animate() method handles the bird's animation. It iterates through the bird's animation frames, updating the displayed image at a specified delay to create a smooth animation. Once the last frame is reached, the animation resets to the first frame, creating a looping effect.

The _jump() method is responsible for making the bird fly higher. It updates the bird's direction.y attribute to a negative value (self.jump_move), causing the bird to move upwards when called.

The update() method is the core of the bird's state. It takes a boolean argument is_jump, indicating whether the bird should perform a jump. If is_jump is true, the bird's _jump() method is called to make it move upwards. The _animate() method is then called to handle the animation of the bird.

Let's now create a class for pipes in our game:

# pipe.py
import pygame

class Pipe(pygame.sprite.Sprite):
    def __init__(self, pos, width, height, flip):
        super().__init__()
        self.width = width
        img_path = 'assets/terrain/pipe.png'
        self.image = pygame.image.load(img_path)
        self.image = pygame.transform.scale(self.image, (width, height))
        if flip:
            flipped_image = pygame.transform.flip(self.image, False, True)
            self.image = flipped_image
        self.rect = self.image.get_rect(topleft = pos)

    # update object position due to world scroll
    def update(self, x_shift):
        self.rect.x += x_shift
        # removes the pipe in the game screen once it is not shown in the screen anymore
        if self.rect.right < (-self.width):
            self.kill()

[here]

The Pipe class manages the pipes' appearance, positioning, and updates during the gameplay.

In the __init__() method, the Pipe class is initialized with the pos (position), width, height, and flip parameters. The image file path for the pipe is set, and the image is loaded and scaled to the specified width and height. If flip is True, the image is flipped vertically to create the illusion of the pipes coming from the top.

The update() method is responsible for updating the position of the pipe due to the world scroll. It takes the x_shift parameter, which represents the horizontal shift caused by the world scrolling. This method increments the self.rect.x (horizontal position) of the pipe by x_shift, effectively moving the pipe to the left along with the world scrolling.

Additionally, the update() method checks if the pipe has moved entirely off the left side of the game screen (when self.rect.right is less than -self.width or the width of the pipe). If this happens, it calls the kill() built-in method to remove the pipe from the game, freeing up resources and maintaining a clean game environment.

And now, we are done coding the game. To test if everything works, run the main.py file. Here are some of the game snapshots:

Check the demo video:

In this tutorial, we've explored the process of creating the iconic Flappy Bird game using Python and the Pygame library. With a straightforward and concise approach, we have built a game that challenges players to navigate a bird through a series of pipes with just a few lines of code. By understanding the game loop, physics, collisions, and animation, we've constructed a dynamic and engaging gaming experience.

Through the Bird class, we managed the bird's animation, movement, and status, while the Pipe class controlled the appearance, positioning, and updates of the pipes. The World class played a central role in orchestrating the game's core mechanics, handling events, updating the game state, and rendering graphics.

By following this guide, you now possess the knowledge and tools to venture further into the world of Python game development. With your newfound skills, you can explore additional features, such as sound effects, multiple levels, and high-score tracking, to enhance the Flappy Bird game or even create your own original games.

Check the complete code here.

Related game tutorials:

Happy coding ♥

Found the article interesting? You'll love our Python Code Generator! Give AI a chance to do the heavy lifting for you. Check it out!

View Full Code Switch My Framework
Sharing is caring!



Read Also



Comment panel

    Got a coding query or need some guidance before you comment? Check out this Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!