3. Arkanoid Level-UP!

portada

En el capítulo anterior desarrollamos una primera versión del Arkanoid mediante las herramientas básicas que nos proporciona Python y la librería tourtle. En este capítulo, implementaremos una nueva versión haciendo uso de Pygame, una librería de Python que nos ofrece una gran variedad de herramientas para desarrollar videojuegos. El uso de esta librería nos ofrecerá una capa de abstracción en cuanto a los detalles de más bajo nivel del juego, como por ejemplo:

  • Gestión de las colisiones

  • Movimiento de los objetos

  • Creación de una interfaz gráfica

  • Gestión de la música del juego

  • Detalles estéticos del juego

See also

Puedes leer más sobre el Pygame aquí

Note

Notarás que en este capítulo no vamos a validar las funciones. El motivo principal es que es demasiado largo y lo hemos dividido en un número muy alto de funciones. Si tuvieramos que validarlas todas, ocuparía el doble. De todas formas, validar todo el código que desarrolles es una práctica muy recomendable que la que no deberías prescindir nunca porque te ayudará a reducir al mínimo el número de errores inesperados.

3.1. Creación de la pantalla de inicio

En primer lugar, configuraremos los elementos básicos de la interfaz del juego. Para ello, puedes desarrollar una función init_window(size) que recibe como único argumento la tupla size con el ancho y el largo de la ventana. El código de la función configura el tamaño de la interfaz y el reloj del juego. La función devuelve devuelve dos variables:

  • screen que contiene la instancia de la ventana del juego

  • clock que contiene la instancia del reloj

See also

Aquí puedes encontrar información sobre los métodos pygame.display y pygame.time

Solución

import pygame

def init_window(size):
    screen = pygame.display.set_mode(size)
    clock = pygame.time.Clock()
    FPS = 10
    clock.tick(FPS)
    return clock, screen
pygame 2.5.0 (SDL 2.28.0, Python 3.9.12)
Hello from the pygame community. https://www.pygame.org/contribute.html

3.2. Lectura de los mensajes de bienvenida

El siguiente paso es implementar una función read_file() que va a leer una serie de mensajes de un fichero de texto. Esta función no recibe ningún argumento de entrada y devuelve una variable messages de tipo LIST que contiene los mensajes a mostrar en la pantalla de inicio. Podríamos cargar los mensajes directamente sobre variables del código sin necesidad de leer ningún fichero de texto. Sin embargo, esta implementación es mucho más flexible y nos permite modificar los mensajes sin necesidad de cambiar el código.

Solución

def read_file():
    file = open("messages.txt", "r")
    messages = file.readlines()
    file.close()
    return messages

3.3. Pintando los mensajes de bienvenida

El objetivo de este apartado es preparar la pantalla principal del juego a partir de los mensajes leídos con la función read_file. Para ello, debes programar una función draw_messages(messages) encargada de añadir los mensajes sobre la interfaz gráfica de nuestro juego. Esta función no devuelve nada y recibe un único argumento de entrada:

  • messages variable LIST que contiene los mensajes iniciales

Note

En la solución propuesta hemos utilizado un bucle para añadir los mensajes en distintas ubicaciones de la pantalla. Todos los mensajes estarán centrados en la mitad del eje X y variarán su posición en el eje Y. Puedes cambiar los valores de init y sep para conseguir un mejor resultado a nivel estético.

init = 200
sep = 50
xaxis = width/2
for i in range(1, len(messages)):
        yaxis = init + sep * i
        rect_text.center = (xaxis, yaxis)
        screen.blit(text, rect_text)

Tip

Si quieres añadir una tipología de letra diferente a tus mensajes de inicio, puedes hacerlo mediante el método font de la libraría pygame.

See also

Puedes encontrar más información sobre el método font método aquí.

Warning

No todas las fuentes son compatibles con el método font del módulo pygame.

Solución

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

def draw_messages(messages):

    init = 300
    sep = 40
    xaxis = width/2
    for i in range(1, len(messages)):
        yaxis = init + sep * i
        font = pygame.font.Font('freesansbold.ttf', 25)
        msg = "¡¡" + messages[i] + "!!"
        text = font.render(msg, True, WHITE, BLACK)
        rect_text = text.get_rect()
        rect_text.center = (xaxis, yaxis)
        screen.blit(text, rect_text)

3.4. Lectura de las imágenes de inicio

El objetivo de este apartado es implementar una función load_images() encargada de leer y añadir imágenes a la interfaz de nuestro juego. Esta función no recibe ni devuelve ningún argumento. Simplemente carga y añade imágenes para mostrar el fondo y el logo de nuestra pantalla principal.

Note

La variable screen es una variable global, por lo que podemos acceder a ella desde el contexto local de la función load_images().

See also

Puedes encontrar más información sobre los objetos image de pygame aquí

Solución

def load_images():
    background = pygame.image.load("images/fondo.jpg")
    background_rect = background.get_rect().move(0, 0)
    screen.blit(background, background_rect)

    logo = pygame.image.load("images/logo.jpg")
    logo_rect = logo.get_rect().move(100, 100)
    screen.blit(logo, logo_rect)

3.5. Pantalla de inicio

Antes de comenzar con la lógica del juego, te aconsejamos desarrollar una función que agrupe las funciones que has programado anteriormente.

Solución

def first_screen():
    load_images()
    messages = read_file()
    draw_messages(messages)

3.6. Evento para comenzar el juego

Los siguientes dos apartados están enfocados en los principales eventos que han de producirse para comenzar y detener el juego. En primer lugar, debes programar una función check_keyboard(), encargada de arrancar la ejecución del juego cuando el usuario presione alguna tecla.

En nuestro caso, el juego comenzará cuando el usuario presione la tecla “W” del teclado. Mientras no se presione dicha tecla, el usuario visualizará la pantalla de inicio programada anteriormente con los mensajes de bienvenida.

Note

Lo primero que hacemos en la solución propuesta es obtener el estado del teclado.

keys = pygame.key.get_pressed()

Donde keys es un diccionario con el siguiente formato.

  • Clave contiene un STRING asociado a cada tecla

  • Valor coontiene un BOOL que indica si una tecla está o no pulsada

Mientras el valor asociado a la tecla W sea falso, no se iniciará el juego.

See also

Puedes encontrar más información del módulo keyboard en pygame aquí

Solución

def check_keyboard():
    keys = pygame.key.get_pressed()
    while keys[pygame.K_w] == False:
        keys = pygame.key.get_pressed()
        pygame.display.flip()
        check_quit()

3.7. Evento para detener el juego

De la misma forma que disponemos de un mecanismo para controlar el inicio de la ejecución del juego, debemos añadir otro que nos permita finalizarla cuando así lo deseemos. De no ser así, tendríamos que finalizar el proceso iniciado por pygame de manera manual desde la linea de comandos.

Para ello, debes programar la función chek_quit() encargada de detener la ejecución del juego cuando el usuario seleccione la casilla X de la interfaz gráfica. De nuevo, no recibe ni devuelve ningún argumento, simplemente finaliza la ejecución del juego.

Note

Pygame nos permite iterar sobre los eventos del juego de manera muy sencilla.

for event in pygame.event.get():

Para comprobar el tipo de evento, puedes acceder al atributo type de los objetos event. Si el tipo del evento es QUIT, entonces el usuario habrá terminado la ejecución del juego. Para finalizar el proceso debes importar la librería sys y ejecutar la siguiente sentencia:

import sys
sys.exit()

Solución

def check_quit():
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

3.8. Música del juego

Una de las múltiples ventajas de utilizar pygame es poder añadir música a nuestro juego con unas pocas líneas de código. En este simple apartado debes programar una función music que no recibe ni devuelve ningún argumento y se encarga de lo siguiente:

    1. Leer el archivo de audio

    1. Activar la reproducción del audio durante la ejecución del juego.

Note

Pygame nos permite iterar sobre los eventos del juego de manera muy sencilla.

for event in pygame.event.get():

Para comprobar el tipo de evento, puedes acceder al atributo type de los objetos event. Si el tipo del evento es QUIT, entonces el usuario habrá terminado la ejecución del juego. Para finalizar el proceso utilizaremos la siguiente sentencia:

sys.exit()

See also

Puedes encontrar más información sobre el módulo mixer aquí

Solución

def music():
    file = 'music/MusicRetro.mp3'
    pygame.init()
    pygame.mixer.init()
    pygame.mixer.music.load(file)
    pygame.mixer.music.play(-1)  

3.9. Creación de los elementos del juego

Ya ha llegado el momento de cargar los elementos principales para comenzar a implementar las funcionalidades del juego. Esta versión del Arkanoid está compuesta por 3 elementos:

  • Pelota

  • Pad (raqueta)

  • Ladrillos

El siguiente apartado está destinado a añadir dichos elementos sobre la interfaz del juego.

Pelota y pad

Debes programar una función ball() y una función pad() encargadas de añadir la pelota y la raqueta sobre la interfaz. Para ello, tienes que cargar la imagen de la pelota y de la raqueta e inicializar sus posiciones.

Estas funciones no reciben argumentos de entrada y devuelven los siguientes parámetros:

  • Instancia de la variable que contiene la imagen cargada

  • Instancia del objeto Rect asociado a cada imagen

See also

Puedes encontrar más información sobre los objetos Rect aquí

Solución

def ball():
    ball = pygame.image.load("images/ball.png")
    ballrect = ball.get_rect().move(width/2, height/2)
    return ball, ballrect
def pad():
    pad = pygame.image.load("images/pad.png")
    rect_pad = pad.get_rect().move(width/2, height - 2 * attempts)
    return pad, rect_pad

Ladrillos

Ahora vas a añadir los ladrillos en la parte superior de la interfaz para comenzar a implementar las colisiones. La inicialización de los ladrillos no será tan sencilla como el del bate o la raqueta. Para conseguirlo debes programar una función bricks_init(columns_number) que recibe un único argumento de entrada:

  • columns_number que contiene una variable INT con el número de ladrillos por fila

Y devuelve dos variables:

  • bricks con una variable tipo LIST con las imágenes cargadas

  • rect_bricks con una variable tipo LIST con los objetos Rect asociados a la lista bricks

Note

Lo primero que hacemos en la solución propuesta es crear las listas de ladrillos que posteriormente serán añadidas a la pantalla del juego. Para ello, hemos usado un bucle para el número de filas y otro bucle para el número de ladrillos por fila. De nuevo, jugaremos con variables init y separation para colocar cada uno de los ladrillos en su ubicación correspondiente.

Solución

def bricks_init(columns_number):

    bricks = []
    rect_bricks = []
    separation = 42
    init = 8
    height = 12
    rows_number = 3

    for j in range(rows_number):

        for i in range(columns_number):

            brick = pygame.image.load("images/Ladrilo.PNG")
            rect_brick = brick.get_rect().move(init + i * separation, j * height)
            bricks.append(brick)
            rect_bricks.append(rect_brick)

    return bricks, rect_bricks

3.10. Añadiendo los elementos a la interfaz

De nuevo vamos a agregar todas las funciones desarrolladas anteriormente para favorecer la legibilidad de nuestro programa final. Ahora tendrás que programar una función draw_screen() que no recibe ni devuelve ningún argumento y se encarga de invocar a los elementos del juego.

En este punto es importante que seamos conscientes de que los ladrillos han de ir desapareciendo a medida que la pelota los golpea, por lo que necesitamos algún mecanismo que añada únicamente los ladrillos que no han sido derribados.

Solución

def draw_screen():
    screen.fill(BLACK)

    screen.blit(ball, ballrect)  
    screen.blit(pad, padrect)
    for i in range(len(bricks)):
        if bricks[i] != "Brick dead":
            screen.blit(bricks[i], rect_bricks[i])

Note

Lo primero que hacemos en la solución propuesta es recorrer cada uno de los elementos pertenecientes a la lista bricks. Cuando desde el programa principal detectemos que un ladrillo ha sido derribado, sustituiremos su instancia en la lista bricks por un mensaje que nos ayude a distinguirlos de los ladrillos que todavía no han sido derribados.

for i in range(len(bricks)):
        if bricks[i] != "Brick dead":
            screen.blit(bricks[i], rect_bricks[i])

De esta manera, solo añadiremos a la pantalla del juego aquellos ladrillos que no hayan sido derribados.

3.11. Movimientos del pad y colisión con la pelota

Este apartado se centra en implementar los movimientos de la raqueta mediante la función pad_movements(speed, padrect) que recibe dos argumentos de entrada y los devuelve actualizados:

  • speed contiene una lista con la velocidad de la pelota

  • padrect contiene un objeto Rect asociado a la raqueta

Respecto al pad, podemos distinguir 3 posibles situaciones:

  1. El usuario teclea mover el pad a la izquierda

  2. El usuario teclea mover el pad a la derecha

  3. La pelota colisiona contra el pad

See also

Puedes encontrar información sobre las colisiones aquí

Solución

def pad_movements(speed, padrect):

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        padrect = padrect.move(-1, 0)
    if keys[pygame.K_RIGHT]:
        padrect = padrect.move(1, 0)
    if padrect.colliderect(ballrect):
        speed[1] = -speed[1]
    return speed, padrect

Note

Para mover el pad seguiremos capturando el teclado y utilizaremos el diccionario keys explicado anteriormente para decidir hacia donde movemos la raqueta. Para cambiar la dirección de la pelota cuando detectemos colisión con el pad, solo invertiremos su sentido, multiplicando la velocidad por -1.

if padrect.colliderect(ballrect):
        speed[1] = -speed[1]

3.12. Calculando coordenadas aleatorias

En este apartado vas a añadir un fondo dinámico a la pantalla del Araknoid. Para ello, vas a simular un fondo de estrellas añadiendo círculos de color blanco en posiciones aleatorias.

En primer lugar, debes programar la función coordinates(), que no recibe ningún argumento y devuelve una lista con las coordenadas X,Y iniciales de las estrellas.

Tip

Puedes usar la librería random para generar aleatoriamente las posiciones iniciales.

x = random.randint(0, width)
y = random.randint(0, heigth)

Solución

import random

def coordinates():

    coor_list = []

    for i in range(60):
        x = random.randint(0, 820)
        y = random.randint(0, 740)
        coor_list.append([x, y])
    return coor_list

3.13. Añadiendo las estrellas

Con la función coordinates() implementada correctamente, vamos a añadir las estrellas a la pantalla del juego mediante la función draw_stars(coor_list), que no devuelve nada y recibe un único argumento de entrada:

  • coor_list que es una variable tipo LIST con las coordenadas aleatorias

Hasta ahora hemos desarrollado un fondo estático, pero podemos convertirlo en dinámico de manera muy sencilla. La idea es aumentar en una unidad la coordenada Y de las estrellas en cada iteración del bucle de juego. De estea manera, simularemos que la estrella se está moviendo en caída libre.

See also

Puedes encontrar más información sobre como figuras con pygame aquí.

Solución

def draw_stars(coor_list):

    for coordinate in coor_list:
        x = coordinate[0]
        y = coordinate[1]

        coordinate[1] += 1
        if coordinate[1] > 740:
            coordinate[1] = 0
        pygame.draw.circle(screen, WHITE, (x, y), 3)

    return coor_list

Note

Lo primero que hacemos es recorrer la lista de coordenadas y aumentar la coordenada Y de cada estrella en una unidad. Si alguna estrella ha llegado a la parte inferior de la pantalla, debemos resetear su posicion, inicializándola a 0.

if coordinate[1] > 740:
coordinate[1] = 0

3.14. Pantalla de derrota

Cuando la raqueta llega tarde a devolver la pelota, el programa principal debe invocar una ventana de derrota encargada de mostrar la puntuación obtenida por el jugador.

La función gameover() no recibe ni devuelve ningún argumento. Simplemente muestra la puntuación obtenida por el jugador en una ventana nueva de manera estática, hasta que el usuario cierre la ventana de manera manual pinchando en la X. Puedes añadir imágenes a esta última pantalla mediante las técnicas aprendidas anteriormente.

Solución

def gameover():

    while True:

        msg = f"Your score has been {score}"
        font = pygame.font.Font('freesansbold.ttf', 45)
        text = font.render(msg, True, WHITE, BLACK)
        rect = text.get_rect()
        rect.center = (420, 400)
        screen = pygame.display.set_mode(size)
        load_images()
        screen.blit(text, rect)

        check_quit()
        pygame.display.flip()

3.15. Programa principal

El programa principal es el motor de nuestro juego. Es el responsable de invocar a las funciones programadas anteriormente en los instantes oportunos. Para conseguiruna correcta simulación del juego, el programa principal gestionará las siguientes funcionalidades:

  • Puntuación del jugador. Por cada ladrillo derribado, la puntuación del jugador aumentará en 10 puntos

  • Salud de los ladrillos. El programa principal debe gestionar la vida de cada uno de ellos. Cada ladrillo necesitará 2 colisiones de la pelota para ser derribado

  • Creación de los objetos del juego. El programa principal invocará a las funciones necesarias para crear los ladrillos, el pad y la pelota, para poder ser añadidos a la pantalla de juego

  • Gestión de las pantallas inicial, de juego y derrota en los instantes adecuados. El programa principal deberá invocar a las funciones encargadas de mostrar las distintas pantallas del juego y los elementos que las componen

  • Gestión de las colisiones de la pelota y vidas del jugador. Cada vez que la pelota colisione con la parte inferior de la interfaz gráfica, el jugador perderá una vida. Cuando el jugador se quede sin vidas, deberá mostrarse la pantalla pantalla de derrota

  • Música del juego. El programa principal debe comenzar la reproducción del audio del juego al inicio de la ejecución del programa

  • Bucle del juego. Cuando el jugador comience a jugar, arrancará un bucle que se ejecutará hasta que el jugador pierda todas sus vidas

Para controlar la dirección de la pelota, se utiliza la variable speed de tipo LIST. Esta variable está formada por dos únicos elementos que reflejan la velocidad en los ejes horizontal y vertical, respectivamente. La lista speed nos ayudará a mover la pelota en determinadas direcciones gracias al método move de los objetos Rect. Los posibles valores de esta lista son 0, +1 y -1, representando los vectores del movimiento.

La pelota estará moviéndose en una dirección determinada en cada iteración del bucle del juego. Cuando la pelota colisione, deberemos invertir su trayectoria, en el eje vertical u horizontal.

ballrect = ballrect.move(speed)
    if ballrect.left < 0 or ballrect.right > width:
        speed[0] = -speed[0]
    if ballrect.top < 0 or ballrect.bottom > height:
        speed[1] = -speed[1]
    if ballrect.bottom == height:

Cuando la pelota colisiona con la parte inferior de la pantalla, el juego deberá registrar que el usuario ha perdido una vida. Además, deberá resetear la posición de la pelota e iniciar un nuevo intento.

if ballrect.bottom == height:

        time.sleep(0.5)
        speed[1] = -speed[1]
        ballrect = ball.get_rect().move(width/2, height/2)
        ballrect = ballrect.move(speed)
        lives -= 1

Si el usuario ha perdido todas las vidas, deberemos salir del bucle del juego y activar la pantalla de derrota.

if lives == 0:
    loser = True
    speed[1] = 0
    speed[0] = 0
    play = False

Si todo ha ido bien, tu juego tendrá una pinta muy similar a esta:

../../_images/mainpage.png
../../_images/playpage.png

Solución

brick_number = 19
brick_health = [2 for i in range(3 * brick_number)]  # list comprehension
score = 0
lives = 3
attempts = 12
size = width, height = 820, 740
speed = [1, 1]
import time
import sys

clock, screen = init_window(size)
music()
first_screen()
check_keyboard()
bricks, rect_bricks = bricks_init(brick_number)
ball, ballrect = ball()
pad, padrect = pad()
coor_list = coordinates()
play = True

while play:

    draw_screen()
    coor_list = draw_stars(coor_list)

    check_quit()

    ballrect = ballrect.move(speed)
    if ballrect.left < 0 or ballrect.right > width:
        speed[0] = -speed[0]
    if ballrect.top < 0 or ballrect.bottom > height:
        speed[1] = -speed[1]
    if ballrect.bottom == height:

        time.sleep(0.5)
        speed[1] = -speed[1]
        ballrect = ball.get_rect().move(width/2, height/2)
        ballrect = ballrect.move(speed)
        lives -= 1
        if lives == 0:
            loser = True
            speed[1] = 0
            speed[0] = 0
            play = False

    speed, padrect = pad_movements(speed, padrect)

    for position in range(len(rect_bricks)):

        if rect_bricks[position].colliderect(ballrect):

            speed[1] = -speed[1]
            brick_health[position] -= 1

            if brick_health[position] == 0:

                score += 10
                bricks[position] = "Brick dead"

    pygame.display.flip()

if loser:
    gameover()

Note

Lo primero que hacemos en la solución propuesta es recorrer los índices de las listas de ladrillos. Estos índices son comunes a las listas rect_bricks, bricks y brick_health.

Si detectamos colisión con la pelota en alguno de los ladrillos, invertiremos la velocidad de la pelota en el eje vertical y restaremos una vida a dicho ladrillo en la lista brick_health.

Si alguno de los ladrillos ha sido derribado por completo (su salud en la lista brick_health es 0) sustituiremos su instancia en la lista bricks por un mensaje que nos facilite saber si el ladrillo ha sido derribado o no.

for position in range(len(rect_bricks)):
        if rect_bricks[position].colliderect(ballrect):
            speed[1] = -speed[1]
            brick_health[position] -= 1
        if brick_health[position] == 0:
            score += 10
            bricks[position] = "Brick dead"

3.16. Extensiones del Juego

Al ser el mismo juego, las sugerencias que te hicimos en el capítulo anterior, aplican también a este. A partir de ahora, confiamos en que con todos los recursos que has aprendido a lo largo de estos capítulos las mejoras se te ocurran a tí.