Jorge Martinez

Aerospace Engineer and Senior Software Developer


Space Invaders con Python y Pygame

Jorge Martínez Garrido

January 31, 2022

python pygame


Vamos a diseñar el popular juego conocido como “Space Invaders” utilizando para ello la librería Pygame.

Space Invaders logo

No vamos a complicar mucho el juego: bastará con crear una matriz de aliens con forma cuadrada que poco a poco se vayan acercando al límite inferior de la pantalla. En este mismo lugar se encuentra el tanque, modelado como un rectángulo sencillo. El tanque puede disparar misiles sencillos, también modelados como rectángulos.

Las reglas del juego son sencillas:

  1. Si destruyes todos los aliens habrás ganado la partida.
  2. Si un alien toca la tierra habrás perdido.

Atacando el problema

En el juego podemos identificar los siguientes objectos:

A continuación se muestra un dibujo que representa todos los elementos anteriores junto con las dimensiones de la pantalla:

Space invaders drawing

Para simplificar el código vamos a utilizar toda la potencia de la programación orientada a objectos que Python nos brinda. Así pues, vamos a crear un total de cinco archivos:

Creando la clase Alien

Vamos a modelar los aliens como rectángulos. Para poder crear un alien necesitamos conocer:

A la clase alien también le podemos añadir un método llamado dibujar(self ,ventana). Cuando llamemos a dicho método, el alien será dibujado en la pantalla. Recuerda: en Pygame hay que crear primero el objecto geométrico (rectángulo, polígono, círuclo…) y luego llamar a la función correspondiente para poder dibujarlo en la pantalla.

Por otro lado, la clase también tendrá otro método para comprobar si el alien ha llegado a la Tierra. Para ello, deberemos comprobar que la posición vertical del borde inferior del cuadrado que representa al alien no supera el límite inferior de la pantalla.

Conocido todo lo anterior, podemos escribir la siguiente clase dentro del módulo alien.py:

# %load src/alien.py
# +
import pygame

class Alien:
    """Una clase para modelar los aliens de Space Invaders."""
    
    def __init__(self, posicion, tamaño, color=(255, 255, 0), velocidad=0.01):
        """
        Crea un alien conocidos su posición, tamaño y color.
        
        Parameters
        ----------
        posicion: tuple
            Coordenadas x e y para la posición del alien.
        tamaño: tuple
            Ancho y largo para la forma del alien.
        color: tuple
            Una tupla con los valores RGB para el color del alien.
        velocidad: float
            Número de pixeles que avanza el alien en cada refresco del juego.
        """
        # Almacenamos los valores proporcionados
        self.x_pos, self.y_pos = posicion
        self.ancho, self.alto = tamaño
        self.color = color
        self.velocidad = velocidad
        
        
    def dibujar(self, ventana):
        """
        Permite dibujar el alien en la ventana o superfície de Pygame deseada.
        
        Parameters
        ----------
        ventana: ~pygame.Surface
            Ventana o superfícia donde se mostrará el alien.
        """
        # Dibujamos el alien en la ventana deseada
        self.forma = pygame.Rect(self.x_pos, self.y_pos, self.ancho, self.alto)
        pygame.draw.rect(ventana, self.color, self.forma)
        
        # Actualizamos su posición, es decir, forzamos que baje hacia el borde inferior de la pantalla
        # Esto simula la invasión.
        self.y_pos += self.velocidad
        
    def ha_llegado_al_suelo(self, alto_ventana):
        """
        Devuelve verdadero o falso en caso de que el alien haya llegado a la Tierra.
        
        Parameters
        ----------
        alto_ventana: int
            Alto de la ventana en pixeles.
        """
        
        # El alien llega a la Tierra cuando la posición de su borde inferior es igual al límite
        # inferior de la pantalla.
        y_pos_borde_inferior_alien = self.y_pos + self.alto
        
        # Tamaño vertical de la pantalla
        return True if y_pos_borde_inferior_alien >= alto_ventana else False
    
    def ha_sido_abatido(self, misil):
        """
        Comprueba si el alien ha sido abatido por el misil.
        
        Parameters
        ----------
        misil: ~Misil
            Instancia de la clase misil.
        """
        return self.forma.collidepoint(misil.x_pos, misil.y_pos)
pygame 2.5.2 (SDL 2.28.2, Python 3.8.10)
Hello from the pygame community. https://www.pygame.org/contribute.html

Creando la clase Tanque

Vamos a seguir la misma idea para generar el tanque. En lugar de actualizar la posición de forma automática como hicimos con los aliens, vamos a añadir en esta clase dos métodos: mover_derecha(self) y mover_izquierda(self). El incremento en píxeles de los dos movimientos anteriores vendrá definido por el parámetro velocidad, el cual se proporciona a la hora de crear la instancia.

Así pues, podemos generar el siguiente código en el módulo tanque.py:

# %load src/tanque.py
# +
import pygame

class Tanque:
    """Una clase para modelar el tanque en Space Invaders."""
    
    def __init__(self, posicion, tamaño, color=(128, 128, 128), velocidad=0.15):
        """
        Crea un tanque conocidos su posición, tamaño, color y velocidad.
        
        Parameters
        ----------
        posicion: tuple
            Coordenadas x e y para la posición del tanque.
        tamaño: tuple
            Ancho y alto del rectángulo para el tanque.
        color: tuple
            Color en RGB para el tanque.
        velocidad: float
            Velocidad horizontal del tanque.
        """
        # Asignamos todos los valores anteriores
        self.x_pos, self.y_pos = posicion
        self.ancho, self.alto = tamaño
        self.color = color
        self.velocidad = velocidad
        
    def dibujar(self, ventana):
        """
        Permite dibujar el tanque en la ventana o superfície de Pygame deseada.
        
        Parameters
        ----------
        ventana: ~pygame.Surface
            Ventana o superfícia donde se mostrará el alien.
        """
        # Generamos el rectángulo que representa el tanque
        self.forma = pygame.Rect(self.x_pos, self.y_pos, self.ancho, self.alto)
        # Dibujamos el alien en la ventana deseada
        pygame.draw.rect(ventana, self.color, self.forma)
        
    def mover_derecha(self, x_lim):
        """
        Actualiza la posición horizontal del tanque moviéndolo a la derecha. Si excede el límite,
        la posición no se actualiza.
        
        Parameters
        ----------
        x_lim: float
            Límite horizontal izquierdo.
        """
        
        # Actualizamos su posición horizontal hacia la derecha
        if not self.x_pos + self.velocidad > x_lim:
            self.x_pos += self.velocidad
            
    def mover_izquierda(self, x_lim):
        """
        Actualiza la posición horizontal del tanque moviéndolo a la izquierda. Si excede el límite,
        la posición no se actualiza.
        
        Parameters
        ----------
        x_lim: float
            Límite horizontal izquierdo.
        """
        
        # Actualizamos su posición horizontal hacia la derecha
        if not self.x_pos - self.velocidad < x_lim:
            self.x_pos -= self.velocidad

Creando la clase Misil

Siguiento la misma línea anteiror, vamos a crear una clase misil. Esta será muy parecidad a la clase Alien, ya que tiene un desplazamiento vertical que se realiza de forma automática. La única diferencia es que un misíl se mueve de abajo hacia arriba en la pantalla y es el objeto con la mayor velocidad de todos.

A continuación se presenta el código fuente para los misiles:

# %load src/misil.py
# +
import pygame

class Misil:
    """Una clase para modelar los misiles de Space Invaders."""
    
    def __init__(self, posicion, tamaño, color=(255, 0, 0), velocidad=0.35):
        """
        Crea un alien conocidos su posición, tamaño y color.
        
        Parameters
        ----------
        posicion: tuple
            Coordenadas x e y para la posición del misil.
        tamaño: float
             Tamaño del radio para la forma del misil.
        color: tuple
            Una tupla con los valores RGB para el color del misil.
        velocidad: float
            Número de pixeles que avanza el misil avanza en cada refresco del juego.
        """
        # Almacenamos los valores proporcionados
        self.x_pos, self.y_pos = posicion
        self.radio = tamaño
        self.color = color
        self.velocidad = velocidad
        
    def dibujar(self, ventana):
        """
        Permite dibujar el alien en la ventana o superfície de Pygame deseada.
        
        Parameters
        ----------
        ventana: ~pygame.Surface
            Ventana o superfícia donde se mostrará el alien.
        """
        # Dibujamos el misil en la ventana deseada. Fíjate que la posición hay que pasarla como
        # una tupla tal que (x_pos, y_pos), pues así lo pide Pygame.
        pygame.draw.circle(ventana, self.color, (self.x_pos, self.y_pos), self.radio)
        
        # Actualizamos su posición para que avance hacia arriba
        self.y_pos -= self.velocidad

Creando la ventana y la lógica del juego

Vamos a comenzar creando una ventana en Pygame que se cierre cuando el jugador así lo indique. Por ejemplo, si el jugador presiona la tecla “ESC” o hace click en botón de cerrar ventana. Los pasos a seguir durante la creación y ejecución de una pantalla son los siguientes:

  1. Inicializamos Pygame para que se encarge de gestionar todo.
  2. Creamos la pantalla con la resoluión y título deseados.
  3. Mostramos la pantalla en un bucle que no termina a menos que el usuario lo indique.
  4. Gestionamos los eventos: clicks, teclas pulsadas…
  5. Una vez cerrada la ventana, cerramos Pygame.

Comenzamos importando pygame en nuestro código:

Función para crear una ventana

Vamos a crear una función para generar una ventana con las dimensiones deseadas y el título que nos apezca. Para ello, simplemente llamamos a las rutinas set_mode y set_caption de Pygame:

def crear_ventana(resolucion, titulo):
    """
    Crea una ventana conocida la resolución y le añade el título deseado.
    
    Parameters
    ----------
    resolucion: tuple
        Valores para el ancho y alto de la pantalla.
    titulo: str
        Texto a mostrar como título de la ventana.
        
    Returns
    -------
    ventana: pygame.Surface
        Devuelve la ventana creada. 
    """
    ventana = pygame.display.set_mode(resolucion)
    pygame.display.set_caption(titulo)
    return ventana

Renderizando la ventana y recogiendo los eventos

Renderizar significa, de forma muy simplificada, “mostrar en pantalla”. Así que vamos a renderizar la ventana junto con un color de fondo. El color elegido será el negro, para poder simular el cielo nocturno. Siguiendo los pasos presentados anteriormente podemos obtener el siguiente código:

import pygame
def main():
    """Función principal del juego."""
    
    # Resolución de la pantalla
    ANCHO, ALTO = 640, 480
    
    # Creamos la ventana y definimos una variable lógica para comprobar si deseamos cerrarla
    resolucion = (ANCHO, ALTO)
    ventana = crear_ventana(resolucion, "Space Invaders en la EPM")
    cerrar_ventana, game_over = False, False
    
    # Creamos todos los aliens formando una escuadra de 3 columnas x 7 aliens en fila
    lista_aliens = [
        Alien((50 * x_alien, 50 * y_alien), (25, 25))
        for y_alien in range(1, 3+1)
        for x_alien in range(1, 11+1)
    ]
    
    # Creamos el tanque, que inicia en la posición central inferior
    tanque = Tanque((ANCHO / 2, 0.95 * ALTO), (40, 10))
    
    # Creamos una lista vacía de misiles
    lista_misiles = []
    
    # Creamos el bucle principal para mostrar la ventana
    while not cerrar_ventana and not game_over:
        
        # Pintamos el fondo de color negro RGB -> (0, 0, 0)
        ventana.fill((0, 0, 0))
        
        # Recogemos los eventos
        for evento in pygame.event.get():
            # Comprobamos si se desea cerrar la ventanta
            if evento.type == pygame.QUIT: cerrar_ventana = True
            # Evaluamos los controles
            if evento.type == pygame.KEYDOWN:                    
                # Generamos un nuevo misil si es necesario
                if evento.key == pygame.K_SPACE: 
                    lista_misiles.append(
                        Misil((tanque.x_pos + tanque.ancho / 2, tanque.y_pos), 1)
                    )
                    
        # Mueve el tanque de forma fluida mientras se presione la tecla correcta
        teclas_presionadas = pygame.key.get_pressed()
        if teclas_presionadas[pygame.K_RIGHT]: tanque.mover_derecha(x_lim=0.95 * ANCHO)
        if teclas_presionadas[pygame.K_LEFT]: tanque.mover_izquierda(x_lim=0.05 * ANCHO)
                
        # Game over si un alien llega al suelo. Comprobamos impactos de misiles
        for alien in lista_aliens:
            # Comprobamos si algún alien ha llegado al suelo
            if alien.ha_llegado_al_suelo(ALTO): 
                game_over = True
                break
            else:
                # Comprobamos que el alien no ha sido abatido por ningún misil
                if len(lista_misiles) > 0:
                    for misil in lista_misiles:
                        if alien.ha_sido_abatido(misil): 
                            # Eliminamos el alien abatido junto con el misil
                            lista_aliens.remove(alien)
                            lista_misiles.remove(misil)
        
        # Dibujamos los alient que hayan sobrevivido        
        for alien in lista_aliens: alien.dibujar(ventana)
            
        # Dibujamos el tanque
        tanque.dibujar(ventana)
        
        # Dibujamos todos los misiles
        for misil in lista_misiles: misil.dibujar(ventana)
                
        # Actualizamos toda la escena
        pygame.display.update()

Finalmente, iniciamos el juego mediante las siguientes líneas de código:

pygame.init()
main()
pygame.quit()

Resultado

Tal y como muestra la animación de abajo, soy bastante malo jugando al Space Invaders! Eso sí, la verdad que el juego está entretenido. Además, con todo lo que hemos aprendido, puedes aplicar la misma lógica por ejemplo para otros juegos clásicos como el Pong Game o el Arkanoid.

Juego

¿Puedes mejorar el código?

Fíjate que un misil sólo se elimina de la lista cuando impacta. ¿Qué pasa si no lo hace? ¡Pues que se queda almacenado dentro de la variable lista_misiles hasta el final.