Construir Flappy Bird con Pygame

PythonPythonBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este proyecto, dividiremos el código para crear un juego simple de Flappy Bird utilizando la librería Pygame en pasos manejables. Siguiendo estos pasos, aprenderás cómo construir el juego gradualmente. Cada paso incluirá una breve explicación, bloques de código y comentarios para ayudarte a entender e implementar el juego. ¡Comencemos!

👀 Vista previa

Vista previa del juego Flappy Bird

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo configurar los archivos del proyecto para el juego Flappy Bird
  • Cómo mostrar la animación de bienvenida del juego
  • Cómo implementar la lógica principal del juego de Flappy Bird
  • Cómo mostrar la pantalla de fin de juego cuando el jugador pierde
  • Cómo definir funciones auxiliares para el juego

🏆 Logros

Después de completar este proyecto, podrás:

  • Utilizar la librería Pygame para crear juegos
  • Comprender conceptos de desarrollo de juegos como los bucles de juego, las colisiones y la animación

Crear los archivos del proyecto

Primero, crearemos los archivos del proyecto para el juego Flappy Bird.

cd ~/project
touch flappy.py
sudo pip install pygame

En este paso, configuraremos la estructura básica del proyecto e importaremos las bibliotecas necesarias. También definiremos algunas constantes y cargaremos los activos iniciales del juego.

from itertools import cycle
import random
import sys
import pygame
from pygame.locals import *

FPS = 30
SCREENWIDTH = 288
SCREENHEIGHT = 512
PIPEGAPSIZE = 100  ## gap between upper and lower part of pipe
BASEY = SCREENHEIGHT * 0.79
## image and hitmask  dicts
IMAGES, HITMASKS = {}, {}

## list of all possible players (tuple of 3 positions of flap)
PLAYERS_LIST = (
    ## red bird
    (
        "data/sprites/redbird-upflap.png",
        "data/sprites/redbird-midflap.png",
        "data/sprites/redbird-downflap.png",
    ),
    ## blue bird
    (
        "data/sprites/bluebird-upflap.png",
        "data/sprites/bluebird-midflap.png",
        "data/sprites/bluebird-downflap.png",
    ),
    ## yellow bird
    (
        "data/sprites/yellowbird-upflap.png",
        "data/sprites/yellowbird-midflap.png",
        "data/sprites/yellowbird-downflap.png",
    ),
)

## list of backgrounds
BACKGROUNDS_LIST = (
    "data/sprites/background-day.png",
    "data/sprites/background-night.png",
)

## list of pipes
PIPES_LIST = (
    "data/sprites/pipe-green.png",
    "data/sprites/pipe-red.png",
)
  • Importamos las bibliotecas necesarias para el juego, incluyendo pygame para crear el juego, random para generar elementos aleatorios, sys para funciones relacionadas con el sistema y pygame.locals para constantes de teclado.
  • Inicializamos Pygame con pygame.init().
  • Constantes como FPS, SCREENWIDTH, SCREENHEIGHT, PIPEGAPSIZE y BASEY se definen para configurar las dimensiones y la velocidad del juego.
  • Creamos diccionarios vacíos (IMAGES y HITMASKS) para almacenar los activos del juego.
  • Listas de activos de jugadores, fondos y tuberías se definen utilizando rutas de archivos.
✨ Revisar Solución y Practicar

Mostrar la animación de bienvenida

En este paso, crearemos la animación de la pantalla de bienvenida del juego Flappy Bird.

def showWelcomeAnimation():
    """Muestra la animación de la pantalla de bienvenida del flappy bird"""
    ## índice del jugador para dibujar en la pantalla
    playerIndex = 0
    playerIndexGen = cycle([0, 1, 2, 1])
    ## iterador utilizado para cambiar el playerIndex después de cada 5ta iteración
    loopIter = 0

    playerx = int(SCREENWIDTH * 0.2)
    playery = int((SCREENHEIGHT - IMAGES["player"][0].get_height()) / 2)

    messagex = int((SCREENWIDTH - IMAGES["message"].get_width()) / 2)
    messagey = int(SCREENHEIGHT * 0.12)

    basex = 0
    ## cantidad por la que la base puede desplazarse como máximo hacia la izquierda
    baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width()

    ## movimiento vertical del jugador en la pantalla de bienvenida
    playerShmVals = {"val": 0, "dir": 1}

    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):
                ## reproducir el primer sonido de flap y devolver valores para mainGame
                return {
                    "playery": playery + playerShmVals["val"],
                    "basex": basex,
                    "playerIndexGen": playerIndexGen,
                }

        ## ajustar playery, playerIndex, basex
        if (loopIter + 1) % 5 == 0:
            playerIndex = next(playerIndexGen)
        loopIter = (loopIter + 1) % 30
        basex = -((-basex + 4) % baseShift)
        playerShm(playerShmVals)

        ## dibujar sprites
        SCREEN.blit(IMAGES["background"], (0, 0))
        SCREEN.blit(
            IMAGES["player"][playerIndex], (playerx, playery + playerShmVals["val"])
        )
        SCREEN.blit(IMAGES["message"], (messagex, messagey))
        SCREEN.blit(IMAGES["base"], (basex, BASEY))

        pygame.display.update()
        FPSCLOCK.tick(FPS)
  • Definimos la función showWelcomeAnimation responsable de mostrar la animación de la pantalla de bienvenida.
  • La función configura variables para la animación y maneja la entrada del usuario para iniciar el juego.
  • Utiliza un bucle para actualizar los fotogramas de la animación y comprobar la entrada del usuario para comenzar el juego.
  • La animación incluye el pájaro volando y un mensaje mostrado en la pantalla.
  • La función pygame.display.update() se utiliza para actualizar la pantalla, y FPSCLOCK.tick(FPS) controla la tasa de fotogramas.
✨ Revisar Solución y Practicar

Lógica principal del juego

En este paso, implementaremos la lógica principal del juego Flappy Bird.

def mainGame(movementInfo):
    score = playerIndex = loopIter = 0
    playerIndexGen = movementInfo["playerIndexGen"]
    playerx, playery = int(SCREENWIDTH * 0.2), movementInfo["playery"]

    basex = movementInfo["basex"]
    baseShift = IMAGES["base"].get_width() - IMAGES["background"].get_width()

    ## obtener 2 nuevas tuberías para agregar a la lista de upperPipes y lowerPipes
    newPipe1 = getRandomPipe()
    newPipe2 = getRandomPipe()

    ## lista de tuberías superiores
    upperPipes = [
        {"x": SCREENWIDTH + 200, "y": newPipe1[0]["y"]},
        {"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[0]["y"]},
    ]

    ## lista de tuberías inferiores
    lowerPipes = [
        {"x": SCREENWIDTH + 200, "y": newPipe1[1]["y"]},
        {"x": SCREENWIDTH + 200 + (SCREENWIDTH / 2), "y": newPipe2[1]["y"]},
    ]

    dt = FPSCLOCK.tick(FPS) / 1000
    pipeVelX = -128 * dt

    ## velocidad del jugador, velocidad máxima, aceleración hacia abajo, aceleración al flap
    playerVelY = -9  ## velocidad del jugador a lo largo de Y, por defecto igual a playerFlapped
    playerMaxVelY = 10  ## velocidad máxima a lo largo de Y, velocidad máxima de descenso
    playerMinVelY = -8  ## velocidad mínima a lo largo de Y, velocidad máxima de ascenso
    playerAccY = 1  ## aceleración hacia abajo del jugador
    playerRot = 45  ## rotación del jugador
    playerVelRot = 3  ## velocidad angular
    playerRotThr = 20  ## umbral de rotación
    playerFlapAcc = -9  ## velocidad del jugador al flap
    playerFlapped = False  ## True cuando el jugador hace flap

    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 playery > -2 * IMAGES["player"][0].get_height():
                    playerVelY = playerFlapAcc
                    playerFlapped = True

        ## comprobar si hay colisión aquí
        crashTest = checkCrash(
            {"x": playerx, "y": playery, "index": playerIndex}, upperPipes, lowerPipes
        )
        if crashTest[0]:
            return {
                "y": playery,
                "groundCrash": crashTest[1],
                "basex": basex,
                "upperPipes": upperPipes,
                "lowerPipes": lowerPipes,
                "score": score,
                "playerVelY": playerVelY,
                "playerRot": playerRot,
            }

        ## comprobar si hay puntaje
        playerMidPos = playerx + IMAGES["player"][0].get_width() / 2
        for pipe in upperPipes:
            pipeMidPos = pipe["x"] + IMAGES["pipe"][0].get_width() / 2
            if pipeMidPos <= playerMidPos < pipeMidPos + 4:
                score += 1

        ## cambio de playerIndex y basex
        if (loopIter + 1) % 3 == 0:
            playerIndex = next(playerIndexGen)
        loopIter = (loopIter + 1) % 30
        basex = -((-basex + 100) % baseShift)

        ## rotar al jugador
        if playerRot > -90:
            playerRot -= playerVelRot

        ## movimiento del jugador
        if playerVelY < playerMaxVelY and not playerFlapped:
            playerVelY += playerAccY
        if playerFlapped:
            playerFlapped = False

            ## más rotación para cubrir el umbral (calculado en la rotación visible)
            playerRot = 45

        playerHeight = IMAGES["player"][playerIndex].get_height()
        playery += min(playerVelY, BASEY - playery - playerHeight)

        ## mover las tuberías hacia la izquierda
        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            uPipe["x"] += pipeVelX
            lPipe["x"] += pipeVelX

        ## agregar una nueva tubería cuando la primera tubería está a punto de tocar el lado izquierdo de la pantalla
        if 3 > len(upperPipes) > 0 and 0 < upperPipes[0]["x"] < 5:
            newPipe = getRandomPipe()
            upperPipes.append(newPipe[0])
            lowerPipes.append(newPipe[1])

        ## eliminar la primera tubería si está fuera de la pantalla
        if len(upperPipes) > 0 and upperPipes[0]["x"] < -IMAGES["pipe"][0].get_width():
            upperPipes.pop(0)
            lowerPipes.pop(0)

        ## dibujar sprites
        SCREEN.blit(IMAGES["background"], (0, 0))

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"]))
            SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"]))

        SCREEN.blit(IMAGES["base"], (basex, BASEY))
        ## mostrar el puntaje para que el jugador ocupe el puntaje
        showScore(score)

        ## La rotación del jugador tiene un umbral
        visibleRot = playerRotThr
        if playerRot <= playerRotThr:
            visibleRot = playerRot

        playerSurface = pygame.transform.rotate(
            IMAGES["player"][playerIndex], visibleRot
        )
        SCREEN.blit(playerSurface, (playerx, playery))

        pygame.display.update()
        FPSCLOCK.tick(FPS)
  • Definimos la función mainGame, que contiene la lógica central del juego Flappy Bird.
  • La función maneja la entrada del usuario, actualiza el estado del juego, comprueba las colisiones y lleva un registro del puntaje.
  • El bucle del juego se ejecuta continuamente, actualizando la pantalla y la lógica del juego.
  • Los controles del jugador se manejan a través de eventos de teclado (espacio o flecha hacia arriba).
  • La función también comprueba las colisiones con las tuberías y el suelo, actualiza el puntaje y gestiona la animación del pájaro.
  • El bucle del juego continúa hasta que el jugador choca o sale del juego.
✨ Revisar Solución y Practicar

Mostrar la pantalla de fin de juego

En este paso, crearemos la pantalla de fin de juego que aparece cuando el jugador pierde.

def showGameOverScreen(crashInfo):
    """Hace que el jugador caiga y muestra la imagen de fin de juego"""
    score = crashInfo["score"]
    playerx = SCREENWIDTH * 0.2
    playery = crashInfo["y"]
    playerHeight = IMAGES["player"][0].get_height()
    playerVelY = crashInfo["playerVelY"]
    playerAccY = 2
    playerRot = crashInfo["playerRot"]
    playerVelRot = 7

    basex = crashInfo["basex"]

    upperPipes, lowerPipes = crashInfo["upperPipes"], crashInfo["lowerPipes"]

    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 playery + playerHeight >= BASEY - 1:
                    return

        ## desplazamiento de y del jugador
        if playery + playerHeight < BASEY - 1:
            playery += min(playerVelY, BASEY - playery - playerHeight)

        ## cambio de velocidad del jugador
        if playerVelY < 15:
            playerVelY += playerAccY

        ## girar solo cuando es una colisión con una tubería
        if not crashInfo["groundCrash"]:
            if playerRot > -90:
                playerRot -= playerVelRot

        ## dibujar sprites
        SCREEN.blit(IMAGES["background"], (0, 0))

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            SCREEN.blit(IMAGES["pipe"][0], (uPipe["x"], uPipe["y"]))
            SCREEN.blit(IMAGES["pipe"][1], (lPipe["x"], lPipe["y"]))

        SCREEN.blit(IMAGES["base"], (basex, BASEY))
        showScore(score)

        playerSurface = pygame.transform.rotate(IMAGES["player"][1], playerRot)
        SCREEN.blit(playerSurface, (playerx, playery))
        SCREEN.blit(IMAGES["gameover"], (50, 180))

        FPSCLOCK.tick(FPS)
        pygame.display.update()
  • La función showGameOverScreen muestra la pantalla de fin de juego cuando el jugador pierde.
  • Muestra el puntaje final del jugador, reproduce efectos de sonido y espera a que el jugador presione la tecla espacio o la flecha hacia arriba para reiniciar el juego.
  • La animación incluye el pájaro cayendo al suelo y el mensaje de fin de juego mostrado en la pantalla.
✨ Revisar Solución y Practicar

Definir funciones auxiliares

En este paso, definimos funciones auxiliares que se utilizan en el juego.

def playerShm(playerShm):
    """Hace oscilar el valor de playerShm['val'] entre 8 y -8"""
    if abs(playerShm["val"]) == 8:
        playerShm["dir"] *= -1

    if playerShm["dir"] == 1:
        playerShm["val"] += 1
    else:
        playerShm["val"] -= 1


def getRandomPipe():
    """Devuelve una tubería generada aleatoriamente"""
    ## y del espacio entre la tubería superior e inferior
    gapY = random.randrange(0, int(BASEY * 0.6 - PIPEGAPSIZE))
    gapY += int(BASEY * 0.2)
    pipeHeight = IMAGES["pipe"][0].get_height()
    pipeX = SCREENWIDTH + 10

    return [
        {"x": pipeX, "y": gapY - pipeHeight},  ## tubería superior
        {"x": pipeX, "y": gapY + PIPEGAPSIZE},  ## tubería inferior
    ]


def showScore(score):
    """Muestra el puntaje en el centro de la pantalla"""
    scoreDigits = [int(x) for x in list(str(score))]
    totalWidth = 0  ## ancho total de todos los números a imprimir

    for digit in scoreDigits:
        totalWidth += IMAGES["numbers"][digit].get_width()

    Xoffset = (SCREENWIDTH - totalWidth) / 2

    for digit in scoreDigits:
        SCREEN.blit(IMAGES["numbers"][digit], (Xoffset, SCREENHEIGHT * 0.1))
        Xoffset += IMAGES["numbers"][digit].get_width()


def checkCrash(player, upperPipes, lowerPipes):
    """Devuelve True si el jugador choca con la base o las tuberías."""
    pi = player["index"]
    player["w"] = IMAGES["player"][0].get_width()
    player["h"] = IMAGES["player"][0].get_height()

    ## si el jugador choca contra el suelo
    if player["y"] + player["h"] >= BASEY - 1:
        return [True, True]
    else:
        playerRect = pygame.Rect(player["x"], player["y"], player["w"], player["h"])
        pipeW = IMAGES["pipe"][0].get_width()
        pipeH = IMAGES["pipe"][0].get_height()

        for uPipe, lPipe in zip(upperPipes, lowerPipes):
            ## rectángulos de las tuberías superior e inferior
            uPipeRect = pygame.Rect(uPipe["x"], uPipe["y"], pipeW, pipeH)
            lPipeRect = pygame.Rect(lPipe["x"], lPipe["y"], pipeW, pipeH)

            ## máscaras de golpeo del jugador y las tuberías superior e inferior
            pHitMask = HITMASKS["player"][pi]
            uHitmask = HITMASKS["pipe"][0]
            lHitmask = HITMASKS["pipe"][1]

            ## si el pájaro choca con la tubería superior o inferior
            uCollide = pixelCollision(playerRect, uPipeRect, pHitMask, uHitmask)
            lCollide = pixelCollision(playerRect, lPipeRect, pHitMask, lHitmask)

            if uCollide or lCollide:
                return [True, False]

    return [False, False]


def pixelCollision(rect1, rect2, hitmask1, hitmask2):
    """Comprueba si dos objetos chocan y no solo sus rectángulos"""
    rect = rect1.clip(rect2)

    if rect.width == 0 or rect.height == 0:
        return False

    x1, y1 = rect.x - rect1.x, rect.y - rect1.y
    x2, y2 = rect.x - rect2.x, rect.y - rect2.y

    for x in range(rect.width):
        for y in range(rect.height):
            if hitmask1[x1 + x][y1 + y] and hitmask2[x2 + x][y2 + y]:
                return True
    return False


def getHitmask(image):
    """Devuelve una máscara de golpeo utilizando el canal alfa de una imagen"""
    mask = []
    for x in range(image.get_width()):
        mask.append([])
        for y in range(image.get_height()):
            mask[x].append(bool(image.get_at((x, y))[3]))
    return mask
  • La función playerShm hace oscilar el valor de playerShm["val"] entre 8 y -8. Esto se utiliza para mover el pájaro hacia arriba y abajo en la pantalla de bienvenida.
  • La función getRandomPipe devuelve una tubería generada aleatoriamente. Genera un espacio aleatorio entre la tubería superior e inferior.
  • La función showScore muestra el puntaje en el centro de la pantalla. Utiliza la lista IMAGES["numbers"] para mostrar el puntaje.
  • La función checkCrash devuelve True si el jugador choca con el suelo o las tuberías. Utiliza la función pixelCollision para comprobar las colisiones.
  • La función pixelCollision comprueba si dos objetos chocan y no solo sus rectángulos. Utiliza la función getHitmask para obtener la máscara de golpeo del jugador y las tuberías.
  • La función getHitmask devuelve una máscara de golpeo utilizando el canal alfa de una imagen. Utiliza la función image.get_at para obtener el valor alfa de cada píxel en la imagen.
✨ Revisar Solución y Practicar

La función principal

En este paso, definimos la función principal que inicializa el juego y comienza el bucle del juego.

def main():
    global SCREEN, FPSCLOCK
    pygame.init()
    FPSCLOCK = pygame.time.Clock()
    SCREEN = pygame.display.set_mode((SCREENWIDTH, SCREENHEIGHT))
    pygame.display.set_caption("Flappy Bird")

    ## sprites de números para mostrar el puntaje
    IMAGES["numbers"] = (
        pygame.image.load("data/sprites/0.png").convert_alpha(),
        pygame.image.load("data/sprites/1.png").convert_alpha(),
        pygame.image.load("data/sprites/2.png").convert_alpha(),
        pygame.image.load("data/sprites/3.png").convert_alpha(),
        pygame.image.load("data/sprites/4.png").convert_alpha(),
        pygame.image.load("data/sprites/5.png").convert_alpha(),
        pygame.image.load("data/sprites/6.png").convert_alpha(),
        pygame.image.load("data/sprites/7.png").convert_alpha(),
        pygame.image.load("data/sprites/8.png").convert_alpha(),
        pygame.image.load("data/sprites/9.png").convert_alpha(),
    )

    ## sprite de fin de juego
    IMAGES["gameover"] = pygame.image.load("data/sprites/gameover.png").convert_alpha()
    ## sprite del mensaje para la pantalla de bienvenida
    IMAGES["message"] = pygame.image.load("data/sprites/message.png").convert_alpha()
    ## sprite de la base (suelo)
    IMAGES["base"] = pygame.image.load("data/sprites/base.png").convert_alpha()

    while True:
        ## seleccionar sprites de fondo aleatorios
        randBg = random.randint(0, len(BACKGROUNDS_LIST) - 1)
        IMAGES["background"] = pygame.image.load(BACKGROUNDS_LIST[randBg]).convert()

        ## seleccionar sprites de jugador aleatorios
        randPlayer = random.randint(0, len(PLAYERS_LIST) - 1)
        IMAGES["player"] = (
            pygame.image.load(PLAYERS_LIST[randPlayer][0]).convert_alpha(),
            pygame.image.load(PLAYERS_LIST[randPlayer][1]).convert_alpha(),
            pygame.image.load(PLAYERS_LIST[randPlayer][2]).convert_alpha(),
        )

        ## seleccionar sprites de tubería aleatorios
        pipeindex = random.randint(0, len(PIPES_LIST) - 1)
        IMAGES["pipe"] = (
            pygame.transform.flip(
                pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(), False, True
            ),
            pygame.image.load(PIPES_LIST[pipeindex]).convert_alpha(),
        )

        ## máscara de golpeo para las tuberías
        HITMASKS["pipe"] = (
            getHitmask(IMAGES["pipe"][0]),
            getHitmask(IMAGES["pipe"][1]),
        )

        ## máscara de golpeo para el jugador
        HITMASKS["player"] = (
            getHitmask(IMAGES["player"][0]),
            getHitmask(IMAGES["player"][1]),
            getHitmask(IMAGES["player"][2]),
        )

        movementInfo = showWelcomeAnimation()
        crashInfo = mainGame(movementInfo)
        showGameOverScreen(crashInfo)
  • La función main inicializa el juego, configura la pantalla y comienza el bucle del juego.
  • Carga los activos del juego, incluyendo imágenes, y selecciona aleatoriamente los sprites de fondo, jugador y tubería.
  • El bucle del juego maneja la animación de bienvenida, el juego principal y la pantalla de fin de juego.
  • El juego continúa en un bucle hasta que el jugador sale del juego o cierra la ventana.
✨ Revisar Solución y Practicar

Ejecutar el juego

En este paso, ejecutaremos el juego Flappy Bird.

if __name__ == "__main__":
    main()
  • La condición __name__ == "__main__" comprueba si el módulo actual se está ejecutando por sí mismo o se ha importado por otro módulo.
  • Si el módulo actual se está ejecutando por sí mismo, se llama a la función main para iniciar el juego.

Una vez que hayas completado todos los pasos, puedes ejecutar el juego Flappy Bird con el siguiente comando:

cd ~/project
python flappy.py
Captura de pantalla del juego Flappy Bird
✨ Revisar Solución y Practicar

Resumen

En este proyecto, hemos dividido el código del juego Flappy Bird en múltiples pasos y hemos proporcionado explicaciones para cada paso. Has aprendido cómo crear una estructura básica de juego, manejar la entrada del jugador, actualizar el estado del juego, comprobar colisiones y mostrar pantallas de juego. Ahora puedes ejecutar y jugar al juego Flappy Bird con el código proporcionado. ¡Disfruta de tu viaje de desarrollo de juegos!