Arkanoid Level-UP!
Contents
3. Arkanoid Level-UP!¶
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:
Leer el archivo de audio
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:
El usuario teclea mover el pad a la izquierda
El usuario teclea mover el pad a la derecha
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:
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í.