Juggling between coding languages? Let our Code Converter help. Your one-stop solution for language conversion. Start 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.
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]
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.
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 ♥
Liked what you read? You'll love what you can learn from our AI-powered Code Explainer. Check it out!
View Full Code Switch My Framework
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!