Cap. 17 - PyGame
PyGame es un paquete que no es parte de la distribución estándar de Python, así que si aun no lo tienes instalado (caso en que un intento de hacer import pygame fallará), puedes descargar e instalar una versión actualizada desde PyGame - Download.
- Estas notas se basan en PyGame 2.1.2, la versión más reciente disponible para Windows en el momento en que se escribieron estas líneas.
- Para instalar en Windows, conviene seguir las instrucciones de este link: PyGame - Getting Started
- En la línea de comandos, escribir:
py -m pip install -U pygame --user
- Esto iniciará la descarga e instalación, que en enero de 2022 instalaba la versión pygame-2.1.2
- Ejemplo de output:
C:\Users\javid\Downloads>py -m pip install -U pygame --user Collecting pygame Downloading pygame-2.1.2-cp310-cp310-win_amd64.whl (8.4 MB) |¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦¦| 8.4 MB 322 kB/s Installing collected packages: pygame Successfully installed pygame-2.1.2
Pygame viene con un conjunto muy amplio de tutoriales, ejemplos y archivos de ayuda, por lo cual hay mucho para explorar y experimentar con esta clase.
- Para encontrar estos recursos habrá que trabajar un poco.
- Si se instaló a PyGame en un sistema Windows, es probable que estén en una carpeta C:\Python31\Lib\site-packages\pygame\ donde encontrarás directorios para docs y ejemplos.
17.1) El loop del juego
La estructura de los juegos que consideraremos seguirá siempre el siguiente patrón:
En todo juego, en la sección setup creamos una ventana, leemos y preparamos cierto contenido, y entonces entramos en el loop del juego (game loop). El loop del juego hace continuamente cuatro cosas principales:
- sondea por eventos - pregunta al sistema si los eventos han ocurrido - y responde apropiadamente,
- actualiza las estructuras de datos internos u objetos que sea necesario cambiar,
- dibuja el estado actual del juego en una superficie no visible,
- muestra al jugador la superficie que acaba de dibujar (la pone en display).
import pygame def main(): """ Set up del juego e inicio de su loop principal """ pygame.init() # Prepara el módulo pygame para su uso tamaño_superficie = 480 # Tamaño deseado de la superficie, en pixels. # Crear superficie (ancho, alto) y su ventana. superficie_principal = pygame.display.set_mode((tamaño_superficie, tamaño_superficie)) # Setear los datos necesarios para describir un pequeño rectángulo y su color pequeño_rectangulo = (300, 200, 150, 90) un_color = (255, 0, 0) # Un color es una combinación de (Rojo, Verde, Azul) while True: ev = pygame.event.poll() # Sondeo a ver si ocurrió un evento if ev.type == pygame.QUIT: # Si el evento es que se hizo click en el botón cerrar ventana break # ... abandonar el loop principal del juego # Actualiza los objetos de tu juego y la estructura de datos aquí... # Dibujamos todo desde cero en cada frame. # Así que comencemos por llenar todo con el color de fondo superficie_principal.fill((0, 200, 255)) # Pintemos un pequeño rectángulo en la superficie principal superficie_principal.fill(un_color, pequeño_rectangulo) # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre! pygame.display.flip() pygame.quit() # Si hemos salido del loop principal del juego, sólo queda cerrar la ventana. main()
Este programa muestra una ventana que se queda ahí hasta que la cerramos:
Pygame hace todos sus dibujos sobre superficies rectangulares. Tras inicializar PyGame en la línea 5, creamos una ventana que contenga a nuestra superficie principal (líneas 6-9). El loop principal del juego se extiende desde la línea 15 a la 30, con los siguientes aspectos fundamentales a tener en cuenta:
- Primero (línea 16) sondeamos para captar el siguiente evento que nos sea reportado. Este paso siempre será seguido por algunas sentencias condicionales que determinarán si tal o cual evento en los cuales estamos interesados han ocurrido. El uso de un evento lo consume (al menos en la lógica PyGame), por lo cual sólo tenemos una chance para captarlo y utilizarlo.
- En la línea 17 testeamos si el tipo de evento es la constante predefinidad pygame.QUIT. Este es el evento que veremos cuando el usuario haga click en el botón cerrar de la ventana PyGame. Como respuesta a este evento, abandonamos el loop principal.
- Una vez que hayamos salido del loop, el código en la línea 32 cierra la ventana, y retornamos de la función main. El programa puede luego continuar haciendo otras cosas, o bien reinicializar pygame y crear una nueva ventana, pero lo más habitual es que termine y punto.
- Hay toda clase de eventos - tecla presionada, movimiento del mouse, click del mouse, movimiento del joystick, etc. Es habitual que manejemos todos los casos que sean relevantes para nuestro juego en código que habría que poner antes de la línea 19. La idea general es "maneja los eventos primero, y luego ocúpate del resto".
- En la línea 20 estaríamos actualizando objetos y datos - por ejemplo, si quisiéramos variar el color, posición o tamaño del rectángulo que vamos a dibujar más adelante, reaisgnaríamos en este momento los valores de un_color o pequeño_rectángulo.
- Una forma moderna de escribir juegos (ahora que tenemos computadoras rápidas y tarjetas gráficas rápidas) es redibujar todo desde cero en cada iteración del loop. Así que lo primero que hacemos en la línea 24 es llenar la superficie completa con un color de fondo. El método fill de una superficie recibe dos argumentos (el color que usará para rellenar, y el rectángulo que va a rellenar). Pero el segundo argumento es opcional, y si no se pasa, entonces se pinta la superficie completa.
- En la línea 27 pintamos un segundo rectángulo, esta vez usando el color un_color. La ubicación y tamaño del rectángulo quedan dados por la tupla pequeño_rectangulo, tupla de 4 elementos (x, y, ancho, alto).
- Es importante comprender que el origen de la superficie PyGame está en la esquina superior izquierda (a diferencia del módulo tortuga, que pone el origen en el centro de la pantalla). Así que si quieres un rectángulo en la parte alta de pantalla, debes utilizar valores de y pequeños.
- Si tu hardware de display de imagen intenta leer de memoria al mismo tiempo que el programa escribe en memoria, interferirán y causarán ruido y parpadeo en pantalla. Para evitar esto, PyGame mantiene dos buffers en la superficie principal - el back buffer (buffer trasero) que es a donde dibuja el programa, mientras que el front buffer (buffer delantero) está siendo mostrado al usuario. Cada vez que el programa termina de preparar el back buffer, intercambia el rol de trasero/delantero de ambos buffers. Así que el dibujo en las líneas 24 a 27 no cambia lo que es visto en pantalla hasta que los buffers son intercambiados por el método flip en la línea 30.
17.2) Mostrar imágenes y texto
Para dibujar una imagen en la superficie principal, cargamos la imagen (digamos, una pelota de playa) en una superficie nueva de su propiedad.
- La superficie principal tiene un método blit que copia píxeles desde la superficie de la bola de playa hacia sí misma.
- Cuando llamamos a blit, podemos especificar dónde debería ubicarse la pelota de playa en la superficie principal.
- El término blit es ampliamente utilizado en computación gráfica, y significa hacer una copia rápida de píxeles de una área de memoria a otra.
Así que en la sección setup, antes de entrar en el game loop, cargaríamos la imagen, así:
pelota = pygame.image.load("pelota.png")
- y despuès de la lìnea 28 del programa anterior, agregarìamos el còdigo siguiente para mostrar la pelota en la posiciòn (100, 120):
superficie_principal.blit(pelota, (100, 120))
Para mostrar texto, debemos hacer 3 cosas. Antes de entrar al loop, instanciamos un objeto font:
# Instanciar fuente Courier de 16 puntos para dibujar texto. mi_fuente = pygame.font.SysFont('Courier', 16)
Y después de la lìnea 28, una vez màs, utilizamos el mètodo render de font para crear una nueva superficie que contiene los píxeles del texto dibujado, y luego, como en el caso de las imágenes, hacemos blit de la nueva superficie en la superficie principal.
- Observar que render requiere dos parámetros extra.
- El segundo le indica si debe suavizar cuidadosamente los bordes del texto al dibujarlo (proceso que se conoce como anti-aliasing)
- El tercero es el color con que queremos dibujar el texto. En este ejemplo usamos (0, 0, 0) para negro.
el_texto = mi_fuente.render('Hola, mundo!', True, (0,0,0)) superficie_principal.blit(el_texto, (10, 10))
Demostraremos estas dos nuevas funcionalidades contando los frames - iteraciones del loop principal - y llevando la cuenta del tiempo.
- En cada frame mostraremos la cuenta del frame y la frecuencia del frame.
- Sólo actualizaremos la frecuencia del frame (frame rate) cada 5000 frames, que es cuando revisaremos el intervalo de tiempo para hacer los cálculos.
import pygame import time def main(): pygame.init() # Prepara el módulo pygame para su uso superficie_principal = pygame.display.set_mode((480, 240)) # Leer una imagen para dibujar. Usa una de tu propiedad. # PyGame maneja los tipos de imagen típicos: gif, jpg, png, etc. pelota = pygame.image.load("pelota.png") # Crear una fuenta para mostrar el texto mi_fuente = pygame.font.SysFont('Courier', 16) cuenta_frames = 0 frecuencia_frames = 0 t0 = time.time() while True: # Sondeo por un evento del teclado, mouse, joystick, etc. ev = pygame.event.poll() if ev.type == pygame.QUIT: # Si el evento es que se hizo click en el botón cerrar ventana break # ... abandonar el loop principal del juego # Implementamos aquí un poco de la lógica del juego cuenta_frames += 1 if cuenta_frames % 500 == 0: t1 = time.time() frecuencia_frames = 500 / (t1-t0) t0 = t1 # Completamente redibujamos la superficie, comenzando por el fondo superficie_principal.fill((0, 200, 255)) # Ponemos un rectángulo rojo en algún punto de la superficie superficie_principal.fill((255, 0, 0), (300, 100, 150, 90)) # Copiamos nuestra imagen a la superficie, en la posición (x, y) señalada: superficie_principal.blit(pelota, (100, 120)) # Creamos una nueva superficie con una imagen del texto el_texto = mi_fuente.render('Frame = {0}, frecuencia = {1:.2f} fps' .format(cuenta_frames, frecuencia_frames), True, (0,0,0)) # Copiamos la superficie del texto a la superficie principal superficie_principal.blit(el_texto, (10, 10)) # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre! pygame.display.flip() pygame.quit() # Si hemos salido del loop principal del juego, sólo queda cerrar la ventana. main()
La frecuencia de frames es ridículamente alta - mucho más alta de lo que el ojo humano puede procesar.
- Los juegos de video comerciales suelen mostrarse a unos 60 frames por segundo (fps).
- Por supuesto, nuestro altísima frecuencia se va a reducir drásticamente cuando comencemos a hacer algo un poco más relevante dentro de nuestro loop principal.
17.3) Dibujando un tablero para el problema de las N reinas
Ya habíamos resuelto el problema de las N reinas. Para el caso del tablero 8x8, una de las soluciones era [6, 4, 2, 0, 5, 7, 1, 3].
- Vamos a usar esa solución como dato, y dibujaremos con PyGame el tablero de ajedrez con esas 8 reinas.
Crearemos un nuevo módulo para el código de este proyecto, que llamaremos dibujar_reinas.py.
- Cuando tengamos nuestros casos de testeo funcionando, podremos volver al programa que resolvía el problema, importar este nuevo módulo, y agregar un llamado a nuestra nueva función para dibujar un tablero cada vez que se encuentre una nueva solución.
Comencemos con un fondo de cuadrados rojos y negros para el tablero.
- Podríamos crear una imagen en un archivo e importarlo, pero dicha solución requeriría distintas imágenes de fondo para distintos tamaños del tablero.
- Por lo tanto, nos limitaremos a dibujar rectángulos rojos y negros del tamaño adecuado en las posiciones que sean necesarias.
import pygame def dibujar_tablero(tablero): """ Dibuja un tablero de ajedrez con reinas, a partir de tablero. """ pygame.init() colores = [(255,0,0), (0,0,0)] # Establece dos colores [rojo, negro] n = len(tablero) # Este es un tablero de ajedrez NxN tamaño_superficie = 480 # Superficie física disponible tamaño_cuadro = tamaño_superficie // n # Tamaño de un cuadro tamaño_superficie = n * tamaño_cuadro # Ajuste para que la proporción sea exacta. # Crear la superficie de (ancho, alto), con su ventana superficie = pygame.display.set_mode((tamaño_superficie, tamaño_superficie))
Aquí hemos precomputado tamaño_cuadro, el tamaño de cada cuadro dado como un número entero, para que los cuadrados encajen exactamente dentro del ancho de pantalla.
- Entonces si queremos dibujar un tablero de tamaño 480x480, con 8x8 cuadros, cada cuadro habrá de tener un tamaño de 60 unidades.
- Pero un tablero de 7x7 no encaja exactamente en 480
- Para evitar que un cuadro quede desparejo por el no-encaje perfecto, recomputamos la superficie para que coincida exactamente con nuestros cuadrados.
Ahora dibujemos los cuadrados, en el loop del juego. Necesitamos un loop anidado: el loop externo recorrerá las filas del tablero, el interno las columnas.
# Dibujar un tablero de ajedrez vacío como fondo for fila in range(n): # Dibujar cada fila en el tablero. color_indice = fila % 2 # Cambiar el color inicial de cada fila for columna in range(n): # Recorrer las columnas dibujando cuadrados este_cuadro = (columna*tamaño_cuadro, fila*tamaño_cuadro, tamaño_cuadro, tamaño_cuadro) superficie.fill(colores[color_indice], este_cuadro) # ahora invertimos el índice de color para el próximo cuadro color_indice = (color_indice + 1) % 2
Hay dos ideas importantes en este código:
- Primero, computamos el rectángulo que ha de ser coloreado a partir de las variables de loop fila y columna, multiplicándolos por el tamaño del cuadro para obtener su posición.
- Por supuesto, cada cuadro tiene un ancho y alto fijos, así que este_cuadro representa el rectángulo que ha de ser rellenado en la iteración actual del loop.
- Segundo, tenemos colores alternantes en cada cuadro.
- En el código de setup inicial habíamos creado una lista que contenía dos colores.
- Aquí utilizamos color_indice (que siempre tendrá el valor de 0 o 1) para iniciar cada fila en un color que será distinto al del inicio de la fila anterior, y para ir invirtiendo colores cada vez que se llena un cuadro.
Para ver el tablero propiamente dicho, hay que agregar el código necesario para hacer el flip de la imagen al display, y también el sondeo de eventos para detectar al menos si el usuario decide cerrar la ventana:
while True: # Sondeo por un evento del teclado, mouse, joystick, etc. ev = pygame.event.poll() if ev.type == pygame.QUIT: # Si el evento es que se hizo click en el botón cerrar ventana break # ... abandonar el loop principal del juego (...) # Líneas del loop principal, ya mostradas arriba # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre! pygame.display.flip()
Un simple llamado a la función dibujar_tablero() nos permite ver el resultado, que es un tablero de ajedrez cuyo tamaño depende del largo de la lista pasada como argumento:
Ahora hay que dibujar las reinas! Recordemos que nuestra solución [6, 4, 2, 0, 5, 7, 1, 3] significa que en la columna 0 del tablero queremos una reina en la fila 6, en la columna 1 queremos una reina en la fila 4, y así sucesivamente. Por lo tanto necesitamos un loop que recorra cada reina de la solución:
for (columna, fila) in enumerate(tablero): # dibujar una reina en columna, fila...
En este capítulo ya habíamos utilizado la imagen de una bola, así que la volveremos a usar en lugar de una reina. En el código setup, antes del loop principal del game, cargamos la imagen de la pelota (como habíamos hecho antes) y en el cuerpo del loop agregamos la línea:
superficie.blit(pelota, (columna * tamaño_cuadro, fila * tamaño_cuadro))
Estamos cerca, pero nos faltaría centrar a esas reinas en sus cuadros!
- Nuestro problema deriva del hecho de que tanto la pelota como el rectángulo tienen su extremo superior derecho izquierdo como punto de referencia.
- Si vamos a centrar la pelota en el cuadro, necesitamos dar un offset extra tanto en la dirección x como en la y.
- Observación: Dado que la pelota es redonda y el cuadro es un cuadrado, el offset en ambas direcciones será el mismo, así que computaremos una solo valor de offset y lo utilizaremos en ambas direcciones.
El offset que necesitamos viene dado por la fórmula: (tamaño_cuadro - tamaño_pelota)/2
- Así que precomputaremos este valor en la sección setup del juego, después de haber leído la pelota y determinado el tamaño del cuadro:
offset_pelota = (tamaño_cuadro - pelota.get_width()) // 2
Ahora sólo falta retocar el código que dibuja a la pelota y terminamos:
superficie.blit(pelota, (columna * tamaño_cuadro + offset_pelota, fila * tamaño_cuadro + offset_pelota))
¿Qué pasaría si la pelota fuera mayor que el cuadro? En este caso, offset_pelota sería negativo y la pelota seguiría estando centrada en el cuadrado, sólo que se saldría de sus bordes o incluso podría taparlo.
Aquí está el programa completo:
import pygame def dibujar_tablero(tablero): """ Dibuja un tablero de ajedrez con reinas, a partir de tablero. """ pygame.init() colores = [(255,0,0), (0,0,0)] # Establece dos colores [rojo, negro] n = len(tablero) # Este es un tablero de ajedrez NxN tamaño_superficie = 480 # Superficie física disponible tamaño_cuadro = tamaño_superficie // n # Tamaño de un cuadro tamaño_superficie = n * tamaño_cuadro # Ajuste para que la proporción sea exacta. # Crear la superficie de (ancho, alto), con su ventana superficie = pygame.display.set_mode((tamaño_superficie, tamaño_superficie)) # Leer la imagen de la pelota (que vamos a usar para representar a las reinas) pelota = pygame.image.load("pelota.png") # Definir el offset de la pelota (para centrarla en lo cuadros) offset_pelota = (tamaño_cuadro - pelota.get_width()) // 2 while True: # Sondeo por un evento del teclado, mouse, joystick, etc. ev = pygame.event.poll() if ev.type == pygame.QUIT: # Si el evento es que se hizo click en el botón cerrar ventana break # ... abandonar el loop principal del juego # Dibujar un tablero de ajedrez vacío como fondo for fila in range(n): # Dibujar cada fila en el tablero. color_indice = fila % 2 # Cambiar el color inicial de cada fila for columna in range(n): # Recorrer las columnas dibujando cuadrados este_cuadro = (columna*tamaño_cuadro, fila*tamaño_cuadro, tamaño_cuadro, tamaño_cuadro) superficie.fill(colores[color_indice], este_cuadro) # ahora invertimos el índice de color para el próximo cuadro color_indice = (color_indice + 1) % 2 # Dibujamos las reinas for (columna, fila) in enumerate(tablero): superficie.blit(pelota, (columna * tamaño_cuadro + offset_pelota, fila * tamaño_cuadro + offset_pelota)) # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre! pygame.display.flip() pygame.quit() # Si hemos salido del loop principal del juego, sólo queda cerrar la ventana. if __name__ == '__main__': dibujar_tablero([0, 5, 3, 1, 6, 4, 2]) # 7 x 7 para testear el ajuste del tamaño de ventana dibujar_tablero([6, 4, 2, 0, 5, 7, 1, 3]) dibujar_tablero([9, 6, 0, 3, 10, 7, 2, 4, 12, 8, 11, 5, 1]) # 13 x 13 dibujar_tablero([11, 4, 8, 12, 2, 7, 3, 15, 0, 14, 10, 6, 13, 1, 5, 9])
Aquí vale la pena detenerse en un punto. La condición if __name __
al final del programa se fija si el nombre del programa que estamos ejecutando es __main__
. Esto permite distinguir entre el caso en que este módulo es ejecutado como el programa principal, y el caso en que es importado por otro módulo.
- Si ejecutamos este módulo en Python, las 4 llamadas a dibujar_tablero que siguen a esa línea serán ejecutadas.
- Sin embargo si importamos el módulo desde otro programa (por ejemplo nuestro programa para resolver el problema de las N reinas, implementado en un capítulo anterior) la condición de la línea
if __name__
va a ser falsa, y los 4 llamados a dibujar_tablero no se van a ejecutar.
En la sección Problema de 8 reinas, parte 2 nuestro programa principal se veía así:
def main(): import random rng = random.Random() # Instanciar al generador tablero = list(range(8)) # Generate la permutación inicial encontrados = 0 intentos = 0 while encontrados < 10: rng.shuffle(tablero) intentos += 1 if not tiene_conflictos(tablero): print("Encontré la solución {0} en {1} intentos.".format(tablero, intentos)) intentos = 0 encontrados += 1 main()
Ahora sólo necesitamos dos cambios.
- En el top del programa, importaremos el módulo dibujar_reinas en el cual acabamos de trabajar (recordar que es conveniente que los dos módulos hayan sido salvados en la misma carpeta).
- Y luego de la línea 10 agregamos un llamado para dibujar la solución que acabamos de descubrir.
dibujar_reinas.dibujar_tablero(tablero)
Y con esto llegamos a una versión muy satisfactoria del programa que es capaz de buscar soluciones al problema de las N reinas, y cuando encuentra cada una, muestra el tablero de ajedrez con las reinas en las posiciones dadas por la misma.
17.4) Sprites
Un sprite es un objeto que se puede mover en un juego, y tiene estado interno y comportamiento propios. Por ejemplo, los personajes que se mueven por su cuenta en juegos de video son sprites, las naves o vehículos son sprites, las balas y bombas son sprites, y el propio jugador es un sprite.
La programación orientada a objetos (POO) es la forma ideal de describir esta situación: cada objeto puede tener sus propios atributos y estado interno, y un par de métodos. Hagamos algunos experimentos con nuestro tablero de N reinas. En vez de ubicar a la reina en su posición final, la vamos a soltar en lo alto del tablero, y dejar que ella caiga a su posición, tal vez incluso rebotando en el camino.
La primera encapsulación que vamos a necesitar es convertir a cada reina en un objeto. Vamos a tener una lista de todos los sprites activos (es decir una lista de los objetos reina) y considerar un par de cosas más en nuestro loop principal del juego:
- Después de manejar los eventos, pero antes de dibujar, llamaremos a un método update (actualizar) para cada sprite. Esto le permite a cada sprite modificar su estado interno de cierta manera - tal vez cambiando su imagen, o su posición, o rotando, o aumentando o disminuyendo su tamaño.
- Una vez que todos los sprites se hayan actualizado a sí mismos, el loop del juego puede empezar a dibujar - primero el fondo, y luego llamando un método dibujar de cada sprite, delegando así a cada objeto la tarea de dibujarse a sí mismo. Esto va en línea con la filosofía de la POO en que no decimos "Oye función dibujar, muestra a esta reina!", sino que preferimos decir: "Oye, reina, dibújate a tí misma!"
Comenzaremos con un objeto simple, sin movimiento ni animación: sólo vamos a implementar el andamiaje necesario para ver cómo encajan todas las piezas:
class SpriteReina: def __init__(self, img, posicion_final): """ Crear e inicializar una reina para la posicino_final dada en el tablero """ self.imagen = img self.posicion_final = posicion_final self.posicion = posicion_final def update(self): return # No hacemos nada por el momento. def dibujar(self, superficie): superficie.blit(self.imagen, self.posicion)
Le hemos dado al sprite 3 atributos: una imagen para dibujar, una posición final y su posición actual.
- Si vamos a mover el sprite por el tablero, la posición actual irá cambiando y no tiene por qué coincidir con la posición final, que es a donde queremos que la reina termine.
En este código por ahora no hemos hecho nada en el método update y nuestro método dibujar (que probablemente seguirá siendo así de simple en el futuro) simplemente se dibuja a sí mismo en la posición actual en la superficie que le es pasada como parámetro.
Con su definición de clase pronta, podemos ahora instanciar nuestras N reinas, ponerlas en una lista de sprites, y hacer que el loop del juego llame a los métodos update y dibujar en cada frame. Los nuevos trozos de código junto con el loop del juego modificados se ven así:
(...) #La parte inicial del código no cambia lista_sprites = [] # Mantenemos una lista con todos los sprites del juego #Creamos un objeto sprite para cada reina y lo ponemos en la lista. for (columna, fila) in enumerate(tablero): una_reina = SpriteReina(pelota, (columna*tamaño_cuadro + offset_pelota, fila*tamaño_cuadro + offset_pelota)) lista_sprites.append(una_reina) while True: # Sondeo por un evento del teclado, mouse, joystick, etc. ev = pygame.event.poll() if ev.type == pygame.QUIT: # Si el evento es que se hizo click en el botón cerrar ventana break # ... abandonar el loop principal del juego #Pedirle a cada sprite que se actualice for sprite in lista_sprites: sprite.update() # Dibujar un tablero de ajedrez vacío como fondo (...) #Esta parte del código no cambia # Pedirle a cada sprite que se dibuje a sí mismo for sprite in lista_sprites: sprite.dibujar(superficie) # Ahora que el dibujo está pronto, le decimos a pygame que lo muestre! pygame.display.flip() (...) #La parte final del código no cambia
Esto funciona igual que nuestra versión anterior, pero hemos dejado ya todo pronto para agregar modificaciones con facilidad en los próximos pasos.
Comencemos con una reina que cae. En todo momento tendrá una velocidad en cierta dirección (en este caso será la dirección y porque se trata de una caída vertical). Así que en el método update del objeto queremos cambiar su posición vertical de acuerdo a su velocidad. Si nuestro tablero de N reinas está flotando en el espacio, la velocidad se mantendría constante, pero como está en la Tierra, tenemos gravedad, la cual cambiará la velocidad en cada intervalo, así que queremos una pelota que cae cada vez más rápido. La gravedad es la misma para todas las reinas, así que no la haremos parte de cada objeto, sino que la mantendremos como una variable a nivel del módulo. Y haremos otro cambio: comenzaremos con cada reina en lo alto del tablero, de forma que pueda caer hacia su posición final. Con estos cambios, obtenemos ahora lo siguiente:
class SpriteReina: def __init__(self, img, posicion_final): """ Crear e inicializar una reina para la posicino_final dada en el tablero """ self.imagen = img self.posicion_final = posicion_final (x, y) = posicion_final self.posicion = (x, 0) # Comenzamos en lo alto, justo sobre posicion_final self.velocidad_y = 0 # La velocidad inicial de caída es 0 def update(self): self.velocidad_y += gravedad; # La gravedad incrementa la velocidad (x, y) = self.posicion nueva_posicion_y = y + self.velocidad_y # La velocidad mueve la bola self.posicion = (x, nueva_posicion_y) # a su nueva posicion def dibujar(self, superficie): # Igual que antes (nada cambió) superficie.blit(self.imagen, self.posicion)
Tenemos ahora un nuevo tablero en que cada reina comienza en el tope de su columna y cae cada vez más rápido, hasta que alcanza el piso del tablero y desaparece para siempre.
- Un buen comienzo: ¡Tenemos movimiento!
El próximo paso es hacer que la bola rebote cuando alcance su propia posicion_final.
- Es muy fácil hacer que algo rebote - basta con invertir el signo de su velocidad, y se moverá con la misma rapidez en dirección contraria.
- Por supuesto, a medida que suba en el tablero va a ser enlentecida por la gravedad (la cual siempre empuja todas las cosas hacia abajo!)
- Y la verás rebotar hasta el lugar del que salió, donde alcanzará velocidad cero, y comenzar a caer nuevamente. Así que tendremos bolas que rebotan eternamente.
Una forma realista de simular el rebote de un objeto es hacer que pierda cierta cantidad de energía en cada golpe.
- Así que en vez de simplemente invertir el signo de la velocidad, multiplicaremos por un factor fraccional negativo menor que 1, por ejemplo -0.65
- Esto significa que la bola sólo retiene un 65% de su energía en cada rebota así que terminará por detenerse, como en la vida real, y quedará apoyada en su "piso".
Sólo necesitamos un par de cambios en el método update, que ahora queda así:
def update(self): self.velocidad_y += gravedad; # La gravedad incrementa la velocidad (x, y) = self.posicion nueva_posicion_y = y + self.velocidad_y # La velocidad mueve la bola (x_final, y_final) = self.posicion_final distancia_faltante = y_final - nueva_posicion_y #Qué tan lejos estamos del "piso" if (distancia_faltante < 0): # Ya estamos por debajo del "piso"? self.velocidad_y = -0.65 * self.velocidad_y # Rebotar nueva_posicion_y = y_final + distancia_faltante # Moverse arriba del "piso" self.posicion = (x, nueva_posicion_y) # Establecemos nuestra nueva posición
Pruébalo por ti mismo y juega con el parámetro gravedad para ver caídas más o menos rápidas de las reinas.
17.5) Eventos
El único tipo de evento que hemos manejado hasta ahora ha sido el evento QUIT. Pero también podríamos detectar eventos keydown y keyup (respectivamente, evento apretar y soltar tecla), movimiento del mouse, botón del mouse up o down, etc. Puedes consultar la documentación de PyGame, siguiendo el link a Event.
Cuando tu programa sondea por eventos y recibe de PyGame un objeto evento, el tipo del evento determinará qué información secundaria está disponible.
- Cada objeto evento viene con un diccionario (tema que veremos a su debido tiempo en este curso)
- El diccionario mantiene ciertas claves y valores que tienen sentido para ese tipo de evento.
Por ejemplo:
- Si el tipo de evento es MOUSEMOTION, el diccionario asociado al evento traerá información sobre la posición del mouse y el estado de sus botones.
- De modo similar, si el evento es KEYDOWN, el diccionario nos dirá qué tecla se presionó, y si alguna tecla modificadora (SHIFT, CONTROL, ALT, etc.) también está siendo presionada.
- También se obtienen eventos cuando la ventana del juego se vuelve activa (obtiene el foco) o pierde el foco.
El objeto evento cuyo tipo es NOEVENT se retorna si no hay eventos esperando. Los eventos pueden imprimirse, lo que permite experimentar y jugar con ellos. Por lo tanto, si ponemos las siguientes líneas de código en el loop del juego, justo después de haber hecho el sondeo de eventos, tendremos mucha información disponible sobre eventos:
if ev.type != pygame.NOEVENT: # Sólo imprimir si hay algo interesante! print(ev)
Si ahora presionas la barra espaciadora mientras ves el tablero del juego, verás el reporte del evento correspondiente. Haz 3 clicks en tu mouse. Mueve el mouse en la ventana. (Esto causará una gran cascada de eventos, así que puede que necesites también filtrar estos para que no se impriman). Obtendrás un output que se verá así:
<Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 0})> <Event(4352-AudioDeviceAdded {'which': 0, 'iscapture': 1})> <Event(32768-ActiveEvent {})> <Event(32774-WindowShown {'window': None})> <Event(32768-ActiveEvent {'gain': 1, 'state': 1})> <Event(32785-WindowFocusGained {'window': None})> <Event(770-TextEditing {'text': '', 'start': 0, 'length': 0, 'window': None})> <Event(32768-ActiveEvent {'gain': 1, 'state': 0})> <Event(32783-WindowEnter {'window': None})> <Event(1024-MouseMotion {'pos': (291, 81), 'rel': (0, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(32770-VideoExpose {})> <Event(32776-WindowExposed {'window': None})> <Event(768-KeyDown {'unicode': ' ', 'key': 32, 'mod': 0, 'scancode': 44, 'window': None})> <Event(771-TextInput {'text': ' ', 'window': None})> <Event(769-KeyUp {'unicode': ' ', 'key': 32, 'mod': 0, 'scancode': 44, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 82), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 83), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 84), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 86), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 87), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 88), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 89), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 90), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 91), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 92), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 92), 'rel': (-1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 93), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 94), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 94), 'rel': (-1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1025-MouseButtonDown {'pos': (289, 94), 'button': 1, 'touch': False, 'window': None})> <Event(1026-MouseButtonUp {'pos': (289, 94), 'button': 1, 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 95), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 98), 'rel': (0, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 99), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 101), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 102), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 103), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 104), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (289, 105), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 106), 'rel': (1, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 107), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 108), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 109), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (290, 110), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 110), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 111), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (292, 111), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1025-MouseButtonDown {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})> <Event(1026-MouseButtonUp {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})> <Event(1025-MouseButtonDown {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})> <Event(1026-MouseButtonUp {'pos': (292, 111), 'button': 1, 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (292, 113), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (293, 116), 'rel': (1, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (293, 118), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (293, 119), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (293, 121), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (293, 124), 'rel': (0, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (293, 128), 'rel': (0, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (291, 135), 'rel': (-2, 7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (286, 151), 'rel': (-5, 16), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (283, 165), 'rel': (-3, 14), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (277, 177), 'rel': (-6, 12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (276, 180), 'rel': (-1, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (272, 184), 'rel': (-4, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (262, 193), 'rel': (-10, 9), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (245, 203), 'rel': (-17, 10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (220, 213), 'rel': (-25, 10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (193, 217), 'rel': (-27, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (138, 219), 'rel': (-55, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (124, 214), 'rel': (-14, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (113, 209), 'rel': (-11, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (105, 202), 'rel': (-8, -7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (97, 194), 'rel': (-8, -8), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (88, 181), 'rel': (-9, -13), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (84, 168), 'rel': (-4, -13), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (79, 154), 'rel': (-5, -14), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (78, 148), 'rel': (-1, -6), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (78, 143), 'rel': (0, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (78, 141), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (78, 139), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (78, 138), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (79, 136), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (80, 134), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (95, 124), 'rel': (15, -10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (132, 103), 'rel': (37, -21), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (179, 88), 'rel': (47, -15), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (224, 76), 'rel': (45, -12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (272, 71), 'rel': (48, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (312, 71), 'rel': (40, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (359, 74), 'rel': (47, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (394, 82), 'rel': (35, 8), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (425, 94), 'rel': (31, 12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (455, 111), 'rel': (30, 17), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (455, 113), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (456, 115), 'rel': (1, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (457, 116), 'rel': (1, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (457, 118), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (457, 119), 'rel': (0, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (457, 121), 'rel': (0, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (457, 124), 'rel': (0, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (456, 128), 'rel': (-1, 4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (450, 143), 'rel': (-6, 15), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (442, 160), 'rel': (-8, 17), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (434, 176), 'rel': (-8, 16), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (428, 187), 'rel': (-6, 11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (421, 197), 'rel': (-7, 10), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (419, 200), 'rel': (-2, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (415, 203), 'rel': (-4, 3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (412, 205), 'rel': (-3, 2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (408, 206), 'rel': (-4, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (404, 207), 'rel': (-4, 1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (391, 207), 'rel': (-13, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (379, 205), 'rel': (-12, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (366, 200), 'rel': (-13, -5), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (355, 194), 'rel': (-11, -6), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (345, 185), 'rel': (-10, -9), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (335, 174), 'rel': (-10, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (327, 163), 'rel': (-8, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (322, 156), 'rel': (-5, -7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (320, 153), 'rel': (-2, -3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (319, 149), 'rel': (-1, -4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (318, 147), 'rel': (-1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 145), 'rel': (-1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 144), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 143), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 142), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 141), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 139), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (317, 138), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (324, 131), 'rel': (7, -7), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (337, 119), 'rel': (13, -12), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (354, 108), 'rel': (17, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (364, 100), 'rel': (10, -8), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (366, 99), 'rel': (2, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (367, 98), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (369, 97), 'rel': (2, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (370, 97), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (371, 97), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (372, 96), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (373, 96), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (374, 96), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (375, 95), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (377, 95), 'rel': (2, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (379, 95), 'rel': (2, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (381, 95), 'rel': (2, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (382, 95), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (383, 95), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (388, 84), 'rel': (5, -11), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (396, 67), 'rel': (8, -17), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (402, 52), 'rel': (6, -15), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (404, 49), 'rel': (2, -3), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (404, 47), 'rel': (0, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (405, 46), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (405, 45), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (406, 44), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (406, 43), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (406, 42), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (407, 42), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (407, 41), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (408, 40), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (409, 39), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (410, 38), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (410, 37), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (411, 36), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (411, 35), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (412, 35), 'rel': (1, 0), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (412, 34), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (413, 33), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (414, 32), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (414, 31), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (415, 30), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (416, 29), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (417, 27), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (418, 26), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (420, 25), 'rel': (2, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (421, 24), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (422, 22), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (423, 21), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (424, 20), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (425, 19), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (426, 18), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (429, 14), 'rel': (3, -4), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (430, 13), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (430, 12), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (431, 11), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (431, 10), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (432, 9), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (433, 8), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (433, 7), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (434, 6), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (435, 4), 'rel': (1, -2), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (436, 3), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (437, 2), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (437, 1), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (438, 0), 'rel': (1, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(1024-MouseMotion {'pos': (438, 0), 'rel': (0, -1), 'buttons': (0, 0, 0), 'touch': False, 'window': None})> <Event(32768-ActiveEvent {'gain': 0, 'state': 0})> <Event(32784-WindowLeave {'window': None})> <Event(32787-WindowClose {'window': None})>
Así que hagamos ahora estos cambios a nuestro código en la parte del código que responde a los eventos:
while True: # Sondeo por un evento del teclado, mouse, joystick, etc. ev = pygame.event.poll() if ev.type == pygame.QUIT: # Si el evento es que se hizo click en el botón cerrar ventana break # ... abandonar el loop principal del juego if ev.type == pygame.KEYDOWN: key = ev.dict['key'] if key == 27: # Si es la tecla Escape ... break # abandonar el juego. if key == ord('r'): colores[0] = (255, 0, 0) # Cambiar a rojo + negro. elif key == ord('g'): colores[0] = (0, 255, 0) # Cambiar a verde (green) + negro. elif key == ord('b'): colores[0] = (0, 0, 255) # Cambiar a azul (blue) + negro. if ev.type == pygame.MOUSEBUTTONDOWN: # Se presionó el botón del mouse? pos_del_click = ev.dict['pos'] # Obtener las coordenadas. print(pos_del_click) # Imprimirlas.
Las líneas centrales muestran un manejo típico del evento KEYDOWN - si una tecla fue presionada, nos fijamos qué tecla es, y ejecutamos cierta acción.
- Tenemos ahora otra forma de abandonar el programa: presionando la tecla ESC.
- También utilizamos algunas teclas para cambiar el color de los cuadros claros del tablero.
Las líneas que responden al evento MOUSEBUTTONDOWN no hacen casi nada: se limitan a imprimir las coordenadas del mouse en el momento del click.
Como un ejercicio final de esta sección, escribiremos una mejor versión de nuestro manejador de los clicks del mouse.
- Lo que haremos será averiguar si el usuario ha hecho click en uno de los cuadros del tablero.
- Si hay un sprite bajo el mouse cuando ocurre el click, le enviaremos el click al sprite y dejaremos que él responda de alguna manera.
Comenzaremos con código que encuentra cuál es el sprite que se encuentra en la posición cliqueada (o si no hay ninguno). Agregamos el método contiene_punto a la clase SpriteReina, que devuelve True si el punto está dentro del rectángulo del sprite:
def contiene_punto(self, pt): """ Retorna True si mi rectángulo de sprite contiene al punto pt """ (mi_x, mi_y) = self.posicion mi_ancho = self.imagen.get_width() mi_alto = self.imagen.get_height() (x, y) = pt return ( x >= mi_x and x < mi_x + mi_ancho and y >= mi_y and y < mi_y + mi_alto)
Ahora en el loop del juego, una vez que hemos visto el evento del mouse, determinamos qué reina, si es que hay alguna, debería ser la que responda al evento:
if ev.type == pygame.MOUSEBUTTONDOWN: pos_del_click = ev.dict['pos'] for sprite in lista_sprites: if sprite.contiene_punto(pos_del_click): sprite.manejar_click() break
Para terminar, debemos escribir un método manejar_click en la clase SpriteReina. Cuando se cliquea un sprite, agregaremos un poco de velocidad hacia arriba, es decir, pateamos la bola hacia arriba.
def manejar_click(self): self.velocidad_y += -0.3 # Patearla para arriba
Con estos cambios tenemos un juego que se puede jugar! Prueba a ver si puedes mantener todas las pelotas en el aire, no dejando que ninguna toque el suelo!
17.6) Una ola de animaciones
Muchos juegos tienen sprites animados: se agachan, saltan o disparan. ¿Cómo lo hacen?
Considera esta secuencia de 10 imágenes. Si las mostramos en rápida sucesión, Duke nos saludará. (Duke es un amistoso personaje del reino de Javalandia).
Una imagen compuesta que contiene pequeños parches (patches) a ser utilizados como parte de una animación se llama sprite sheet (hoja de sprites).
- Descarga este sprite haciendo click derecho en tu navegador y guárdalo en tu carpeta de trabajo con el nombre duke_spritesheet.png.
El sprite sheet fue preparado cuidadosamente: cada uno de los 10 parches está espaciado a 50 píxeles de su vecino. Por lo tanto, si quisiéramos dibujar el parche número 4 (comenzando a contar desde 0), dibujaríamos el rectángulo que comienza en la posición x = 200 y tiene 50 píxeles de ancho, dentro del sprite sheet.
- En la imagen siguiente mostramos resaltado el parche correspondiente a la posición número 4, del que estamos hablando:
El método blit
que hemos venido usando - para copiar píxeles desde una superficie a otra - puede copiar un sub-rectángulo de la superficie fuente. Por lo tanto, la gran idea aquí es que cada vez que vayamos a dibujar a Duke, no vamos a hacer blit del sprite sheet completo, sino que pasaremos un argumento extra, correspondiente a un rectángulo, para indicar cuál es la porción del spread sheet que queremos utilizar.
Agregaremos en esta sección más código al que ya tenemos para dibujar las reinas. Lo que queremos es poner algunas instancias de Duke en algunas partes del tablero. Si el usuario hace click en una de ellas, responderemos saludando, un ciclo completo de su animación.
Pero antes de hacerlo, necesitamos otro cambio. Hasta ahora, nuestro loop del juego ha estado corriendo con un frame rate altísimo, bastante fluctuante e impredecible. Por lo cual elegimos algunos números mágicos para la gravedad y los rebotes y patadas a la pelota, más bien por ensayo y error. Si vamos a animar a más sprites, debemos adaptar el loop de nuestro juego para que opere a una frecuencia de frames conocida y estable. Esto nos permitirá planear de mejor modo nuestras animaciones.
PyGame nos da las herramientas para hacer esto en tan sólo dos líneas de código. En la sección setup del juego, instanciamos un nuevo objeto Clock.
mi_reloj = pygame.time.Clock()
Y justo al final del loop del juego, llamamos a un método de este objeto que limita el frame rate a lo que nosotros especifiquemos.
- Vamos pues a planear nuestro juego y animaciones para funcionar a 60 frames por segundo, agregando la siguiente línea al final de nuestro loop del juego:
mi_reloj.tick(60) # Dejar pasar el tiempo para que el frame rate se reduzca a 60 fps
Verás que tienes que ir atrás y ajustar los números para gravedad y pateado de la pelota, cosa de que los movimientos se vean bien con este frame rate que es mucho más lento.
- Valores recomendados: 0.05 para gravedad y -3 para la patada hacia arriba.
Cuando planeamos una animación para que sólo funcione bien a cierta frecuencia de frames, decimos que hemos horneado la animación.
- En este caso estamos horneando nuestras animaciones a 60 frames por segundo.
Para seguir con la política que ya aplicamos para el dibujo y movimientos de las reinas, queremos crear una clase SpriteDuke que tenga los mismos métodos que la clase SpriteReina. Entonces podremos agregar una o más instancias de Duke a nuestra lista_sprites, y nuestro loop del juego llamará entonces a métodos de la instancia de Duke. Comencemos por un esbozo del esqueleto de la nueva clase:
class SpriteDuke: def __init__(self, img, posicion_final): self.imagen = img self.posicion = posicion_final def update(self): return def dibujar(self, superficie): return def manejar_click(self): return def contiene_punto(self, pt): # Usar el código de ReinaSprite aquí (es el mismo) return
Los únicos cambios que tenemos que hacer a la implementación del juego están todos en la sección setup. Vamos a leer el sprite sheet de Duke e instanciar un par de instancias de la clase SpriteDuke, en las posiciones en que lo queremos ubicar en el tablero. Así que antes de entrar en el loop del juego, agregamos este código:
# Cargar el sprite sheet de Duke duke_sprite_sheet = pygame.image.load("duke_spritesheet.png") # Instanciar dos instancias de Duke, y ponerlas en el tablero duke1 = SpriteDuke(duke_sprite_sheet,(tamaño_cuadro*2, 0)) duke2 = SpriteDuke(duke_sprite_sheet,(tamaño_cuadro*5, tamaño_cuadro)) # Y luego agregarlos a la lista de sprites que es manejada por el loop principal del juego lista_sprites.append(duke1) lista_sprites.append(duke2)
Ahora el loop del game va a testear si cada instancia fue cliqueada, lo cual llamará al manejador de clicks para cada instancia. También va a llamar a update y draw para todas las instancias. Todos los cambios que nos quedan por hacer han de realizarse dentro de la clase SpriteDuke.
Comencemos por dibujar uno de los parches. Introduciremos un nuevo atributo, parche_actual, en la clase. Guarda un valor entre 0 y 9, y determina cuál parche hay que dibujar.
- Por lo tanto, el trabajo del método dibujar será computar el sub-rectángulo del parche que hay que dibujar, y hacer blit sólo de esa porción del sprite sheet en la superficie.
def dibujar(self, superficie): patch_rectangulo = (self.parche_actual * 50, 0, 50, self.imagen.get_height()) superficie.blit(self.imagen, self.posicion, patch_rectangulo) def __init__(self, img, posicion_final): self.imagen = img self.posicion = posicion_final self.parche_actual = 0
- Después de este cambio ya vemos a Duke en el tablero. Pero falta hacer que reaccione a nuestros clicks.
Ahora comencemos a implementar la animación en sí. Necesitamos modificar el método update de SpriteDuke para que si estamos en plena animación, se modifique el valor de parche_actual con al frecuencia adecuada, y se decida correctamente cuándo es tiempo de volver a la posición de descanso y detener la animación. Una cuestión importante es que el frame rate del juego no tiene por qué ser el mismo que el de la animación (animation rate), que es la frecuencia con la que vamos cambiando entre los parches de animación de Duke. Planearemos el ciclo de animación de Duke para que dure 1 segundo, por lo cual queremos poder ejecutar 10 cambios de parche cada 60 llamadas a update (porque, recordemos, el frame rate del juego es de 60 frames por segundo). Así es como se va haciendo el horneado de la animación! Así que necesitamos un contador de animación en la clase, que será cero cuando no estemos animando, y que se irá incrementando en cada llamada de update hasta llegar a 59 (durante la animación), volviendo entonces a 0. Podemos entonces dividir el contador de la animación por 6 (división entera), para deducir el valor de parche_actual y dibujar el parche correcto.
def update(self): if self.contador_frame_animacion > 0: self.contador_frame_animacion = (self.contador_frame_animacion + 1 ) % 60 self.parche_actual = self.contador_frame_animacion // 6 def __init__(self, img, posicion_final): self.imagen = img self.posicion = posicion_final self.parche_actual = 0 self.contador_frame_animacion = 0
Observar que si contador_frame_animacion es cero, es decir si Duke está quieto, nada ocurre en el update. Pero si inicializamos la cuenta del contador, contará hasta 59 antes de regresar a cero.
- Observar también que como contador_frame_animacion sólo puede tener valores entre 0 y 59, el parche_actual siempre los tendrá entre 0 y 9, como necesitamos!
Ahora, ¿cómo y cuándo iniciamos la animación? Lo hacemos así, como respuesta a un click del mouse:
def manejar_click(self): if self.contador_frame_animacion == 0: self.contador_frame_animacion = 5
Dos observaciones importantes:
- Sólo comenzamos la animación si Duke está descansando. Los clicks en Duke mientras ya está saludando serán ignorados.
- Cuando comenzamos la animación, ponemos el contador en 5 - lo cual significa que en el siguiente llamado a update se convertirá en un 6 y la imagen cambiará. Si hubiéramos seteado la variable a 1, habría que haber esperado a 5 llamados a update antes de que la animación comenzara - un pequeño lag, pero suficiente para dar una mala impresión de lentitud en la respuesta al jugador.
No olvidar inicializar los dos nuevos atributos parche_actual y contador_frame_animacion en el método __init__ (como ya hemos señalado antes). Así queda el código final de la clase:
class SpriteDuke: def __init__(self, img, posicion_final): self.imagen = img self.posicion = posicion_final self.parche_actual = 0 self.contador_frame_animacion = 0 def update(self): if self.contador_frame_animacion > 0: self.contador_frame_animacion = (self.contador_frame_animacion + 1 ) % 60 self.parche_actual = self.contador_frame_animacion // 6 def dibujar(self, superficie): patch_rectangulo = (self.parche_actual * 50, 0, 50, self.imagen.get_height()) superficie.blit(self.imagen, self.posicion, patch_rectangulo) def manejar_click(self): if self.contador_frame_animacion == 0: self.contador_frame_animacion = 5 def contiene_punto(self, pt): """ Retorna True si mi rectángulo de sprite contiene al punto pt """ (mi_x, mi_y) = self.posicion mi_ancho = self.imagen.get_width() mi_alto = self.imagen.get_height() (x, y) = pt return ( x >= mi_x and x < mi_x + mi_ancho and y >= mi_y and y < mi_y + mi_alto)
Ahora vemos dos instancias de Duke en el tablero, y si hacemos click sobre alguna de ellas, nos saluda.
17.7) Aliens - un caso de estudio
Encontrar los juegos de ejemplo que vienen en el paquete PyGame y juega al juego Aliens. Luego lee el código en un editor o entorno Python en que se muestren las líneas de código.
- En un sistema Windows, el link puede ser algo así: C:\Python3\Lib\site-packages\pygame\examples
- En Windows 10, se puede encontrar en un link así: C:\Users\[nombre-usuario]\AppData\Roaming\Python\Python310\site-packages\pygame\examples
Hace unas cuantas cosas más avanzadas que las que hicimos nosotros, y se basa en el framework de PyGame para la mayor parte de su lógica. Aquí hay algunas cuestiones que puedes observar: (hacerlo con tiempo)
- El frame rate está deliberadamente restringido cerca del final del loop del juego, en la línea 311 (391). Si cambiamos ese número podemos hacer al juego más lento, o injugablemente rápido!
- Hay distintos tipos de sprites. Explosiones, disparos, bombas, aliens y jugador. Algunos tienen más de una imagen - intercambiando las imágenes se consigue la animación de los sprites, por ejemplo en la línea 112 se implementa el cambio de las luces de las naves Alien.
- Diferentes tipos de objetos se referencian en diferentes grupos de sprites, y PyGame ayuda a mantener estos grupos. Esto permite que el programa cheque colisiones entre, por ejemplo, la lista de disparos del jugador y la lista de naves que están atacando. PyGame hace una buena parte del trabajo por nosotros.
- A diferencia de lo que ocurría en nuestro juego, los objetos en este juego tienen un tiempo de vida limitado, y deben desaparecer.
- Por ejemplo, si disparamos, un objeto Shot es creado - y si alcanza el top de la ventana sin haber explotado contra nada, debe ser eliminado del juego. Las líneas 141-142 se ocupan de esto.
- De modo similar, cuando una bomba que cae se acerca al suelo (línea 156) instancia un nuevo sprite de Explosión, y la bomba se mata a sí misma.
- Hay timings aleatorios que ayudan a hacer el juego más entretenido - cuándo aparecerá un nuevo Alien, cuándo un Alien disparará nuevamente, etc.
- El juego también emite sonidos: un poco relajante loop sonoro como música de fondo, más sonidos para los disparos y explosiones.
17.8) Reflexiones
La programación orientada a objetos es una herramienta muy buena para la organización del software.
- En los ejemplos de este capítulo, comenzamos a usar (y es de esperar que a apreciar) estas ventajas.
- Tuvimos N reinas que mantenía cada una su propio estado, cayendo a su propia posición final, rebotando, siendo pateada, etc., independientemente de las demás.
- Podíamos haber implementado todo esto sin el poder organizacional de los objetos - por ejemplo manteniendo una lista de velocidades para las reinas, listas de posiciones finales, etc.
- Pero nuestro código hubiera sido mucho más complejo, más extraño y el resultado final hubiera sido más pobre que el que implementamos.
17.9) Glosario
- frecuencia de animación, frecuencia de frames, blit, loop del juego, pixel, sondeo de eventos, sprite, superficie
- animación horneada (una animación diseñada para ser mostrada a cierta frecuencia de frames - reduce la cantidad de computaciones necesarias durante la ejecución - muy utilizada por juegos comerciales)
17.10) Ejercicios
1) Diviértete con Python y con PyGame.
2) Intencionalmente dejamos un bug en el código que animaba a Duke. Si haces click en un cuadro a la derecha de Duke, saludará también. ¿Por qué? Corrígelo con un ajuste que sólo requiera modificar una línea de código.
- Respuesta: No era cierto que se podía utilizar el mismo código para la función contiene_punto de la clase SpriteReina en la clase SpriteDuke.
- La razón: en SpriteDuke, el ancho de self.imagen es el del sprite completo con las 10 figuras de Duke - al no dividir ese ancho por 10, la zona del click de Duke se extiende 10 veces más de lo que esperábamos.
- Para corregirlo alcanza con modificar una línea de código en la función contiene_punto de la clase SpriteDuke, así:
mi_ancho = self.imagen.get_width()/10
3) Usa tu buscador preferido para encontrar una librería de imágenes para "sprite sheet playing cards". Crea una lista [0..51] para representar las 52 cartas del mazo de cartas inglesas. Mezcla las cartas, toma las 5 primeras como en una partida de poker. Muestra en pantalla tu mano de poker. (hacerlo)
- Variante criolla: buscar "sprite sheet cartas españolas", crear una lista [0..47] para esas barajas, o bien [0..39] si se eliminan los 8s y 9s. Mezclar las cartas, tomar 3 y mostrar en pantalla una mano de truco.
4) Como el juego Aliens se desarrolla en el espacio exterior, no hay gravedad: los disparos vuelan para siempre y las bombas no se aceleran al caer. Agrega un poco de gravedad al juego. Decide al hacerlo si permitirás que tus propios tiros podrán caer sobre tu cabeza y matarte en tal caso. (hacerlo - es recomendable crear una copia de aliens.py de PyGame y trabajar sobre esa copia para no alterar el original)
5) Esos apestosos Aliens se cruzan entre sí sin dañarse! Modifica el juego para que choquen y se destruyan mutuamente en una grandiosa explosión. (hacerlo)