Bloques en 3D y volumen

This commit is contained in:
luisgulo 2026-10-04 15:13:14 +02:00
parent ce640e1d93
commit 1b272ba47e
2 changed files with 120 additions and 128 deletions

View File

@ -1,7 +1,8 @@
import math
import sys import sys
import math
import pygame import pygame
# Inicialización de Pygame
pygame.init() pygame.init()
# Configuración de pantalla # Configuración de pantalla
@ -13,19 +14,12 @@ FPS = 60
TILE_WIDTH = 64 TILE_WIDTH = 64
TILE_HEIGHT = 32 TILE_HEIGHT = 32
GRID_SIZE_X = 8 GRID_SIZE_X = 8
GRID_SIZE_Y = 120 # Distancia total de la autopista GRID_SIZE_Y = 120
# Paleta de Colores # Colores de apoyo
COLOR_BG = (15, 15, 25) COLOR_BG = (15, 15, 25)
COLOR_ROAD = (50, 50, 65) COLOR_ROAD = (50, 50, 65)
COLOR_GRID = (30, 30, 45) COLOR_GRID = (30, 30, 45)
COLOR_VORTEX = (220, 50, 50)
COLOR_PLAYER = (255, 220, 0)
COLOR_SPARE = (180, 160, 0)
COLOR_BLOCK = (180, 70, 40)
COLOR_WALL = (80, 90, 110)
COLOR_ENEMY = (50, 200, 100)
COLOR_BULLET = (0, 255, 255)
def grid_to_iso(gx, gy, gz=0): def grid_to_iso(gx, gy, gz=0):
@ -35,64 +29,109 @@ def grid_to_iso(gx, gy, gz=0):
return iso_x, iso_y return iso_x, iso_y
class Entity: # ==============================================================================
# GENERADOR DE SPRITES AUTOMÁTICO (O Carga desde PNG si existen)
# ==============================================================================
def load_or_create_sprite(filename, create_func):
"""Intenta cargar una imagen PNG. Si no existe, la crea dinámicamente."""
try:
image = pygame.image.load(filename).convert_alpha()
return image
except (pygame.error, FileNotFoundError):
return create_func()
def __init__(self, gx, gy, color, is_solid=True):
def create_player_sprite():
surf = pygame.Surface((64, 64), pygame.SRCALPHA)
# Cuerpo principal Lasertron
pygame.draw.ellipse(surf, (255, 200, 0), (16, 20, 32, 28))
pygame.draw.ellipse(surf, (255, 255, 200), (20, 12, 24, 20))
pygame.draw.circle(surf, (0, 200, 255), (32, 20), 6) # Visor
# Cañón de disparo
pygame.draw.rect(surf, (150, 150, 150), (30, 6, 4, 10))
return surf
def create_vortex_sprite():
surf = pygame.Surface((64, 64), pygame.SRCALPHA)
# Base pesada VORTEX
pygame.draw.ellipse(surf, (180, 40, 40), (8, 24, 48, 30))
pygame.draw.ellipse(surf, (220, 60, 60), (14, 16, 36, 22))
pygame.draw.circle(surf, (255, 255, 255), (32, 24), 8)
pygame.draw.circle(surf, (255, 0, 0), (32, 24), 4)
return surf
def create_block_sprite():
surf = pygame.Surface((64, 64), pygame.SRCALPHA)
# Cara superior
pts_top = [(32, 8), (56, 20), (32, 32), (8, 20)]
pygame.draw.polygon(surf, (210, 90, 50), pts_top)
# Cara izquierda
pts_left = [(8, 20), (32, 32), (32, 52), (8, 40)]
pygame.draw.polygon(surf, (150, 60, 30), pts_left)
# Cara derecha
pts_right = [(32, 32), (56, 20), (56, 40), (32, 52)]
pygame.draw.polygon(surf, (180, 75, 40), pts_right)
# Bordes
pygame.draw.polygon(surf, (255, 255, 255), pts_top, 1)
return surf
def create_enemy_sprite():
surf = pygame.Surface((64, 64), pygame.SRCALPHA)
# Dron Alienígena
pygame.draw.ellipse(surf, (50, 220, 100), (12, 16, 40, 32))
pygame.draw.circle(surf, (255, 0, 100), (32, 30), 8)
pygame.draw.line(surf, (50, 220, 100), (20, 48), (12, 58), 3)
pygame.draw.line(surf, (50, 220, 100), (44, 48), (52, 58), 3)
return surf
def create_bullet_sprite():
surf = pygame.Surface((32, 32), pygame.SRCALPHA)
pygame.draw.circle(surf, (0, 255, 255), (16, 16), 6)
pygame.draw.circle(surf, (255, 255, 255), (16, 16), 3)
return surf
# ==============================================================================
# CLASES DEL JUEGO
# ==============================================================================
class Entity:
def __init__(self, gx, gy, sprite, is_solid=True):
self.gx = float(gx) self.gx = float(gx)
self.gy = float(gy) self.gy = float(gy)
self.gz = 0 self.gz = 0
self.color = color self.sprite = sprite
self.is_solid = is_solid self.is_solid = is_solid
self.radius = 0.4
self.height = 20
def draw(self, surface, camera_offset_x, camera_offset_y): def draw(self, surface, camera_offset_x, camera_offset_y):
screen_x, screen_y = grid_to_iso(self.gx, self.gy, self.gz) screen_x, screen_y = grid_to_iso(self.gx, self.gy, self.gz)
draw_x = screen_x + camera_offset_x draw_x = screen_x + camera_offset_x - (self.sprite.get_width() // 2)
draw_y = screen_y + camera_offset_y draw_y = screen_y + camera_offset_y - (self.sprite.get_height() // 2)
surface.blit(self.sprite, (draw_x, draw_y))
points = [
(draw_x, draw_y - self.height),
(
draw_x + TILE_WIDTH // 2,
draw_y - self.height + TILE_HEIGHT // 2,
),
(draw_x, draw_y - self.height + TILE_HEIGHT),
(
draw_x - TILE_WIDTH // 2,
draw_y - self.height + TILE_HEIGHT // 2,
),
]
pygame.draw.polygon(surface, self.color, points)
pygame.draw.polygon(surface, (255, 255, 255), points, 2)
class Bullet(Entity): class Bullet(Entity):
def __init__(self, gx, gy, dir_x, dir_y, sprite):
def __init__(self, gx, gy, dir_x, dir_y): super().__init__(gx, gy, sprite, is_solid=False)
super().__init__(gx, gy, COLOR_BULLET, is_solid=False)
self.dir_x = dir_x self.dir_x = dir_x
self.dir_y = dir_y self.dir_y = dir_y
self.speed = 0.25 self.speed = 0.25
self.height = 10
self.alive = True self.alive = True
def update(self, entities): def update(self, entities):
self.gx += self.dir_x * self.speed self.gx += self.dir_x * self.speed
self.gy += self.dir_y * self.speed self.gy += self.dir_y * self.speed
# Comprobar límites
if not (0 <= self.gx < GRID_SIZE_X and 0 <= self.gy < GRID_SIZE_Y): if not (0 <= self.gx < GRID_SIZE_X and 0 <= self.gy < GRID_SIZE_Y):
self.alive = False self.alive = False
return return
# Impacto con entidades
for entity in entities: for entity in entities:
if entity != self and entity.is_solid: if entity != self and entity.is_solid:
if ( if abs(entity.gx - self.gx) < 0.5 and abs(entity.gy - self.gy) < 0.5:
abs(entity.gx - self.gx) < 0.5
and abs(entity.gy - self.gy) < 0.5
):
self.alive = False self.alive = False
if isinstance(entity, (PushBlock, Enemy)): if isinstance(entity, (PushBlock, Enemy)):
entity.health -= 1 entity.health -= 1
@ -100,10 +139,9 @@ class Bullet(Entity):
class PushBlock(Entity): class PushBlock(Entity):
def __init__(self, gx, gy, sprite):
def __init__(self, gx, gy): super().__init__(gx, gy, sprite, is_solid=True)
super().__init__(gx, gy, COLOR_BLOCK, is_solid=True) self.health = 2
self.health = 2 # Se puede destruir tras 2 disparos
def push(self, dx, dy, entities): def push(self, dx, dy, entities):
new_gx = self.gx + dx new_gx = self.gx + dx
@ -114,10 +152,7 @@ class PushBlock(Entity):
for entity in entities: for entity in entities:
if entity != self and entity.is_solid: if entity != self and entity.is_solid:
if ( if abs(entity.gx - new_gx) < 0.5 and abs(entity.gy - new_gy) < 0.5:
abs(entity.gx - new_gx) < 0.5
and abs(entity.gy - new_gy) < 0.5
):
return False return False
self.gx = new_gx self.gx = new_gx
@ -126,9 +161,8 @@ class PushBlock(Entity):
class Enemy(Entity): class Enemy(Entity):
def __init__(self, gx, gy, sprite):
def __init__(self, gx, gy): super().__init__(gx, gy, sprite, is_solid=True)
super().__init__(gx, gy, COLOR_ENEMY, is_solid=True)
self.speed = 0.03 self.speed = 0.03
self.direction = 1 self.direction = 1
self.health = 1 self.health = 1
@ -136,7 +170,6 @@ class Enemy(Entity):
def update(self, entities): def update(self, entities):
target_gx = self.gx + (self.speed * self.direction) target_gx = self.gx + (self.speed * self.direction)
# Rebotar en los bordes de la carretera
if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5: if target_gx <= 0.5 or target_gx >= GRID_SIZE_X - 0.5:
self.direction *= -1 self.direction *= -1
target_gx = self.gx + (self.speed * self.direction) target_gx = self.gx + (self.speed * self.direction)
@ -145,12 +178,12 @@ class Enemy(Entity):
class Player(Entity): class Player(Entity):
def __init__(self, gx, gy, sprite, bullet_sprite):
def __init__(self, gx, gy): super().__init__(gx, gy, sprite, is_solid=True)
super().__init__(gx, gy, COLOR_PLAYER, is_solid=True) self.bullet_sprite = bullet_sprite
self.speed = 0.09 self.speed = 0.09
self.facing_x = 0.0 self.facing_x = 0.0
self.facing_y = -1.0 # Mirando hacia adelante por defecto self.facing_y = -1.0
self.shoot_cooldown = 0 self.shoot_cooldown = 0
def update(self, keys, entities, bullets): def update(self, keys, entities, bullets):
@ -165,7 +198,6 @@ class Player(Entity):
dy += self.speed dy += self.speed
if dx != 0 or dy != 0: if dx != 0 or dy != 0:
# Actualizar la dirección en la que mira el jugador
norm = math.sqrt(dx * dx + dy * dy) norm = math.sqrt(dx * dx + dy * dy)
self.facing_x = dx / norm self.facing_x = dx / norm
self.facing_y = dy / norm self.facing_y = dy / norm
@ -177,17 +209,10 @@ class Player(Entity):
blocked = False blocked = False
for entity in entities: for entity in entities:
if entity != self and entity.is_solid: if entity != self and entity.is_solid:
if ( if abs(entity.gx - target_gx) < 0.5 and abs(entity.gy - target_gy) < 0.5:
abs(entity.gx - target_gx) < 0.5
and abs(entity.gy - target_gy) < 0.5
):
if isinstance(entity, PushBlock): if isinstance(entity, PushBlock):
push_dx = ( push_dx = 0.15 if dx > 0 else (-0.15 if dx < 0 else 0)
0.15 if dx > 0 else (-0.15 if dx < 0 else 0) push_dy = 0.15 if dy > 0 else (-0.15 if dy < 0 else 0)
)
push_dy = (
0.15 if dy > 0 else (-0.15 if dy < 0 else 0)
)
if not entity.push(push_dx, push_dy, entities): if not entity.push(push_dx, push_dy, entities):
blocked = True blocked = True
else: else:
@ -196,24 +221,19 @@ class Player(Entity):
self.gx = target_gx self.gx = target_gx
self.gy = target_gy self.gy = target_gy
# Gestión del enfriamiento de disparo
if self.shoot_cooldown > 0: if self.shoot_cooldown > 0:
self.shoot_cooldown -= 1 self.shoot_cooldown -= 1
# Disparar al pulsar la barra espaciadora
if keys[pygame.K_SPACE] and self.shoot_cooldown == 0: if keys[pygame.K_SPACE] and self.shoot_cooldown == 0:
# Generar la bala desplazada por delante del jugador para evitar la auto-colisión
spawn_x = self.gx + (self.facing_x * 0.7) spawn_x = self.gx + (self.facing_x * 0.7)
spawn_y = self.gy + (self.facing_y * 0.7) spawn_y = self.gy + (self.facing_y * 0.7)
bullets.append(Bullet(spawn_x, spawn_y, self.facing_x, self.facing_y, self.bullet_sprite))
bullets.append(Bullet(spawn_x, spawn_y, self.facing_x, self.facing_y)) self.shoot_cooldown = 15
self.shoot_cooldown = 15 # Cadencia de tiro
class Vortex(Entity): class Vortex(Entity):
def __init__(self, gx, gy, sprite):
def __init__(self, gx, gy): super().__init__(gx, gy, sprite, is_solid=True)
super().__init__(gx, gy, COLOR_VORTEX, is_solid=True)
self.speed = 0.012 self.speed = 0.012
def update(self, entities): def update(self, entities):
@ -222,10 +242,7 @@ class Vortex(Entity):
blocked = False blocked = False
for entity in entities: for entity in entities:
if entity != self and entity.is_solid: if entity != self and entity.is_solid:
if ( if abs(entity.gx - self.gx) < 0.6 and abs(entity.gy - target_gy) < 0.6:
abs(entity.gx - self.gx) < 0.6
and abs(entity.gy - target_gy) < 0.6
):
blocked = True blocked = True
break break
@ -234,53 +251,49 @@ class Vortex(Entity):
class Game: class Game:
def __init__(self): def __init__(self):
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Highway Encounter - Full Game") pygame.display.set_caption("Highway Encounter - PNG Sprites Version")
self.clock = pygame.time.Clock() self.clock = pygame.time.Clock()
self.font = pygame.font.SysFont("monospace", 18, bold=True) self.font = pygame.font.SysFont("monospace", 18, bold=True)
# Carga o Generación de Sprites
self.spr_player = load_or_create_sprite("player.png", create_player_sprite)
self.spr_vortex = load_or_create_sprite("vortex.png", create_vortex_sprite)
self.spr_block = load_or_create_sprite("block.png", create_block_sprite)
self.spr_enemy = load_or_create_sprite("enemy.png", create_enemy_sprite)
self.spr_bullet = load_or_create_sprite("bullet.png", create_bullet_sprite)
self.lives = 4 self.lives = 4
self.score = 0 self.score = 0
self.reset_level() self.reset_level()
def reset_level(self): def reset_level(self):
self.vortex = Vortex(3.5, GRID_SIZE_Y - 5) self.vortex = Vortex(3.5, GRID_SIZE_Y - 5, self.spr_vortex)
self.player = Player(3.5, GRID_SIZE_Y - 8) self.player = Player(3.5, GRID_SIZE_Y - 8, self.spr_player, self.spr_bullet)
self.entities = [self.vortex, self.player] self.entities = [self.vortex, self.player]
self.bullets = [] self.bullets = []
# Crear escuadrón de reserva (Vidas restantes)
self.spares = [] self.spares = []
for i in range(self.lives - 1): for i in range(self.lives - 1):
spare = Entity( spare = Entity(3.5, GRID_SIZE_Y - 3 + (i * 1.2), self.spr_player, is_solid=False)
3.5, GRID_SIZE_Y - 3 + (i * 1.2), COLOR_SPARE, is_solid=False
)
self.spares.append(spare) self.spares.append(spare)
# Generar obstáculos y enemigos en la autopista
for y in range(10, GRID_SIZE_Y - 15, 6): for y in range(10, GRID_SIZE_Y - 15, 6):
self.entities.append(PushBlock(y % GRID_SIZE_X, y)) self.entities.append(PushBlock(y % GRID_SIZE_X, y, self.spr_block))
if y % 12 == 0: if y % 12 == 0:
self.entities.append(Enemy(3.5, y - 2)) self.entities.append(Enemy(3.5, y - 2, self.spr_enemy))
def handle_collisions(self): def handle_collisions(self):
# Muerte del jugador por contacto con enemigos
for entity in self.entities: for entity in self.entities:
if isinstance(entity, Enemy): if isinstance(entity, Enemy):
if ( if abs(entity.gx - self.player.gx) < 0.5 and abs(entity.gy - self.player.gy) < 0.5:
abs(entity.gx - self.player.gx) < 0.5
and abs(entity.gy - self.player.gy) < 0.5
):
self.lives -= 1 self.lives -= 1
if self.lives > 0: if self.lives > 0:
self.reset_level() self.reset_level()
return return
# Eliminar bloques/enemigos destruidos
for entity in self.entities[:]: for entity in self.entities[:]:
if hasattr(entity, "health") and entity.health <= 0: if hasattr(entity, "health") and entity.health <= 0:
self.entities.remove(entity) self.entities.remove(entity)
@ -304,12 +317,8 @@ class Game:
if self.lives <= 0: if self.lives <= 0:
self.screen.fill((0, 0, 0)) self.screen.fill((0, 0, 0))
msg = self.font.render( msg = self.font.render("GAME OVER - Press R to Restart", True, (255, 50, 50))
"GAME OVER - Press R to Restart", True, (255, 50, 50) self.screen.blit(msg, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2))
)
self.screen.blit(
msg, (SCREEN_WIDTH // 2 - 150, SCREEN_HEIGHT // 2)
)
pygame.display.flip() pygame.display.flip()
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
@ -319,7 +328,6 @@ class Game:
self.reset_level() self.reset_level()
continue continue
# Actualizaciones
keys = pygame.key.get_pressed() keys = pygame.key.get_pressed()
self.player.update(keys, self.entities, self.bullets) self.player.update(keys, self.entities, self.bullets)
self.vortex.update(self.entities) self.vortex.update(self.entities)
@ -335,45 +343,30 @@ class Game:
self.handle_collisions() self.handle_collisions()
# Posicionamiento de cámara centrado en el jugador
iso_x, iso_y = grid_to_iso(self.player.gx, self.player.gy) iso_x, iso_y = grid_to_iso(self.player.gx, self.player.gy)
camera_offset_x = SCREEN_WIDTH // 2 - iso_x camera_offset_x = SCREEN_WIDTH // 2 - iso_x
camera_offset_y = SCREEN_HEIGHT // 2 - iso_y + 100 camera_offset_y = SCREEN_HEIGHT // 2 - iso_y + 100
# Renderizado
self.screen.fill(COLOR_BG) self.screen.fill(COLOR_BG)
# Dibujar suelo
for gy in range(0, GRID_SIZE_Y): for gy in range(0, GRID_SIZE_Y):
for gx in range(0, GRID_SIZE_X): for gx in range(0, GRID_SIZE_X):
sx, sy = grid_to_iso(gx, gy) sx, sy = grid_to_iso(gx, gy)
draw_x = sx + camera_offset_x draw_x = sx + camera_offset_x
draw_y = sy + camera_offset_y draw_y = sy + camera_offset_y
if ( if -100 < draw_x < SCREEN_WIDTH + 100 and -100 < draw_y < SCREEN_HEIGHT + 100:
-100 < draw_x < SCREEN_WIDTH + 100
and -100 < draw_y < SCREEN_HEIGHT + 100
):
pts = [ pts = [
(draw_x, draw_y), (draw_x, draw_y),
( (draw_x + TILE_WIDTH // 2, draw_y + TILE_HEIGHT // 2),
draw_x + TILE_WIDTH // 2,
draw_y + TILE_HEIGHT // 2,
),
(draw_x, draw_y + TILE_HEIGHT), (draw_x, draw_y + TILE_HEIGHT),
( (draw_x - TILE_WIDTH // 2, draw_y + TILE_HEIGHT // 2),
draw_x - TILE_WIDTH // 2,
draw_y + TILE_HEIGHT // 2,
),
] ]
pygame.draw.polygon(self.screen, COLOR_ROAD, pts) pygame.draw.polygon(self.screen, COLOR_ROAD, pts)
pygame.draw.polygon(self.screen, COLOR_GRID, pts, 1) pygame.draw.polygon(self.screen, COLOR_GRID, pts, 1)
# Renderizado en orden de profundidad (Z-Sorting por Y)
all_drawables = self.entities + self.bullets + self.spares all_drawables = self.entities + self.bullets + self.spares
sorted_entities = sorted( sorted_entities = sorted(all_drawables, key=lambda e: e.gy, reverse=True)
all_drawables, key=lambda e: e.gy, reverse=True
)
for entity in sorted_entities: for entity in sorted_entities:
entity.draw(self.screen, camera_offset_x, camera_offset_y) entity.draw(self.screen, camera_offset_x, camera_offset_y)
@ -386,4 +379,3 @@ class Game:
if __name__ == "__main__": if __name__ == "__main__":
Game().run() Game().run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB