En el desarrollo de un juego al estilo "Space Invaders" utilizando Pygame, la creación de los enemigos es un paso fundamental. Este segmento se enfoca en establecer las bases para los adversarios alienígenas, comenzando por una sola entidad y progresando hasta la formación de una flota completa que ocupará la parte superior de la pantalla.
Preparación y Planificación de la Flota
Antes de sumergirnos en la codificación, es crucial definir los pasos necesarios para introducir a los eenmigos en el juego:
- Diseñar la clase base para los enemigos, definiendo su apariencia y propiedades iniciales.
- Instanciar una primera unidad enemiga para verificar su correcta visualización y posicionamiento.
- Calcular la distribución óptima de los enemigos en el espacio disponible de la pantalla, tanto horizontal como verticalmente.
- Implementar la lógica para generar una cuadrícula de enemigos que formen la flota inicial.
- Establecer los cimientos para el movimiento coordinado de la flota y la interacción con los proyectiles del jugador.
Mantener el código organizado y modular es esencial. Por ello, se recomienda la creación de funciones auxiliares que gestionen tareas específicas, como la generación individual de un enemigo o el cálculo de su posición dentro de la formación.
Definición de la Entidad Enemiga
Cada enemigo será una instancia de una clase dedicada, similar a cómo se gestiona la nave del jugador. Para simplificar, utilizaremos una imagen bitmap para representar a cada unidad. Asegúrate de guardar la imagen seleccionada (por ejemplo, enemigo.png) en una carpeta de recursos (por ejemplo, assets/).
Clase Invasor
La clase Invasor manejará las propiedades y el comportamiento individual de cada enemigo. A continuación, se presenta su implementación en el archivo invasor.py:
import pygame
from pygame.sprite import Sprite
class Invasor(Sprite):
"""Representa un único invasor en la flota."""
def __init__(self, motor_juego):
"""Inicializa al invasor y establece su posición de inicio."""
super().__init__()
self.superficie_juego = motor_juego.superficie_juego # Obtenemos la superficie de dibujo
# Carga la imagen del invasor y obtiene su rectángulo (hitbox)
self.imagen_invasor = pygame.image.load('assets/enemigo.png')
self.caja_colision = self.imagen_invasor.get_rect()
# Cada invasor comienza cerca de la esquina superior izquierda de la pantalla.
# Establecemos un margen inicial equivalente a su propio tamaño.
self.caja_colision.x = self.caja_colision.width
self.caja_colision.y = self.caja_colision.height
# Almacena la posición horizontal exacta del invasor en punto flotante.
# Esto permitirá movimientos más precisos.
self.posicion_flotante_x = float(self.caja_colision.x)
El código de esta clase comparte similitudes con la clase de la nave del jugador, pero su enfoque principal es la ubicación inicial. Cada Invasor se posiciona con un margen de su propio ancho y alto, facilitando su visualización. Se mantiene un registro preciso de la posición horizontal (posicion_flotante_x) para futuras actualizaciones de movimiento.
Instanciando el Primer Enemigo
Para visualizar nuestro primer enemigo, debemos crear una instancia de la clase Invasor. Esta configuración se integrará en el método __init__() de la clase principal del juego, AlienInvasion (o su equivalente). Dada la complejidad de gestionar una flota, introduciremos un método auxiliar, _establecer_formacion_enemiga().
Primero, asegurémonos de importar la nueva clase Invasor en el archivo principal del juego:
# ...
from proyectil import Proyectil
from invasor import Invasor # Nueva importación
Ahora, modificaremos el método __init__() de la clase principal del juego:
def __init__(self):
# ... inicialización de otras propiedades
self.nave_jugador = Nave(self) # Suponiendo una clase Nave para el jugador
self.proyectiles_activos = pygame.sprite.Group()
self.flota_invasora = pygame.sprite.Group() # Grupo para almacenar la flota
self._establecer_formacion_enemiga() # Llamada al nuevo método
Se ha creado un grupo de Pygame llamado flota_invasora para contener todas las unidades enemigas, y luego se invoca a _establecer_formacion_enemiga(). La implementación inicial de este método es la siguiente:
def _establecer_formacion_enemiga(self):
"""Crea la flota inicial de invasores."""
# Solo crea un único invasor por ahora.
primer_invasor = Invasor(self)
self.flota_invasora.add(primer_invasor)
Finalmente, para que el enemigo sea visible, es necesario invocar el método draw() del grupo self.flota_invasora en la función de actualización de pantalla, _actualizar_pantalla():
def _actualizar_pantalla(self):
# ...
for bala in self.proyectiles_activos.sprites():
bala.dibujar_proyectil()
self.flota_invasora.draw(self.superficie_juego) # Dibuja todos los invasores del grupo
pygame.display.flip()
Al ejecutar el juego, deberías ver un único enemigo posicionado correctamente en la esquina superior izquierda de la pantalla.
Construcción de la Flota Completa
El siguiente paso es expandir este único enemigo a una formación completa. Esto implica calcular cuántos enemigos pueden caber en una fila y cuántas filas se pueden generar, respetadno los márgenes y el tamaño de la pantalla.
Cálculo de la Capacidad Horizontal
Para determinar cuántos enemigos caben en una fila, necesitamos conocer el ancho del enemigo y el ancho total de la pantalla. Se dejarán márgenes a cada lado, equivalentes al ancho de un enemigo. Además, cada enemigo en la fila tendrá un espacio a su derecha igual a su propio ancho.
# Suponiendo que 'configuracion_juego' contiene 'ancho_pantalla'
# Obtener el ancho de un invasor de muestra para los cálculos
invasor_modelo = Invasor(self)
ancho_unidad = invasor_modelo.caja_colision.width
# Calcular el espacio horizontal disponible para los invasores
margen_total_x = 2 * ancho_unidad # Un ancho a cada lado
espacio_efectivo_horizontal = self.configuracion_juego.ancho_pantalla - margen_total_x
# Calcular cuántos invasores caben, considerando su ancho y el espacio entre ellos
# Cada invasor ocupa "dos anchos": su imagen y el espacio a su derecha.
unidades_por_fila = espacio_efectivo_horizontal // (2 * ancho_unidad)
Creación de una Fila de Enemigos
Utilizando los cálculos anteriores, podemos adaptar _establecer_formacion_enemiga() para generar una fila completa:
def _establecer_formacion_enemiga(self):
"""Crea la flota de invasores calculando cuántos caben en cada fila."""
# Creamos un invasor temporal para obtener sus dimensiones. No se añade al grupo.
invasor_modelo = Invasor(self)
ancho_unidad, altura_unidad = invasor_modelo.caja_colision.size
# Cálculos de espacio horizontal
margen_total_x = 2 * ancho_unidad
espacio_efectivo_horizontal = self.configuracion_juego.ancho_pantalla - margen_total_x
unidades_por_fila = espacio_efectivo_horizontal // (2 * ancho_unidad)
# Bucle para crear la primera fila de invasores
for indice_columna in range(unidades_por_fila):
self._generar_unidad_enemiga(indice_columna, 0) # Crear y posicionar un invasor
En este punto, se utiliza el método auxiliar _generar_unidad_enemiga() para manejar la creación y posicionamiento de cada enemigo individual. Este enfoque promueve la claridad y la reutilización del código.
Refactorizando para la Creación Individual
El método _generar_unidad_enemiga() encapsulará la lógica para crear y posicionar un solo invasor. Se añade a la clase principal del juego:
def _generar_unidad_enemiga(self, indice_columna, indice_fila):
"""Crea un invasor y lo posiciona en la fila y columna especificadas."""
nuevo_invasor = Invasor(self)
ancho_unidad, altura_unidad = nuevo_invasor.caja_colision.size
# Calcula la posición horizontal (x)
# El primer invasor tiene un margen de 'ancho_unidad'.
# Cada invasor subsiguiente se mueve '2 * ancho_unidad' a la derecha.
posicion_x = ancho_unidad + (2 * ancho_unidad * indice_columna)
nuevo_invasor.posicion_flotante_x = posicion_x
nuevo_invasor.caja_colision.x = posicion_x
# Calcula la posición vertical (y)
# El primer invasor tiene un margen superior de 'altura_unidad'.
# Cada fila subsiguiente se mueve '2 * altura_unidad' hacia abajo.
posicion_y = altura_unidad + (2 * altura_unidad * indice_fila)
nuevo_invasor.caja_colision.y = posicion_y
self.flota_invasora.add(nuevo_invasor)
Este método ahora acepta tanto el índice de columna como el de fila, lo que permite un posicionamiento preciso de cada unidad en la cuadrícula.
Añadiendo Múltiples Filas de Enemigos
Para completar la flota, necesitamos calcular cuántas filas de enemigos pueden caber verticalmente en la pantalla. Esto requiere considerar la altura del enemigo, la altura de la nave del jugador y un espacio adecuado entre las filas y con el jugador.
def _establecer_formacion_enemiga(self):
"""Crea la flota de invasores, calculando la disposición completa."""
invasor_modelo = Invasor(self)
ancho_unidad, altura_unidad = invasor_modelo.caja_colision.size
# Cálculos de espacio horizontal
margen_total_x = 2 * ancho_unidad
espacio_efectivo_horizontal = self.configuracion_juego.ancho_pantalla - margen_total_x
unidades_por_fila = espacio_efectivo_horizontal // (2 * ancho_unidad)
# Cálculos de espacio vertical
altura_nave_jugador = self.nave_jugador.caja_colision.height
# Se deja espacio superior (altura_unidad), espacio entre filas (2*altura_unidad * num_filas)
# y espacio entre la flota y la nave (2*altura_unidad)
espacio_vertical_disponible = (self.configuracion_juego.alto_pantalla -
(3 * altura_unidad) - altura_nave_jugador)
cantidad_filas = espacio_vertical_disponible // (2 * altura_unidad)
# Bucle anidado para crear la flota completa
for indice_fila in range(cantidad_filas):
for indice_columna in range(unidades_por_fila):
self._generar_unidad_enemiga(indice_columna, indice_fila)
Se introducen dos bucles anidados: el bucle exterior itera sobre el número de filas, y el bucle interior, sobre el número de columnas dentro de cada fila. Cada llamada a _generar_unidad_enemiga() ahora recibe tanto el indice_columna como el indice_fila, permitiendo que el método calcule la posición Y de cada invasor. La posición Y se ajusta para dejar un margen superior y espacio entre cada fila de enemigos.
Al ejecutar el juego con estos cambios, se visualizará una imponente flota de enemigos cubriendo la parte superior de la pantalla, lista para el siguiente desafío del juego.