Trong bài này chúng ta sẽ cùng lập trình game Flappy Bird bằng Python và thư viện Pygame: tạo cửa sổ game, nạp hình ảnh, sinh ống ngẫu nhiên, xử lý va chạm, tính điểm, và cuối cùng là code hoàn chỉnh chạy được.
Flappy Bird là tựa game của anh Nguyễn Hà Đông từng gây sốt khắp thế giới năm 2013. Luật chơi cực đơn giản nhưng để đạt điểm cao thì không hề dễ. Tự tay code lại game này là cách rất hay để hiểu vòng lặp game, xử lý sự kiện và va chạm hoạt động ra sao nhé.
Game Flappy Bird là gì?
Flappy Bird là game trên điện thoại ra mắt năm 2013, do lập trình viên người Việt Nguyễn Hà Đông phát triển. Người chơi điều khiển một chú chim bay qua các khe hở giữa những đường ống mà không được chạm vào.
Cách chơi chỉ có một thao tác: chạm vào màn hình để chim vỗ cánh bay lên, không chạm thì chim rơi xuống. Chim chạm đất hoặc đâm vào ống là thua. Mỗi lần bay qua một cặp ống được cộng 1 điểm.
Game nổi tiếng rất nhanh nhờ lối chơi đơn giản mà gây nghiện, nhưng cũng bị nhiều người chê vì quá khó. Đến tháng 2 năm 2014, anh Đông đã tự gỡ game khỏi các cửa hàng ứng dụng. Dù vậy Flappy Bird vẫn là nguồn cảm hứng cho rất nhiều game và bài tập lập trình sau này.

Hướng dẫn lập trình game Flappy Bird với Python
Chúng ta sẽ dùng Pygame, thư viện mã nguồn mở chuyên để làm game 2D bằng Python. Pygame lo phần vẽ hình ảnh, bắt sự kiện bàn phím và điều khiển tốc độ khung hình, việc của chúng ta là viết logic game.
Nếu chưa quen với Pygame, các bạn nên làm bài game rắn săn mồi trước, bài đó đi từng bước nhỏ và dễ hiểu hơn.
Tạo dự án Python
Đầu tiên tạo một dự án Python mới. Ví dụ với PyCharm:
1. Tạo dự án: chọn tạo dự án mới, nhập tên dự án rồi bấm “Create”.
2. Tạo file Python: chuột phải vào tên dự án, chọn “New” rồi chọn “Python File”.
3. Đặt tên file: nhập tên file, ví dụ flappy.py, rồi nhấn Enter.
Nếu dùng VS Code thì chỉ cần tạo một thư mục, mở thư mục đó trong VS Code và tạo file flappy.py là xong.
Trong thư mục dự án, tạo thêm thư mục images chứa các ảnh của game: background.jpg (hình nền), bird.png (con chim), pipe.png (đường ống), base.jfif (mặt đất) và 10 ảnh chữ số 0.png đến 9.png để hiển thị điểm. Các bạn tìm bộ ảnh “flappy bird sprites” trên mạng là có đủ.
Cách tạo game Flappy Bird thông qua thư viện Pygame
Dự án đã sẵn sàng, giờ chúng ta viết code theo từng phần.

- Cài đặt thư viện Pygame:
pip install pygame
- Khai báo kích thước cửa sổ game (chiều rộng, chiều cao) và đường dẫn tới các file ảnh: đường ống, con chim, hình nền, mặt đất.
# Import module
import random
import sys
import pygame
from pygame.locals import *
# All the Game Variables
window_width = 600
window_height = 499
# set height and width of window
window = pygame.display.set_mode((window_width, window_height))
elevation = window_height * 0.8
game_images = {}
framepersecond = 32
pipeimage = 'images/pipe.png'
background_image = 'images/background.jpg'
birdplayer_image = 'images/bird.png'
sealevel_image = 'images/base.jfif'
elevation là độ cao của mặt đất, bằng 80% chiều cao cửa sổ. framepersecond là số khung hình mỗi giây của game.
Lưu ý: Đường dẫn ảnh ở đây là đường dẫn tương đối, tính từ thư mục mà bạn đứng khi chạy lệnh python chứ không phải thư mục chứa file .py. Nếu báo lỗi FileNotFoundError: No file ‘images/pipe.png’ found thì mở terminal ngay trong thư mục dự án rồi mới chạy python flappy.py. Đừng viết ‘/images/bird.png’ có dấu / ở đầu, đó là đường dẫn tính từ gốc ổ đĩa.
- Tiếp theo là phần khởi động game.
pygame.init()khởi tạo thư viện,pygame.display.set_caption()đặt tiêu đề cửa sổ,pygame.time.Clock()tạo đồng hồ để giữ tốc độ game ổn định. Sau đó dùngpygame.image.load()nạp toàn bộ hình ảnh vào dictionarygame_images.
Ống phía trên chính là ảnh ống phía dưới được xoay ngược 180 độ bằng pygame.transform.rotate(), nên chỉ cần một file pipe.png.
# program where the game starts
if __name__ == "__main__":
# For initializing modules of pygame library
pygame.init()
framepersecond_clock = pygame.time.Clock()
# Sets the title on top of game window
pygame.display.set_caption('Flappy Bird Game')
# Load all the images which we will use in the game
# images for displaying score
game_images['scoreimages'] = (
pygame.image.load('images/0.png').convert_alpha(),
pygame.image.load('images/1.png').convert_alpha(),
pygame.image.load('images/2.png').convert_alpha(),
pygame.image.load('images/3.png').convert_alpha(),
pygame.image.load('images/4.png').convert_alpha(),
pygame.image.load('images/5.png').convert_alpha(),
pygame.image.load('images/6.png').convert_alpha(),
pygame.image.load('images/7.png').convert_alpha(),
pygame.image.load('images/8.png').convert_alpha(),
pygame.image.load('images/9.png').convert_alpha()
)
game_images['flappybird'] = pygame.image.load(
birdplayer_image).convert_alpha()
game_images['sea_level'] = pygame.image.load(
sealevel_image).convert_alpha()
game_images['background'] = pygame.image.load(
background_image).convert_alpha()
game_images['pipeimage'] = (pygame.transform.rotate(pygame.image.load(
pipeimage).convert_alpha(), 180), pygame.image.load(
pipeimage).convert_alpha())
print("WELCOME TO THE FLAPPY BIRD GAME")
print("Press space or up key to start the game")
- Màn hình chờ: đặt con chim ở giữa màn hình, vẽ hình nền, chim và mặt đất rồi chờ người chơi nhấn phím. Nhấn Space hoặc phím mũi tên lên thì gọi hàm
flappygame()để vào game, nhấn Esc hoặc bấm nút X thì thoát bằngsys.exit().
Game phải chạy liên tục cho tới khi người chơi thoát, nên chúng ta dùng vòng lặp while True vô hạn. Có 2 vòng lồng nhau: vòng ngoài để mỗi lần thua xong thì đặt lại vị trí chim, vòng trong là màn hình chờ. Đoạn code này nằm tiếp ngay bên dưới phần 3, vẫn thụt vào trong khối if __name__ == "__main__":.
# Here starts the main game
while True:
# sets the coordinates of flappy bird
horizontal = int(window_width/5)
vertical = int(
(window_height - game_images['flappybird'].get_height())/2)
# for sea level
ground = 0
while True:
for event in pygame.event.get():
# if user clicks on cross button, close the game
if event.type == QUIT or (event.type == KEYDOWN and
event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# If the user presses space or
# up key, start the game for them
elif event.type == KEYDOWN and (event.key == K_SPACE or
event.key == K_UP):
flappygame()
# if user doesn't press any key, just draw the welcome screen
window.blit(game_images['background'], (0, 0))
window.blit(game_images['flappybird'],
(horizontal, vertical))
window.blit(game_images['sea_level'], (ground, elevation))
# Just Refresh the screen
pygame.display.update()
# set the rate of frame per second
framepersecond_clock.tick(framepersecond)
- Hàm
createPipe()tạo một cặp ống mới với khe hở ở độ cao ngẫu nhiên. Chiều cao ảnh ống lấy bằngget_height(). Vị trí ống dướiy2được chọn ngẫu nhiên bằngrandom.randrange()trong phạm vi phù hợp với chiều cao cửa sổ, từ đó tính ra vị trí ống trêny1sao cho giữa 2 ống luôn chừa một khe đủ rộng cho chim bay qua. Hàm trả về một list gồm 2 dictionary chứa tọa độ ống trên và ống dưới.
def createPipe():
offset = window_height/3
pipeHeight = game_images['pipeimage'][0].get_height()
# generating random height of pipes
y2 = offset + \
random.randrange(
0, int(window_height - game_images['sea_level'].get_height() - 1.2 * offset))
pipeX = window_width + 10
y1 = pipeHeight - y2 + offset
pipe = [
# upper Pipe
{'x': pipeX, 'y': -y1},
# lower Pipe
{'x': pipeX, 'y': y2}
]
return pipe
- Hàm
isGameOver()kiểm tra chim đã thua chưa: rơi xuống mặt đất, bay vượt lên khỏi mép trên màn hình, hoặc đâm vào ống trên hay ống dưới.
def isGameOver(horizontal, vertical, up_pipes, down_pipes):
# Checking if bird is above the sealevel or flies out of the top
if vertical > elevation - 25 or vertical < 0:
return True
# Checking if bird hits the upper pipe or not
for pipe in up_pipes:
pipeHeight = game_images['pipeimage'][0].get_height()
if (vertical < pipeHeight + pipe['y'] and
abs(horizontal - pipe['x']) < game_images['pipeimage'][0].get_width()):
return True
# Checking if bird hits the lower pipe or not
for pipe in down_pipes:
if (vertical + game_images['flappybird'].get_height() > pipe['y'] and
abs(horizontal - pipe['x']) < game_images['pipeimage'][0].get_width()):
return True
return False
Lưu ý: Một điều kiện if dài muốn xuống dòng thì phải đặt cả biểu thức trong cặp ngoặc tròn như trên, hoặc dùng dấu \ ở cuối dòng. Viết if (a > b) xong xuống dòng rồi mới viết and … ở dòng dưới là Python báo SyntaxError ngay, vì nó hiểu dòng if đã kết thúc ở dấu ngoặc đóng.
- Hàm chính
flappygame()chứa vòng lặp của màn chơi. Đầu tiên tạo 2 cặp ống bằngcreatePipe()và lưu vào 2 list:up_pipeschứa các ống trên,down_pipeschứa các ống dưới. Tiếp theo khai báo vận tốc: ống chạy sang trái 4 pixel mỗi khung hình, chim chịu gia tốc rơi xuống và có vận tốc tối đa.
Trong mỗi vòng lặp:
- Bắt sự kiện bằng
pygame.event.get(), nhấn Space hoặc phím lên thì chim vỗ cánh bay lên. - Gọi
isGameOver(), thua thìreturnđể quay về màn hình chờ. - Kiểm tra chim đã bay qua ống chưa để cộng điểm.
- Cập nhật vị trí chim, dịch các ống sang trái, thêm ống mới và xóa ống đã ra khỏi màn hình.
- Vẽ lại hình nền, ống, mặt đất, chim và điểm số lên màn hình.
def flappygame():
your_score = 0
horizontal = int(window_width/5)
vertical = int(window_width/2)
ground = 0
mytempheight = 100
# Generating two pipes for blitting on window
first_pipe = createPipe()
second_pipe = createPipe()
# List containing lower pipes
down_pipes = [
{'x': window_width+300-mytempheight,
'y': first_pipe[1]['y']},
{'x': window_width+300-mytempheight+(window_width/2),
'y': second_pipe[1]['y']},
]
# List Containing upper pipes
up_pipes = [
{'x': window_width+300-mytempheight,
'y': first_pipe[0]['y']},
{'x': window_width+200-mytempheight+(window_width/2),
'y': second_pipe[0]['y']},
]
# pipe velocity along x
pipeVelX = -4
# bird velocity
bird_velocity_y = -9
bird_Max_Vel_Y = 10
bird_Min_Vel_Y = -8
birdAccY = 1
# velocity while flapping
bird_flap_velocity = -8
# It is true only when the bird is flapping
bird_flapped = False
while True:
# Handling the key pressing events
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 vertical > 0:
bird_velocity_y = bird_flap_velocity
bird_flapped = True
# This function will return true
# if the flappybird is crashed
game_over = isGameOver(horizontal,
vertical,
up_pipes,
down_pipes)
if game_over:
return
# check for your_score
playerMidPos = horizontal + game_images['flappybird'].get_width()/2
for pipe in up_pipes:
pipeMidPos = pipe['x'] + game_images['pipeimage'][0].get_width()/2
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
your_score += 1
print(f"Your score is {your_score}")
if bird_velocity_y < bird_Max_Vel_Y and not bird_flapped:
bird_velocity_y += birdAccY
if bird_flapped:
bird_flapped = False
playerHeight = game_images['flappybird'].get_height()
vertical = vertical + \
min(bird_velocity_y, elevation - vertical - playerHeight)
# move pipes to the left
for upperPipe, lowerPipe in zip(up_pipes, down_pipes):
upperPipe['x'] += pipeVelX
lowerPipe['x'] += pipeVelX
# Add a new pipe when the first is
# about to cross the leftmost part of the screen
if 0 < up_pipes[0]['x'] < 5:
newpipe = createPipe()
up_pipes.append(newpipe[0])
down_pipes.append(newpipe[1])
# if the pipe is out of the screen, remove it
if up_pipes[0]['x'] < -game_images['pipeimage'][0].get_width():
up_pipes.pop(0)
down_pipes.pop(0)
# Lets blit our game images now
window.blit(game_images['background'], (0, 0))
for upperPipe, lowerPipe in zip(up_pipes, down_pipes):
window.blit(game_images['pipeimage'][0],
(upperPipe['x'], upperPipe['y']))
window.blit(game_images['pipeimage'][1],
(lowerPipe['x'], lowerPipe['y']))
window.blit(game_images['sea_level'], (ground, elevation))
window.blit(game_images['flappybird'], (horizontal, vertical))
# Fetching the digits of score.
numbers = [int(x) for x in list(str(your_score))]
width = 0
# finding the width of score images from numbers.
for num in numbers:
width += game_images['scoreimages'][num].get_width()
Xoffset = (window_width - width)/1.1
# Blitting the images on the window.
for num in numbers:
window.blit(game_images['scoreimages'][num],
(Xoffset, window_width*0.02))
Xoffset += game_images['scoreimages'][num].get_width()
# Refreshing the game window and displaying the score.
pygame.display.update()
framepersecond_clock.tick(framepersecond)
Dưới đây là toàn bộ code hoàn chỉnh của game Flappy Bird, các bạn copy vào file flappy.py, đặt thư mục images bên cạnh rồi chạy là chơi được.
# Import module
import random
import sys
import pygame
from pygame.locals import *
# All the Game Variables
window_width = 600
window_height = 499
# set height and width of window
window = pygame.display.set_mode((window_width, window_height))
elevation = window_height * 0.8
game_images = {}
framepersecond = 32
pipeimage = 'images/pipe.png'
background_image = 'images/background.jpg'
birdplayer_image = 'images/bird.png'
sealevel_image = 'images/base.jfif'
def flappygame():
your_score = 0
horizontal = int(window_width/5)
vertical = int(window_width/2)
ground = 0
mytempheight = 100
# Generating two pipes for blitting on window
first_pipe = createPipe()
second_pipe = createPipe()
# List containing lower pipes
down_pipes = [
{'x': window_width+300-mytempheight,
'y': first_pipe[1]['y']},
{'x': window_width+300-mytempheight+(window_width/2),
'y': second_pipe[1]['y']},
]
# List Containing upper pipes
up_pipes = [
{'x': window_width+300-mytempheight,
'y': first_pipe[0]['y']},
{'x': window_width+200-mytempheight+(window_width/2),
'y': second_pipe[0]['y']},
]
# pipe velocity along x
pipeVelX = -4
# bird velocity
bird_velocity_y = -9
bird_Max_Vel_Y = 10
bird_Min_Vel_Y = -8
birdAccY = 1
# velocity while flapping
bird_flap_velocity = -8
# It is true only when the bird is flapping
bird_flapped = False
while True:
# Handling the key pressing events
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 vertical > 0:
bird_velocity_y = bird_flap_velocity
bird_flapped = True
# This function will return true
# if the flappybird is crashed
game_over = isGameOver(horizontal,
vertical,
up_pipes,
down_pipes)
if game_over:
return
# check for your_score
playerMidPos = horizontal + game_images['flappybird'].get_width()/2
for pipe in up_pipes:
pipeMidPos = pipe['x'] + game_images['pipeimage'][0].get_width()/2
if pipeMidPos <= playerMidPos < pipeMidPos + 4:
your_score += 1
print(f"Your score is {your_score}")
if bird_velocity_y < bird_Max_Vel_Y and not bird_flapped:
bird_velocity_y += birdAccY
if bird_flapped:
bird_flapped = False
playerHeight = game_images['flappybird'].get_height()
vertical = vertical + \
min(bird_velocity_y, elevation - vertical - playerHeight)
# move pipes to the left
for upperPipe, lowerPipe in zip(up_pipes, down_pipes):
upperPipe['x'] += pipeVelX
lowerPipe['x'] += pipeVelX
# Add a new pipe when the first is
# about to cross the leftmost part of the screen
if 0 < up_pipes[0]['x'] < 5:
newpipe = createPipe()
up_pipes.append(newpipe[0])
down_pipes.append(newpipe[1])
# if the pipe is out of the screen, remove it
if up_pipes[0]['x'] < -game_images['pipeimage'][0].get_width():
up_pipes.pop(0)
down_pipes.pop(0)
# Lets blit our game images now
window.blit(game_images['background'], (0, 0))
for upperPipe, lowerPipe in zip(up_pipes, down_pipes):
window.blit(game_images['pipeimage'][0],
(upperPipe['x'], upperPipe['y']))
window.blit(game_images['pipeimage'][1],
(lowerPipe['x'], lowerPipe['y']))
window.blit(game_images['sea_level'], (ground, elevation))
window.blit(game_images['flappybird'], (horizontal, vertical))
# Fetching the digits of score.
numbers = [int(x) for x in list(str(your_score))]
width = 0
# finding the width of score images from numbers.
for num in numbers:
width += game_images['scoreimages'][num].get_width()
Xoffset = (window_width - width)/1.1
# Blitting the images on the window.
for num in numbers:
window.blit(game_images['scoreimages'][num],
(Xoffset, window_width*0.02))
Xoffset += game_images['scoreimages'][num].get_width()
# Refreshing the game window and displaying the score.
pygame.display.update()
framepersecond_clock.tick(framepersecond)
def isGameOver(horizontal, vertical, up_pipes, down_pipes):
# Checking if bird is above the sealevel or flies out of the top
if vertical > elevation - 25 or vertical < 0:
return True
# Checking if bird hits the upper pipe or not
for pipe in up_pipes:
pipeHeight = game_images['pipeimage'][0].get_height()
if (vertical < pipeHeight + pipe['y'] and
abs(horizontal - pipe['x']) < game_images['pipeimage'][0].get_width()):
return True
# Checking if bird hits the lower pipe or not
for pipe in down_pipes:
if (vertical + game_images['flappybird'].get_height() > pipe['y'] and
abs(horizontal - pipe['x']) < game_images['pipeimage'][0].get_width()):
return True
return False
def createPipe():
offset = window_height/3
pipeHeight = game_images['pipeimage'][0].get_height()
# generating random height of pipes
y2 = offset + \
random.randrange(
0, int(window_height - game_images['sea_level'].get_height() - 1.2 * offset))
pipeX = window_width + 10
y1 = pipeHeight - y2 + offset
pipe = [
# upper Pipe
{'x': pipeX, 'y': -y1},
# lower Pipe
{'x': pipeX, 'y': y2}
]
return pipe
# program where the game starts
if __name__ == "__main__":
# For initializing modules of pygame library
pygame.init()
framepersecond_clock = pygame.time.Clock()
# Sets the title on top of game window
pygame.display.set_caption('Flappy Bird Game')
# Load all the images which we will use in the game
# images for displaying score
game_images['scoreimages'] = (
pygame.image.load('images/0.png').convert_alpha(),
pygame.image.load('images/1.png').convert_alpha(),
pygame.image.load('images/2.png').convert_alpha(),
pygame.image.load('images/3.png').convert_alpha(),
pygame.image.load('images/4.png').convert_alpha(),
pygame.image.load('images/5.png').convert_alpha(),
pygame.image.load('images/6.png').convert_alpha(),
pygame.image.load('images/7.png').convert_alpha(),
pygame.image.load('images/8.png').convert_alpha(),
pygame.image.load('images/9.png').convert_alpha()
)
game_images['flappybird'] = pygame.image.load(
birdplayer_image).convert_alpha()
game_images['sea_level'] = pygame.image.load(
sealevel_image).convert_alpha()
game_images['background'] = pygame.image.load(
background_image).convert_alpha()
game_images['pipeimage'] = (pygame.transform.rotate(pygame.image.load(
pipeimage).convert_alpha(), 180), pygame.image.load(
pipeimage).convert_alpha())
print("WELCOME TO THE FLAPPY BIRD GAME")
print("Press space or up key to start the game")
# Here starts the main game
while True:
# sets the coordinates of flappy bird
horizontal = int(window_width/5)
vertical = int(
(window_height - game_images['flappybird'].get_height())/2)
# for sea level
ground = 0
while True:
for event in pygame.event.get():
# if user clicks on cross button, close the game
if event.type == QUIT or (event.type == KEYDOWN and
event.key == K_ESCAPE):
pygame.quit()
sys.exit()
# If the user presses space or
# up key, start the game for them
elif event.type == KEYDOWN and (event.key == K_SPACE or
event.key == K_UP):
flappygame()
# if user doesn't press any key, just draw the welcome screen
window.blit(game_images['background'], (0, 0))
window.blit(game_images['flappybird'],
(horizontal, vertical))
window.blit(game_images['sea_level'], (ground, elevation))
# Just Refresh the screen
pygame.display.update()
# set the rate of frame per second
framepersecond_clock.tick(framepersecond)

Tổng kết
Game Flappy Bird tuy đơn giản nhưng có đủ các thành phần của một game thật: vòng lặp game, xử lý phím, mô phỏng trọng lực, sinh chướng ngại vật ngẫu nhiên, phát hiện va chạm và tính điểm. Các bạn thử chỉnh pipeVelX cho ống chạy nhanh hơn, hoặc bird_flap_velocity để chim bay cao hơn, xem game thay đổi thế nào nhé.
Nếu thấy bài viết này hay, hãy chia sẻ tới bạn bè đang học Python. Và nếu thắc mắc điều gì, hãy để lại bình luận bên dưới.

