🚀 Supercharge your YouTube channel's growth with AI.
Try YTGrowAI FreeFlappy Bird Game in Python

Flappy Bird started as a mobile game where you tap to make a bird fly through gaps in pipes. The bird falls with gravity, and each tap gives it an upward push. Hit a pipe or the ground, and the game ends. I remember wasting way too much time on this game back in 2013, and building a clone of it in Python was one of the first game projects I ever completed. There is something satisfying about getting the physics just right so the bird feels responsive but still challenging.
In this tutorial, I walk through building a complete Flappy Bird clone using Python and Pygame. I cover everything from setting up the game window and game loop, to handling bird physics with gravity and flap velocity, to detecting collisions and tracking scores. By the end, you will have a fully playable game with a welcome screen, gameplay, and game-over state.
TLDR
- Install Pygame with
pip install pygameand initialize it withpygame.init() - The bird falls with gravity and jumps when you press Space or Up arrow
- Pipes spawn at random heights, and you detect collisions with the bird rect
- Track score by checking when the bird passes the center of each pipe
- The game has three states: welcome screen, playing, and game over
Setting Up Pygame
Before writing any game code, you need Pygame installed. Open your terminal and run:
pip install pygame
Once installed, import the modules you need. I use random for generating pipe positions, sys for exiting the program cleanly, and pygame along with pygame.locals for the game itself.
import random
import sys
import pygame
from pygame.locals import *
Game Constants and Window Setup
Define the screen dimensions and frame rate first. Flappy Bird uses a small window by default, and I keep the frame rate at 32 FPS to match the original game feel. The bird image is roughly 35 pixels wide and 25 pixels tall, which influences how I calculate collision zones later.
fps = 32
screen_width = 289
screen_height = 511
screen = pygame.display.set_mode((screen_width, screen_height))
ground_y = screen_height * 0.8
Store your image and sound file paths in dictionaries. Using convert_alpha() when loading images preserves transparency, which matters for the bird sprite.
game_images = {}
game_sounds = {}
player = 'gallery/images/bird.png'
background = 'gallery/images/background.png'
pipe = 'gallery/images/pipe.png'
title = 'gallery/images/title.png'
The Game Loop and __main__
Every Pygame game starts with pygame.init() to initialize all modules. I create a clock object with pygame.time.Clock() and pass it the FPS each frame to lock the frame rate. The main loop runs forever, alternating between the welcome screen and the main game.
if __name__ == "__main__":
pygame.init()
fps_clock = pygame.time.Clock()
pygame.display.set_caption('Flappy Bird')
load_images()
load_sounds()
while True:
welcomeScreen()
mainGame()
Loading Images and Sounds
I load the number sprites for the score display, the welcome message image, the ground base, and the pipe images. For pipes, I load the same image twice: once for the lower pipe and once rotated 180 degrees for the upper pipe.
def load_images():
game_images['numbers'] = (
pygame.image.load('gallery/images/0.png').convert_alpha(),
pygame.image.load('gallery/images/1.png').convert_alpha(),
pygame.image.load('gallery/images/2.png').convert_alpha(),
pygame.image.load('gallery/images/3.png').convert_alpha(),
pygame.image.load('gallery/images/4.png').convert_alpha(),
pygame.image.load('gallery/images/5.png').convert_alpha(),
pygame.image.load('gallery/images/6.png').convert_alpha(),
pygame.image.load('gallery/images/7.png').convert_alpha(),
pygame.image.load('gallery/images/8.png').convert_alpha(),
pygame.image.load('gallery/images/9.png').convert_alpha()
)
game_images['message'] = pygame.image.load('gallery/images/message.png').convert_alpha()
game_images['base'] = pygame.image.load('gallery/images/base.png').convert_alpha()
game_images['pipe'] = (
pygame.transform.rotate(pygame.image.load(pipe).convert_alpha(), 180),
pygame.image.load(pipe).convert_alpha()
)
game_images['background'] = pygame.image.load(background).convert_alpha()
game_images['player'] = pygame.image.load(player).convert_alpha()
game_images['title'] = pygame.image.load(title).convert_alpha()
For sounds, I use pygame.mixer.Sound() with paths to WAV files. I load five sounds: wing (flap), hit (collision), die (game over), point (score), and swoosh (screen transition).
def load_sounds():
game_sounds['wing'] = pygame.mixer.Sound('gallery/audio/wing.wav')
game_sounds['hit'] = pygame.mixer.Sound('gallery/audio/hit.wav')
game_sounds['die'] = pygame.mixer.Sound('gallery/audio/die.wav')
game_sounds['point'] = pygame.mixer.Sound('gallery/audio/point.wav')
game_sounds['swoosh'] = pygame.mixer.Sound('gallery/audio/swoosh.wav')
Welcome Screen
The welcome screen shows the bird floating with a title and a message prompting the player to press Space or Up. It runs in a loop until the player presses a key, then returns control to the main game.
def welcomeScreen():
player_x = int(screen_width / 8)
player_y = int((screen_height - game_images['player'].get_height()) / 2)
message_x = int((screen_width - game_images['message'].get_width()) / 2)
message_y = int(screen_height * 0.2)
title_x = int((screen_width - game_images['message'].get_width()) / 2)
title_y = int(screen_height * 0.04)
base_x = 0
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
elif event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
return
screen.blit(game_images['background'], (0, 0))
screen.blit(game_images['message'], (message_x, message_y))
screen.blit(game_images['player'], (player_x, player_y))
screen.blit(game_images['base'], (base_x, ground_y))
screen.blit(game_images['title'], (title_x, title_y))
pygame.display.update()
fps_clock.tick(fps)
Bird Physics
The bird in Flappy Bird has simple but specific physics. Gravity pulls it down at a constant rate. When the player presses Space or Up, the bird gets an upward velocity impulse. I set gravity to 1 pixel per frame squared, flap velocity to -8 (upward), and clamp the fall speed between -8 and 10.
playerVelY = -9
playerMaxVelY = 10
playerMinVelY = -8
playerAccY = 1
playerFlapVel = -8
playerFlapped = False
Each frame, if the bird is not at the top of the screen and the player presses a key, I set playerVelY = playerFlapVel and mark playerFlapped = True. On subsequent frames, I apply gravity until the velocity reaches playerMaxVelY.
Pipe Generation
Pipes are generated in pairs: one at the top (inverted) and one at the bottom, separated by a gap. The gap position varies randomly each time a new pipe pair is created. I use getRandomPipe() to generate these pairs.
def getRandomPipe():
pipeHeight = game_images['pipe'][0].get_height()
offset = screen_height / 3
y2 = offset + random.randrange(0, int(screen_height - game_images['base'].get_height() - 1.2 * offset))
pipeX = screen_width + 10
y1 = pipeHeight - y2 + offset
pipe = [
{'x': pipeX, 'y': -y1},
{'x': pipeX, 'y': y2}
]
return pipe
The pipe moves left at a constant velocity of -4 pixels per frame. When a pipe moves off the left edge of the screen, I remove it from the list. When the first pipe in the list gets close to the left edge, I append a new pipe pair.
Collision Detection
Collision detection checks three things: whether the bird hit the ground, whether it hit the top of the screen, and whether it overlaps with any pipe. I check the bird against the top and bottom edges first, then against the pipe rectangles.
<pre class="wp-block-syntaxhighlighter-code">
def isCollide(player_x, player_y, upperPipes, lowerPipes):
if player_y > ground_y - 25 or player_y < 0:
game_sounds['hit'].play()
return True
for pipe in upperPipes:
pipeHeight = game_images['pipe'][0].get_height()
if (player_y < pipeHeight + pipe['y']) and (abs(player_x - pipe['x']) < game_images['pipe'][0].get_width() - 15):
game_sounds['hit'].play()
return True
for pipe in lowerPipes:
if (player_y + game_images['player'].get_height() > pipe['y']) and (abs(player_x - pipe['x']) < game_images['pipe'][0].get_width() - 15):
game_sounds['hit'].play()
return True
return False
</pre>
I use a 15-pixel horizontal margin on pipe collision detection. Without this margin, the game feels unfair because pixels slightly outside the visible pipe can trigger a collision. Adjusting this value is one of the first things I tweak when playtesting.
Scoring
Score increments when the bird passes the horizontal center of a pipe. I calculate the bird’s mid position and compare it against each pipe’s center. Once the bird crosses a pipe center, I add 1 to the score and play the point sound.
<pre class="wp-block-syntaxhighlighter-code">
playerMidPos = player_x + game_images['player'].get_width() / 2
for pipe in upperPipes:
pipeMidPos = pipe['x'] + game_images['pipe'][0].get_width() / 2
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
score += 1
print(f"Your Score is {score}")
game_sounds['point'].play()
</pre>
The score digits are drawn using image sprites. I calculate the total width of all digits, center them horizontally on the screen, and blit each digit image at the correct offset.
myDigits = [int(x) for x in list(str(score))]
width = 0
for digit in myDigits:
width += game_images['numbers'][digit].get_width()
Xoffset = (screen_width - width) / 2
for digit in myDigits:
screen.blit(game_images['numbers'][digit], (Xoffset, screen_height * 0.12))
Xoffset += game_images['numbers'][digit].get_width()
The Main Game Function
The mainGame() function ties everything together. It initializes the score and player position, creates the first two pipes, and enters the main game loop where it handles input, physics, collision, scoring, and rendering.
<pre class="wp-block-syntaxhighlighter-code">
def mainGame():
score = 0
player_x = int(screen_width / 8)
player_y = int(screen_height / 2)
base_x = 0
newPipe1 = getRandomPipe()
newPipe2 = getRandomPipe()
upperPipes = [
{'x': screen_width + 200, 'y': newPipe1[0]['y']},
{'x': screen_width + 200 + (screen_width / 2), 'y': newPipe2[0]['y']}
]
lowerPipes = [
{'x': screen_width + 200, 'y': newPipe1[1]['y']},
{'x': screen_width + 200 + (screen_width / 2), 'y': newPipe2[1]['y']}
]
pipeVelX = -4
playerVelY = -9
playerMaxVelY = 10
playerMinVelY = -8
playerAccY = 1
playerFlapVel = -8
playerFlapped = False
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
if player_y > 0:
playerVelY = playerFlapVel
playerFlapped = True
game_sounds['wing'].play()
crashTest = isCollide(player_x, player_y, upperPipes, lowerPipes)
if crashTest:
return
playerMidPos = player_x + game_images['player'].get_width() / 2
for pipe in upperPipes:
pipeMidPos = pipe['x'] + game_images['pipe'][0].get_width() / 2
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
score += 1
print(f"Your Score is {score}")
game_sounds['point'].play()
if playerVelY < playerMaxVelY and not playerFlapped:
playerVelY += playerAccY
if playerFlapped:
playerFlapped = False
playerHeight = game_images['player'].get_height()
player_y = player_y + min(playerVelY, ground_y - player_y - playerHeight)
for upperPipe, lowerPipe in zip(upperPipes, lowerPipes):
upperPipe['x'] += pipeVelX
lowerPipe['x'] += pipeVelX
if 0 < upperPipes[0]['x'] < 5:
newPipe = getRandomPipe()
upperPipes.append(newPipe[0])
lowerPipes.append(newPipe[1])
if upperPipes[0]['x'] < -game_images['pipe'][0].get_width():
upperPipes.pop(0)
lowerPipes.pop(0)
screen.blit(game_images['background'], (0, 0))
for upperPipe, lowerPipe in zip(upperPipes, lowerPipes):
screen.blit(game_images['pipe'][0], (upperPipe['x'], upperPipe['y']))
screen.blit(game_images['pipe'][1], (lowerPipe['x'], lowerPipe['y']))
screen.blit(game_images['base'], (base_x, ground_y))
screen.blit(game_images['player'], (player_x, player_y))
myDigits = [int(x) for x in list(str(score))]
width = 0
for digit in myDigits:
width += game_images['numbers'][digit].get_width()
Xoffset = (screen_width - width) / 2
for digit in myDigits:
screen.blit(game_images['numbers'][digit], (Xoffset, screen_height * 0.12))
Xoffset += game_images['numbers'][digit].get_width()
pygame.display.update()
fps_clock.tick(fps)
</pre>
Complete Working Code
Here is the full game in one script. Save this as flappy_bird.py and run it with python flappy_bird.py. Make sure the image and audio files exist in the gallery/images/ and gallery/audio/ directories respectively.
<pre class="wp-block-syntaxhighlighter-code">
import random
import sys
import pygame
from pygame.locals import *
fps = 32
screen_width = 289
screen_height = 511
screen = pygame.display.set_mode((screen_width, screen_height))
ground_y = screen_height * 0.8
game_images = {}
game_sounds = {}
player = 'gallery/images/bird.png'
background = 'gallery/images/background.png'
pipe = 'gallery/images/pipe.png'
title = 'gallery/images/title.png'
def load_images():
game_images['numbers'] = (
pygame.image.load('gallery/images/0.png').convert_alpha(),
pygame.image.load('gallery/images/1.png').convert_alpha(),
pygame.image.load('gallery/images/2.png').convert_alpha(),
pygame.image.load('gallery/images/3.png').convert_alpha(),
pygame.image.load('gallery/images/4.png').convert_alpha(),
pygame.image.load('gallery/images/5.png').convert_alpha(),
pygame.image.load('gallery/images/6.png').convert_alpha(),
pygame.image.load('gallery/images/7.png').convert_alpha(),
pygame.image.load('gallery/images/8.png').convert_alpha(),
pygame.image.load('gallery/images/9.png').convert_alpha()
)
game_images['message'] = pygame.image.load('gallery/images/message.png').convert_alpha()
game_images['base'] = pygame.image.load('gallery/images/base.png').convert_alpha()
game_images['pipe'] = (
pygame.transform.rotate(pygame.image.load(pipe).convert_alpha(), 180),
pygame.image.load(pipe).convert_alpha()
)
game_images['background'] = pygame.image.load(background).convert_alpha()
game_images['player'] = pygame.image.load(player).convert_alpha()
game_images['title'] = pygame.image.load(title).convert_alpha()
def load_sounds():
game_sounds['wing'] = pygame.mixer.Sound('gallery/audio/wing.wav')
game_sounds['hit'] = pygame.mixer.Sound('gallery/audio/hit.wav')
game_sounds['die'] = pygame.mixer.Sound('gallery/audio/die.wav')
game_sounds['point'] = pygame.mixer.Sound('gallery/audio/point.wav')
game_sounds['swoosh'] = pygame.mixer.Sound('gallery/audio/swoosh.wav')
def welcomeScreen():
player_x = int(screen_width / 8)
player_y = int((screen_height - game_images['player'].get_height()) / 2)
message_x = int((screen_width - game_images['message'].get_width()) / 2)
message_y = int(screen_height * 0.2)
title_x = int((screen_width - game_images['message'].get_width()) / 2)
title_y = int(screen_height * 0.04)
base_x = 0
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
elif event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
return
screen.blit(game_images['background'], (0, 0))
screen.blit(game_images['message'], (message_x, message_y))
screen.blit(game_images['player'], (player_x, player_y))
screen.blit(game_images['base'], (base_x, ground_y))
screen.blit(game_images['title'], (title_x, title_y))
pygame.display.update()
fps_clock.tick(fps)
def getRandomPipe():
pipeHeight = game_images['pipe'][0].get_height()
offset = screen_height / 3
y2 = offset + random.randrange(0, int(screen_height - game_images['base'].get_height() - 1.2 * offset))
pipeX = screen_width + 10
y1 = pipeHeight - y2 + offset
pipe = [
{'x': pipeX, 'y': -y1},
{'x': pipeX, 'y': y2}
]
return pipe
def isCollide(player_x, player_y, upperPipes, lowerPipes):
if player_y > ground_y - 25 or player_y < 0:
game_sounds['hit'].play()
return True
for pipe in upperPipes:
pipeHeight = game_images['pipe'][0].get_height()
if (player_y < pipeHeight + pipe['y']) and (abs(player_x - pipe['x']) < game_images['pipe'][0].get_width() - 15):
game_sounds['hit'].play()
return True
for pipe in lowerPipes:
if (player_y + game_images['player'].get_height() > pipe['y']) and (abs(player_x - pipe['x']) < game_images['pipe'][0].get_width() - 15):
game_sounds['hit'].play()
return True
return False
def mainGame():
score = 0
player_x = int(screen_width / 8)
player_y = int(screen_height / 2)
base_x = 0
newPipe1 = getRandomPipe()
newPipe2 = getRandomPipe()
upperPipes = [
{'x': screen_width + 200, 'y': newPipe1[0]['y']},
{'x': screen_width + 200 + (screen_width / 2), 'y': newPipe2[0]['y']}
]
lowerPipes = [
{'x': screen_width + 200, 'y': newPipe1[1]['y']},
{'x': screen_width + 200 + (screen_width / 2), 'y': newPipe2[1]['y']}
]
pipeVelX = -4
playerVelY = -9
playerMaxVelY = 10
playerAccY = 1
playerFlapVel = -8
playerFlapped = False
while True:
for event in pygame.event.get():
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit()
sys.exit()
if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
if player_y > 0:
playerVelY = playerFlapVel
playerFlapped = True
game_sounds['wing'].play()
crashTest = isCollide(player_x, player_y, upperPipes, lowerPipes)
if crashTest:
return
playerMidPos = player_x + game_images['player'].get_width() / 2
for pipe in upperPipes:
pipeMidPos = pipe['x'] + game_images['pipe'][0].get_width() / 2
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
score += 1
print(f"Your Score is {score}")
game_sounds['point'].play()
if playerVelY < playerMaxVelY and not playerFlapped:
playerVelY += playerAccY
if playerFlapped:
playerFlapped = False
playerHeight = game_images['player'].get_height()
player_y = player_y + min(playerVelY, ground_y - player_y - playerHeight)
for upperPipe, lowerPipe in zip(upperPipes, lowerPipes):
upperPipe['x'] += pipeVelX
lowerPipe['x'] += pipeVelX
if 0 < upperPipes[0]['x'] < 5:
newPipe = getRandomPipe()
upperPipes.append(newPipe[0])
lowerPipes.append(newPipe[1])
if upperPipes[0]['x'] < -game_images['pipe'][0].get_width():
upperPipes.pop(0)
lowerPipes.pop(0)
screen.blit(game_images['background'], (0, 0))
for upperPipe, lowerPipe in zip(upperPipes, lowerPipes):
screen.blit(game_images['pipe'][0], (upperPipe['x'], upperPipe['y']))
screen.blit(game_images['pipe'][1], (lowerPipe['x'], lowerPipe['y']))
screen.blit(game_images['base'], (base_x, ground_y))
screen.blit(game_images['player'], (player_x, player_y))
myDigits = [int(x) for x in list(str(score))]
width = 0
for digit in myDigits:
width += game_images['numbers'][digit].get_width()
Xoffset = (screen_width - width) / 2
for digit in myDigits:
screen.blit(game_images['numbers'][digit], (Xoffset, screen_height * 0.12))
Xoffset += game_images['numbers'][digit].get_width()
pygame.display.update()
fps_clock.tick(fps)
if __name__ == "__main__":
pygame.init()
fps_clock = pygame.time.Clock()
pygame.display.set_caption('Flappy Bird')
load_images()
load_sounds()
while True:
welcomeScreen()
mainGame()
</pre>
What You Can Improve
The basic game works, but I have a few things I always come back to when improving it. Adding a high score that persists between sessions using a JSON file or pygame.local is one of the first things I implement. Another is background music that loops throughout gameplay instead of just sound effects on events.
You can also add difficulty progression, where the pipe gap shrinks or the pipe speed increases as the score goes up. I have also tried adding a replay feature that shows your last game trajectory before the game-over screen, which players seem to enjoy.
FAQ
How do I install Pygame on Windows?
Open Command Prompt and run pip install pygame. If you have multiple Python versions, use pip3 install pygame or python -m pip install pygame.
Why does the bird not respond to key presses sometimes?
Pygame checks keyboard state per frame. If you press a key between frames, it might get missed. Using pygame.event.wait() instead of pygame.event.get() in the main loop can help, but for Flappy Bird the current approach usually works fine if your FPS is stable.
How do I add images and sounds to the game?
Create a gallery/images/ folder and put your bird, pipe, background, base, title, and message images there. Create a gallery/audio/ folder for WAV files named wing.wav, hit.wav, die.wav, point.wav, and swoosh.wav. Update the file paths in the script to match your folder structure.
Can I use a different image size for the bird?
Yes, but you need to adjust the collision detection margins if your sprite is significantly larger or smaller. The 15-pixel horizontal margin I use in isCollide() assumes a bird around 35 by 25 pixels. Larger sprites may need a bigger margin to avoid unfair collisions.
How do I build an executable from this game?
Install pyinstaller with pip install pyinstaller and run pyinstaller --onefile flappy_bird.py. Make sure all image and audio paths are relative to the script, not absolute paths, so the executable finds them when packaged.
Why does the score sometimes skip a number?
Your frame rate might be too low or inconsistent. If a frame takes longer than expected, the bird can jump further horizontally between frames, potentially skipping past the pipe center detection zone. Keeping FPS stable at 32 or higher usually fixes this.


