Hướng Dẫn Lập Trình Game Flappy Bird Với Python

Lập trình game Flappy Bird

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.

Game Flappy Bird
Hình 1. Game Flappy Bird

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.

Sử dụng thư viện Pygame để làm game Flappy Bird
Hình 2. Sử dụng thư viện Pygame để làm game Flappy Bird
  1. Cài đặt thư viện Pygame:
pip install pygame
  1. 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.

  1. 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ùng pygame.image.load() nạp toàn bộ hình ảnh vào dictionary game_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")
  1. 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ằng sys.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)
  1. 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ằng get_height(). Vị trí ống dưới y2 được chọn ngẫu nhiên bằng random.randrange() trong phạm vi phù hợp với chiều cao cửa sổ, từ đó tính ra vị trí ống trên y1 sao 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
  1. 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.

  1. 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ằng createPipe() và lưu vào 2 list: up_pipes chứa các ống trên, down_pipes chứ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)
Giao diện game Flappy Bird với Python
Hình 3. Giao diện game Flappy Bird với Python

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.

5/5 - (2 bình chọn)

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *