Desarrollo de Juego Arcade con Pygame: Sistema de Partículas y Controles Táctiles

Para gestionar la selección de portadas en la aplicación, implementamos una interfaz gráfica con Tkinter que permite a los usuarios establecer y previsualizar imágenes de portada.

import tkinter as tk
from PIL import Image, ImageTk

class GestorPortada:
    def __init__(self, ventana_principal, gestor_imagenes, administrador_portadas):
        self.raiz = ventana_principal
        self.gestor_img = gestor_imagenes
        self.admin_portadas = administrador_portadas
        self.crear_interfaz()
    
    def crear_interfaz(self):
        # Botón para establecer portada
        btn_establecer = tk.Button(
            self.raiz,
            text="Establecer como portada",
            command=self.establecer_portada_actual
        )
        btn_establecer.pack(pady=10)
        
        # Botón para previsualizar
        btn_vista_previa = tk.Button(
            self.raiz,
            text="Ver portada actual",
            command=self.mostrar_vista_previa
        )
        btn_vista_previa.pack(pady=10)
    
    def establecer_portada_actual(self):
        imagen_actual = self.gestor_img.obtener_imagen_actual()
        if self.admin_portadas.guardar_portada(imagen_actual):
            tk.messagebox.showinfo("Éxito", "Portada configurada correctamente")
        else:
            tk.messagebox.showerror("Error", "No se pudo establecer la portada")
    
    def mostrar_vista_previa(self):
        ventana_previa = tk.Toplevel(self.raiz)
        ventana_previa.title("Vista previa de portada")
        ventana_previa.geometry("450x350")
        
        ruta_portada = self.admin_portadas.obtener_ruta_portada()
        if ruta_portada:
            img = Image.open(ruta_portada)
            img = img.resize((400, 300), Image.Resampling.LANCZOS)
            imagen_tk = ImageTk.PhotoImage(img)
            
            etiqueta = tk.Label(ventana_previa, image=imagen_tk)
            etiqueta.imagen = imagen_tk
            etiqueta.pack(expand=True, fill=tk.BOTH)

Configuración del Motor Pygame

Configuramos los parámetros fundamentales del juego y preparamos el entorno gráfico.

import pygame
import random
import sys
import time
import math
from pygame.locals import *

# Inicialización del motor
pygame.init()
pygame.font.init()

# Dimensiones de la ventana
ANCHO_VENTANA = 1080
ALTO_VENTANA = 1920
ANCHO_JUEGO = ANCHO_VENTANA

# Tamaños de entidades
ANCHO_JUGADOR, ALTO_JUGADOR = 60, 60
ANCHO_ENEMIGO, ALTO_ENEMIGO = 50, 50

# Posición inicial de enemigos
POS_INICIAL_ENEMIGO = -ALTO_ENEMIGO - 20

# Configuración de controles táctiles
ZONA_MUERTA_TOQUE = 30
VELOCIDAD_MOVIMIENTO = 17

# Ruta de fuente
ruta_fuente = pygame.font.match_font('simsun')

Estructura de Datos para Entidades

Implementamos una lista enlazada simple para gestionar dinámicamente los proyectiles y enemigos.

class Entidad:
    def __init__(self):
        self.pos_x = 0
        self.pos_y = 0
        self.siguiente = None

# Listas principales
lista_proyectiles = Entidad()
nave_jugador = Entidad()
enemigo_alpha = Entidad()
enemigo_beta = Entidad()

# Temporizadores del juego
temporizador_disparo = 0
temporizador_enemigo1 = 0
temporizador_enemigo2 = 0

# Estado del juego
puntuacion = 0
destruidos = 0
salud_jugador = 1
juego_terminado = False

# Sistema de partículas
rastros_proyectiles = []

# Animación de texto
animacion_preparado = {
    'inicio': 0,
    'duracion': 2000,
    'pos_y_base': ALTO_VENTANA // 2,
    'rebote': 0,
    'opacidad': 255,
    'tamaño': 180
}

Gestión de Recursos Gráficos

Creamos superficies de coler para los elementos del juego y configuramos el sistema de partículas.

def crear_superficie_color(ancho, alto, color):
    superficie = pygame.Surface((ancho, alto), pygame.SRCALPHA)
    superficie.fill(color)
    return superficie

# Sprites del juego
sprite_jugador = crear_superficie_color(ANCHO_JUGADOR, ALTO_JUGADOR, (0, 180, 255))
sprite_enemigo = crear_superficie_color(ANCHO_ENEMIGO, ALTO_ENEMIGO, (255, 50, 50))
sprite_fondo = crear_superficie_color(ANCHO_VENTANA, ALTO_VENTANA, (0, 0, 30))

# Ventana del juego
ventana = pygame.display.set_mode((ANCHO_VENTANA, ALTO_VENTANA))
pygame.display.set_caption("Arcade Espacial - Efectos de Partículas")
reloj = pygame.time.Clock()

def obtener_fuente(tamaño):
    try:
        return pygame.font.Font(ruta_fuente, tamaño)
    except:
        return pygame.font.SysFont(None, tamaño)

Sistema de Partículas para Proyectiles

Generamos efectos visuales de estela para los disparos del jugador.

def generar_estela_proyectil(x, y):
    for _ in range(random.randint(3, 6)):
        desplazamiento_x = random.randint(-4, 4)
        desplazamiento_y = random.randint(18, 32)
        tamano = random.randint(2, 5)
        transparencia = random.randint(120, 220)
        color_particula = (255, random.randint(170, 255), 20, transparencia)
        
        rastros_proyectiles.append({
            'coord_x': x + desplazamiento_x,
            'coord_y': y + desplazamiento_y,
            'dimension': tamano,
            'color': color_particula,
            'vida_util': random.randint(8, 15),
            'vel_y': random.randint(-3, 0)
        })

def actualizar_dibujar_estelas():
    for estela in rastros_proyectiles[:]:
        estela['coord_y'] += estela['vel_y']
        estela['vida_util'] -= 1
        
        r, g, b, a = estela['color']
        nuevo_alpha = max(0, a - 12)
        estela['color'] = (r, g, b, nuevo_alpha)
        
        if estela['vida_util'] <= 0 or nuevo_alpha == 0:
            rastros_proyectiles.remove(estela)
        else:
            pygame.draw.circle(
                ventana, 
                estela['color'], 
                (int(estela['coord_x']), int(estela['coord_y'])), 
                estela['dimension']
            )

Inteligencia Artificial de Enemigos

Implementamos un sistema de esquiva básico para que los enmeigos intenten evitar los proyectiles.

def calcular_esquiva(enemigo_x, enemigo_y, ancho_enem, alto_enem):
    proyectil_actual = lista_proyectiles.siguiente
    rango_deteccion = 550
    direccion_movimiento = 0
    
    while proyectil_actual is not None:
        distancia_vertical = enemigo_y - proyectil_actual.pos_y
        if 0 < distancia_vertical < rango_deteccion:
            if (proyectil_actual.pos_x < enemigo_x + ancho_enem and 
                proyectil_actual.pos_x + 10 > enemigo_x):
                
                velocidad_proyectil = 12
                tiempo_impacto = max(1, distancia_vertical // velocidad_proyectil)
                
                if tiempo_impacto <= 2:
                    if proyectil_actual.pos_x + 12 < enemigo_x + ancho_enem // 2:
                        direccion_movimiento = 1
                    else:
                        direccion_movimiento = -1
                        break
        
        proyectil_actual = proyectil_actual.siguiente
    
    return direccion_movimiento

Inicialización del Juego

Configuramos las posiciones iniciales de todas las entidades y mostramos la animación de inicio.

def inicializar_posiciones():
    nave_jugador.pos_x = ANCHO_VENTANA // 2 - ANCHO_JUGADOR // 2
    nave_jugador.pos_y = ALTO_VENTANA - ALTO_JUGADOR - 40
    nave_jugador.siguiente = None
    
    lista_proyectiles.siguiente = None
    
    enemigo_alpha.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
    enemigo_alpha.pos_y = POS_INICIAL_ENEMIGO
    enemigo_alpha.siguiente = None
    
    enemigo_beta.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
    enemigo_beta.pos_y = POS_INICIAL_ENEMIGO - 180
    enemigo_beta.siguiente = None

def dibujar_texto_preparado():
    global animacion_preparado
    tiempo_actual = pygame.time.get_ticks()
    transcurrido = tiempo_actual - animacion_preparado['inicio']
    progreso = min(1.0, transcurrido / animacion_preparado['duracion'])
    
    # Efecto de rebote
    fuerza_rebote = 35
    velocidad_rebote = 5.5
    animacion_preparado['rebote'] = fuerza_rebote * abs(
        math.sin(progreso * velocidad_rebote * math.pi)
    )
    
    # Ciclo de colores
    if progreso < 0.3:
        color_texto = (255, 255, 255)
    elif progreso < 0.6:
        color_texto = (200, 201, 255)
    else:
        color_texto = (254, 255, 255)
    
    # Parpadeo de opacidad
    intensidad = 1 + 0.1 * math.sin(progreso * 15)
    alpha = int(255 * intensidad * (1 - (progreso * 0.3)))
    
    fuente_principal = obtener_fuente(animacion_preparado['tamaño'])
    superficie_texto = fuente_principal.render("¡Prepárate para la misión!", True, color_texto)
    superficie_texto.set_alpha(alpha)
    
    pos_y_texto = animacion_preparado['pos_y_base'] - animacion_preparado['rebote']
    rect_texto = superficie_texto.get_rect(center=(ANCHO_VENTANA // 2, pos_y_texto))
    ventana.blit(superficie_texto, rect_texto)
    
    fuente_secundaria = obtener_fuente(45)
    superficie_ayuda = fuente_secundaria.render("Toca la pantalla para mover tu nave", True, (255, 255, 255))
    superficie_ayuda.set_alpha(alpha)
    pos_y_ayuda = pos_y_texto + 140 - (animacion_preparado['rebote'] * 0.5)
    rect_ayuda = superficie_ayuda.get_rect(center=(ANCHO_VENTANA // 2, pos_y_ayuda))
    ventana.blit(superficie_ayuda, rect_ayuda)

def secuencia_inicio():
    global temporizador_disparo, temporizador_enemigo1, temporizador_enemigo2
    ventana.fill((0, 0, 0))
    animacion_preparado['inicio'] = pygame.time.get_ticks()
    
    tiempo_inicio = time.time()
    while time.time() - tiempo_inicio < 2.2:
        ventana.fill((0, 0, 0))
        dibujar_texto_preparado()
        
        for evento in pygame.event.get():
            if evento.type == QUIT:
                pygame.quit()
                sys.exit()
        
        pygame.display.update()
        reloj.tick(60)
    
    tiempo_actual = pygame.time.get_ticks()
    temporizador_disparo = tiempo_actual
    temporizador_enemigo1 = tiempo_actual
    temporizador_enemigo2 = tiempo_actual

Sistema de Disparos

Gestionamos la creación y movimiento de los proyectiles del jugador.

def crear_proyectil():
    global temporizador_disparo
    nuevo_proyectil = Entidad()
    nuevo_proyectil.pos_x = nave_jugador.pos_x + ANCHO_JUGADOR // 2 - 5
    nuevo_proyectil.pos_y = nave_jugador.pos_y - 30
    nuevo_proyectil.siguiente = lista_proyectiles.siguiente
    lista_proyectiles.siguiente = nuevo_proyectil

# Animación de puntuación
datos_puntuacion = {
    'visible': False,
    'valor': 0,
    'inicio': 0,
    'duracion': 2000,
    'pos_y_base': ALTO_VENTANA // 2 - 180,
    'rebote': 0
}

def mostrar_animacion_puntuacion():
    global datos_puntuacion, juego_terminado
    tiempo_actual = pygame.time.get_ticks()
    
    if not datos_puntuacion['visible']:
        return
    
    transcurrido = tiempo_actual - datos_puntuacion['inicio']
    progreso = min(1.0, transcurrido / datos_puntuacion['duracion'])
    
    # Escalado de fuente
    if progreso < 0.2:
        tamaño_fuente = int(280 + (70 - 28) * (progreso / 0.2))
    elif progreso < 0.7:
        tamaño_fuente = 700
    else:
        tamaño_fuente = int(70 - (70 - 40) * ((progreso - 0.7) / 0.3))
    
    # Efecto de rebote
    fuerza_rebote = 25
    velocidad_rebote = 9
    datos_puntuacion['rebote'] = fuerza_rebote * abs(
        math.sin(progreso * velocidad_rebote * math.pi)
    )
    
    # Transición de colores
    if progreso < 0.3:
        color_puntos = (255, 255, 0)
    elif progreso < 0.6:
        color_puntos = (255, 200, 0)
    else:
        color_puntos = (255, 220, 100)
    
    # Control de opacidad
    if juego_terminado:
        alpha = 255
    elif progreso < 0.3:
        alpha = 255
    else:
        alpha = int(255 * (1 - (progreso - 0.3) / 0.7))
    
    fuente_puntos = obtener_fuente(tamaño_fuente)
    superficie_puntos = fuente_puntos.render(f"+{datos_puntuacion['valor']}", True, color_puntos)
    superficie_puntos.set_alpha(alpha)
    
    pos_y_puntos = datos_puntuacion['pos_y_base'] - datos_puntuacion['rebote']
    rect_puntos = superficie_puntos.get_rect(center=(ANCHO_VENTANA // 2, pos_y_puntos))
    ventana.blit(superficie_puntos, rect_puntos)
    
    # Efecto de brillo
    superficie_brillo = fuente_puntos.render(f"+{datos_puntuacion['valor']}", True, (255, 255, 150))
    superficie_brillo.set_alpha(int(alpha * 0.4))
    for dx, dy in [(-3, -3), (-3, 0), (-3, 3), (0, -3), (0, 3), (3, -3), (3, 0), (3, 3)]:
        ventana.blit(superficie_brillo, 
                   (rect_puntos.centerx - superficie_brillo.get_width() // 2 + dx,
                    rect_puntos.centery - superficie_brillo.get_height() // 2 + dy))

Bucle Principle del Juego

El núcleo del juego donde se procesan todos los sistemas.

def bucle_principal():
    global destruidos, salud_jugador, desplazamiento_fondo, temporizador_disparo
    global temporizador_enemigo1, temporizador_enemigo2, juego_terminado
    
    inicializar_posiciones()
    secuencia_inicio()
    
    ejecutando = True
    toque_activo = False
    posicion_toque = (0, 0)
    desplazamiento_fondo = 0
    
    while ejecutando:
        tiempo_actual = pygame.time.get_ticks()
        ventana.fill((0, 0, 0))
        
        # Sistema de fondo deslizable
        desplazamiento_fondo += 2
        if desplazamiento_fondo >= ALTO_VENTANA:
            desplazamiento_fondo = 0
        ventana.blit(sprite_fondo, (0, desplazamiento_fondo - ALTO_VENTANA))
        ventana.blit(sprite_fondo, (0, desplazamiento_fondo))
        
        # Renderizado de entidades
        ventana.blit(sprite_jugador, (nave_jugador.pos_x, nave_jugador.pos_y))
        ventana.blit(sprite_enemigo, (enemigo_alpha.pos_x, enemigo_alpha.pos_y))
        ventana.blit(sprite_enemigo, (enemigo_beta.pos_x, enemigo_beta.pos_y))
        
        # Generación de disparos
        if tiempo_actual - temporizador_disparo >= 580 and not juego_terminado:
            crear_proyectil()
            temporizador_disparo = tiempo_actual
        
        # Procesamiento de proyectiles
        proyectil_actual = lista_proyectiles.siguiente
        anterior = lista_proyectiles
        
        while proyectil_actual is not None:
            # Dibujar proyectil
            pygame.draw.rect(ventana, (255, 255, 100), 
                           (proyectil_actual.pos_x, proyectil_actual.pos_y, 10, 32))
            pygame.draw.rect(ventana, (255, 255, 255), 
                           (proyectil_actual.pos_x, proyectil_actual.pos_y, 10, 6))
            
            proyectil_actual.pos_y -= 12
            generar_estela_proyectil(proyectil_actual.pos_x, proyectil_actual.pos_y)
            
            # Detección de impactos
            impacto = False
            valor_impacto = 0
            
            if not juego_terminado:
                # Colisión con enemigo alpha
                if (proyectil_actual.pos_y - 8 <= enemigo_alpha.pos_y + ALTO_ENEMIGO and
                    proyectil_actual.pos_y + 32 + 8 >= enemigo_alpha.pos_y and
                    proyectil_actual.pos_x - 4 <= enemigo_alpha.pos_x + ANCHO_ENEMIGO and
                    proyectil_actual.pos_x + 10 + 4 >= enemigo_alpha.pos_x):
                    impacto = True
                    valor_impacto = 1
                    enemigo_alpha.pos_y = POS_INICIAL_ENEMIGO
                    enemigo_alpha.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
                    destruidos += valor_impacto
                
                # Colisión con enemigo beta
                elif (proyectil_actual.pos_y - 8 <= enemigo_beta.pos_y + ALTO_ENEMIGO and
                      proyectil_actual.pos_y + 32 + 8 >= enemigo_beta.pos_y and
                      proyectil_actual.pos_x - 4 <= enemigo_beta.pos_x + ANCHO_ENEMIGO and
                      proyectil_actual.pos_x + 10 + 4 >= enemigo_beta.pos_x):
                    impacto = True
                    valor_impacto = 1
                    enemigo_beta.pos_y = POS_INICIAL_ENEMIGO - 180
                    enemigo_beta.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
                    destruidos += valor_impacto
            
            # Activar animación de puntuación
            if impacto and valor_impacto > 0:
                datos_puntuacion['visible'] = True
                datos_puntuacion['valor'] = destruidos
                datos_puntuacion['inicio'] = tiempo_actual
            
            # Eliminar proyectil
            if impacto or proyectil_actual.pos_y < -60:
                anterior.siguiente = proyectil_actual.siguiente
                del proyectil_actual
                proyectil_actual = anterior.siguiente
            else:
                anterior = proyectil_actual
                proyectil_actual = proyectil_actual.siguiente
        
        # Actualizar efectos visuales
        actualizar_dibujar_estelas()
        mostrar_animacion_puntuacion()
        
        # Movimiento de enemigo alpha con IA
        if tiempo_actual - temporizador_enemigo1 >= 55 and not juego_terminado:
            direccion = calcular_esquiva(enemigo_alpha.pos_x, enemigo_alpha.pos_y, 
                                       ANCHO_ENEMIGO, ALTO_ENEMIGO)
            if direccion == 1 and enemigo_alpha.pos_x < ANCHO_JUEGO - ANCHO_ENEMIGO:
                enemigo_alpha.pos_x += 28
            elif direccion == -1 and enemigo_alpha.pos_x > 0:
                enemigo_alpha.pos_x -= 7
            
            if enemigo_alpha.pos_y < ALTO_VENTANA:
                enemigo_alpha.pos_y += 4
            else:
                enemigo_alpha.pos_y = POS_INICIAL_ENEMIGO
                enemigo_alpha.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
            
            temporizador_enemigo1 = tiempo_actual
        
        # Movimiento de enemigo beta con IA
        if tiempo_actual - temporizador_enemigo2 >= 75 and not juego_terminado:
            direccion = calcular_esquiva(enemigo_beta.pos_x, enemigo_beta.pos_y, 
                                       ANCHO_ENEMIGO, ALTO_ENEMIGO)
            if direccion == 1 and enemigo_beta.pos_x < ANCHO_JUEGO - ANCHO_ENEMIGO:
                enemigo_beta.pos_x += 18
            elif direccion == -1 and enemigo_beta.pos_x > 0:
                enemigo_beta.pos_x -= 6
            
            if enemigo_beta.pos_y < ALTO_VENTANA:
                enemigo_beta.pos_y += 2
            else:
                enemigo_beta.pos_y = POS_INICIAL_ENEMIGO - 180
                enemigo_beta.pos_x = random.randint(0, ANCHO_JUEGO - ANCHO_ENEMIGO)
            
            temporizador_enemigo2 = tiempo_actual
        
        # Detección de colisiones jugador-enemigo
        colision_alpha = (nave_jugador.pos_x + ANCHO_JUGADOR >= enemigo_alpha.pos_x and 
                         nave_jugador.pos_x <= enemigo_alpha.pos_x + ANCHO_ENEMIGO and
                         nave_jugador.pos_y + ALTO_JUGADOR >= enemigo_alpha.pos_y and 
                         nave_jugador.pos_y <= enemigo_alpha.pos_y + ALTO_ENEMIGO)
        
        colision_beta = (nave_jugador.pos_x + ANCHO_JUGADOR >= enemigo_beta.pos_x and 
                        nave_jugador.pos_x <= enemigo_beta.pos_x + ANCHO_ENEMIGO and
                        nave_jugador.pos_y + ALTO_JUGADOR >= enemigo_beta.pos_y and 
                        nave_jugador.pos_y <= enemigo_beta.pos_y + ALTO_ENEMIGO)
        
        if colision_alpha or colision_beta:
            salud_jugador = 0
        
        # Pantalla de Game Over
        if salud_jugador == 0:
            fuente_gameover = obtener_fuente(350)
            texto_gameover = fuente_gameover.render("¡JUEGO TERMINADO!", True, (255, 0, 0))
            rect_gameover = texto_gameover.get_rect(center=(ANCHO_VENTANA // 2, ALTO_VENTANA // 2))
            ventana.blit(texto_gameover, rect_gameover)
            
            fuente_puntuacion = obtener_fuente(160)
            texto_puntuacion = fuente_puntuacion.render(f"Puntuación Final: {destruidos}", 
                                                      True, (255, 255, 255))
            rect_puntuacion = texto_puntuacion.get_rect(
                center=(ANCHO_VENTANA // 2, ALTO_VENTANA // 2 + 240))
            ventana.blit(texto_puntuacion, rect_puntuacion)
            
            pygame.display.update()
            time.sleep(3)
            ejecutando = False
        
        # Manejo de eventos
        for evento in pygame.event.get():
            if evento.type == QUIT:
                ejecutando = False
            elif evento.type == MOUSEBUTTONDOWN:
                toque_activo = True
                posicion_toque = evento.pos
            elif evento.type == MOUSEMOTION:
                if toque_activo:
                    posicion_toque = evento.pos
            elif evento.type == MOUSEBUTTONUP:
                toque_activo = False
        
        # Control táctil
        if toque_activo:
            x_toque, y_toque = posicion_toque
            objetivo_x = x_toque - ANCHO_JUGADOR // 2
            objetivo_y = y_toque - ALTO_JUGADOR // 2
            
            objetivo_x = max(0, min(objetivo_x, ANCHO_VENTANA - ANCHO_JUGADOR))
            objetivo_y = max(0, min(objetivo_y, ALTO_VENTANA - ALTO_JUGADOR))
            
            if abs(nave_jugador.pos_x - objetivo_x) > ZONA_MUERTA_TOQUE:
                nave_jugador.pos_x += VELOCIDAD_MOVIMIENTO if objetivo_x > nave_jugador.pos_x else -VELOCIDAD_MOVIMIENTO
            if abs(nave_jugador.pos_y - objetivo_y) > ZONA_MUERTA_TOQUE:
                nave_jugador.pos_y += VELOCIDAD_MOVIMIENTO if objetivo_y > nave_jugador.pos_y else -VELOCIDAD_MOVIMIENTO
        
        # Controles de teclado (alternativo)
        teclas = pygame.key.get_pressed()
        if teclas[K_UP] and nave_jugador.pos_y > 0:
            nave_jugador.pos_y -= 5
        if teclas[K_DOWN] and nave_jugador.pos_y < ALTO_VENTANA - ALTO_JUGADOR:
            nave_jugador.pos_y += 5
        if teclas[K_LEFT] and nave_jugador.pos_x > 0:
            nave_jugador.pos_x -= 5
        if teclas[K_RIGHT] and nave_jugador.pos_x < ANCHO_VENTANA - ANCHO_JUGADOR:
            nave_jugador.pos_x += 5
        
        pygame.display.update()
        reloj.tick(60)
    
    pygame.quit()
    sys.exit(0)

# Punto de entrada
if __name__ == "__main__":
    bucle_principal()

Etiquetas: pygame Python tkinter Game-Development particle-system

Publicado el 10-5 00:13