En el post anterior puse un script para poder apreciar la diferencia de precisión existente entre las colisiones basadas en los rectángulos delimitadores de los sprites y aquellas basadas en las máscaras de sus formas, y en éste voy a poner un script de ejemplo de la utilización de la colisión por máscaras en un videojuego 2D, para lo que tomo otra vez como referencia uno de mis video juegos tipo arcade y género plataformas preferido, además de ser uno de los más populares de todos los tiempos: 'Donkey Kong'.
Para este script de ejemplo parto del que incluí en la última entrada del tutorial correspondiente a 'Salto de personajes'.
El script final, sobre el que, tras leer el post anterior, no creo que haya que realizar explicaciones adicionales a los comentarios que en él figuran, es el siguiente:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# COLISIONES PRECISAS (MÁSCARAS):
#
# Colisiones entre sprites utilizando el método pygame.sprite.collide_mask().
#
# Autor: Mikel García Larragan.
# https://mikelgarcialarragan.blogspot.com/
import pygame
import os
class DonkeyKong(pygame.sprite.Sprite):
# Donkey Kong estará definido por la clase DonkeyKong, que hereda de la
# clase sprite.Sprite de Pygame. Clase base para objetos visibles del
# juego.
def __init__(self, x, y):
super().__init__()
self.x, self.y = x, y
# Dibujar a Donkey Kong en su posición inicial.
self.image = pygame.image.load('recursos/imagenes/donkey_kong_1.png') \
.convert_alpha()
def animar(self, imagen):
# Método que permite que Donkey Kong tire barriles durante el juego y
# gruña al finalizar éste.
self.imagen = imagen
self.image = pygame.image.load('recursos/imagenes/donkey_kong_' +
str(self.imagen) + '.png').convert_alpha()
def update(self):
# La función update() se ejecutará en cada frame, y permite actualizar
# la posición de Donkey Kong.
self.rect = self.image.get_rect(center=(self.x, self.y))
class Jumpman(pygame.sprite.Sprite):
# Jumpman (Mario) estará definido por la clase Jumpman, que hereda de la
# clase sprite.Sprite de Pygame. Clase base para objetos visibles del
# juego.
def __init__(self, x, y):
super().__init__()
self.x, self.y = x, y
self.image = pygame.image.load('recursos/imagenes/' +
'jumpman_derecha_1.png').convert_alpha()
self.mask = pygame.mask.from_surface(self.image)
self.rect = self.image.get_rect(center=(self.x, self.y))
def mover(self, movimiento, sentido, paso, vel_x, vel_y):
# Método que permite que Jumpman se mueva (ande o salte) en el sentido
# indicado.
self.movimiento = movimiento
self.sentido = sentido
self.paso = paso
self.x += vel_x
self.y += vel_y
if self.movimiento == 'ANDAR':
self.image = pygame.image.load('recursos/imagenes/' +
'jumpman_' + self.sentido + '_' +
str(self.paso) + '.png') \
.convert_alpha()
elif self.movimiento == 'SALTAR':
self.image = pygame.image.load('recursos/imagenes/' +
'jumpman_saltar_' +
self.sentido + '.png') \
.convert_alpha()
def finalizar_mover(self):
# Método que permite completar el movimiento de Jumpman, tanto cuando
# anda como cuando salta, mostrando la imagen correspondiente a Jumpman
# parado.
self.image = pygame.image.load('recursos/imagenes/' +
'jumpman_' + self.sentido + '_1.png') \
.convert_alpha()
def morir(self):
# Método que permite mostrar la muerte de Jumpman cuando colisiona con
# un barril, cae al piso inmediatamente inferior al que se encuentra
# o salta fuera de la ventana.
self.image = pygame.image.load('recursos/imagenes/' +
'jumpman_morir.png').convert_alpha()
def update(self):
# El método update() se ejecutará en cada frame y permite actualizar la
# posición de Jumpman.
self.rect = self.image.get_rect(center=(self.x, self.y))
class Tramo(pygame.sprite.Sprite):
# Los tramos del suelo por donde anda y salta Jumpman estarán definidos por
# la clase Tramo, que hereda de la clase sprite.Sprite de Pygame. Clase
# base para objetos visibles del juego.
def __init__(self, x, y):
super().__init__()
self.x, self.y = x, y
self.image = pygame.image.load('recursos/imagenes/tramo.png') \
.convert_alpha()
self.rect = self.image.get_rect(center=(self.x, self.y))
self.alto_tramo = self.image.get_height()
def update(self):
# El método update() se ejecutará en cada frame, y permite actualizar
# los tramos.
self.rect = self.image.get_rect(center=(self.x, self.y))
class Barril(pygame.sprite.Sprite):
# Los barriles que arroja Donkey Kong estarán definidos por la clase
# Barril, que hereda de la clase sprite.Sprite de Pygame. Clase base
# para objetos visibles del juego.
def __init__(self, piso, x, y):
super().__init__()
self.piso = piso
self.imagen = 1
self.x, self.y = x, y
self.vel_x, self.vel_y = 3, 1
self.image = pygame.image.load('recursos/imagenes/barril_' +
str(self.imagen) + '.png') \
.convert_alpha()
self.mask = pygame.mask.from_surface(self.image)
self.rect = self.image.get_rect(center=(self.x, self.y))
def caer(self, vel_x, vel_y):
# Método que permite que los barriles vayan cayendo por las
# platafornas.
self.vel_x, self.vel_y = vel_x, vel_y
self.imagen += 1
if self.imagen > 4:
self.imagen = 1
self.image = pygame.image.load('recursos/imagenes/barril_' +
str(self.imagen) + '.png') \
.convert_alpha()
self.mask = pygame.mask.from_surface(self.image)
def update(self):
# El método update() se ejecutará en cada frame, y permite actualizar
# la posición de los barriles.
self.rect.move_ip(self.vel_x, self.vel_y)
def donkey_kong():
# Inicializar módulos internos de pygame.
pygame.init()
# Establecer tamaño de la ventana.
# Crear una superficie de visualización: la ventana, y centrarla.
os.environ['SDL_VIDEO_CENTERED']='1'
ventana = pygame.display.set_mode((674, 430))
# Título de la ventana.
pygame.display.set_caption('Donkey Kong')
# Cargar la imagen del fondo y cambiar el formato de píxel para crear una
# copia que se dibujará más rápidamente en la pantalla.
fondo = pygame.image.load('recursos/imagenes/fondo_2.png')
fondo = fondo.convert()
# Cargar archivos de fuente y de sonidos.
font_emulogic_12 = pygame.font.Font(
'recursos/fuentes/emulogic.ttf', 12)
font_emulogic_48 = pygame.font.Font(
'recursos/fuentes/emulogic.ttf', 48)
sonido_andar = pygame.mixer.Sound(
'recursos/sonidos/andar.wav')
sonido_saltar = pygame.mixer.Sound(
'recursos/sonidos/saltar.wav')
sonido_morir = pygame.mixer.Sound(
'recursos/sonidos/morir.wav')
sonido_juego = pygame.mixer.Sound(
'recursos/sonidos/juego.wav')
sonido_game_over = pygame.mixer.Sound(
'recursos/sonidos/game_over.wav')
# Crear canales para sonidos simultáneos.
canal1 = pygame.mixer.Channel(0)
canal2 = pygame.mixer.Channel(1)
# Grupo individual para Donkey Kong y dibujarlo.
grupo_donkey_kong = pygame.sprite.GroupSingle()
donkey_kong = DonkeyKong(125, 170)
grupo_donkey_kong.add(donkey_kong)
# Grupo de sprites. Agrupamos los tramos del suelo de los diferentes pisos
# usando la clase sprite.Group de Pygame para hacer que dicha clase se
# encargue de actualizarlos y dibujarlos, sin necesidad de hacerlo
# manualmente.
grupo_tramos_0 = pygame.sprite.Group()
grupo_tramos_1 = pygame.sprite.Group()
grupo_tramos_2 = pygame.sprite.Group()
# Establecer el tiempo transcurrido (valor de un contador) desde que se
# muestra una imagen de Donkey Kong tirando barriles o gruñendo y el que
# debe transcurrir hasta que se muestre la siguiente imagen.
contador_inicio_imagen_donkey_kong = 0
contador_fin_imagen_donkey_kong = 75
# Imagen de Donkey Kong a mostrar cuando tira barriles.
imagen_donkey_kong = 2
# Variable boolena que indica si Donkey Kong ha tirado un barril.
barril_tirado = False
# Colocar los tramos del suelo del piso 0 en la pantalla.
for num_tramo in range(14):
tramo = Tramo(24 + 48 * num_tramo, 396)
grupo_tramos_0.add(tramo)
# Colocar los tramos del suelo del piso 1 en la pantalla.
for num_tramo in range(13):
tramo = Tramo(72 + 48 * num_tramo, 327 - num_tramo * 2)
grupo_tramos_1.add(tramo)
# Colocar los tramos del suelo del piso 2 en la pantalla.
for num_tramo in range(13):
if num_tramo < 9:
tramo = Tramo(24 + 48 * num_tramo, 230)
else:
tramo = Tramo(24 + 48 * num_tramo, 232 + (num_tramo - 9) * 2)
grupo_tramos_2.add(tramo)
# Posición inicial de Jumpman.
x, y = 120, 296
# Grupo individual para Jumpman y dibujarlo.
grupo_jumpman = pygame.sprite.GroupSingle()
jumpman = Jumpman(x, y)
grupo_jumpman.add(jumpman)
# Variable booleana que indica si Jumpman está vivo o muerto.
jumpman_vivo = True
# Variables booleanas que indican si Jumpman se encuentra en el suelo o
# está saltando, respectivamente.
en_suelo = True
en_salto = False
# Sentido del movimiento de Jumpman, tanto cuando anda como cuando salta.
sentido = 'DERECHA'
# Velocidad de movimiento en ambos ejes.
vel_x, vel_y = 3, 0
# Variables correspondientes al salto.
sentido_salto = 0
altura_salto = 9
gravedad = 1
# Establecer el tiempo transcurrido (valor de un contador) desde el inicio
# del movimiento de Jumpman, tanto cuando está andando como cuando salta, y
# el que debe transcurrir hasta su finalización.
contador_inicio_mover = 0
contador_fin_mover = 10
# Variable booleana que indica si se debe finalizar de mostrar el
# movimiento de Jumpman (andar o saltar).
finalizar_mover = False
# Grupo para los barriles que arroja Donkey Kong.
grupo_barriles = pygame.sprite.Group()
# Variables booleanas que indican si se ha producido una colisión entre los
# rectángulos delimitadores de los sprites de Jumpman y un barril, y de sus
# máscaras (formas), respectivamente.
colision_rect = colision_mask = False
# Establecer el tiempo transcurrido (valor de un contador) desde que se
# empieza a mostrar el mensaje de colisión de los rectángulos delimitadores
# de los sprites de Jumpman y un barril, y el que debe transcurrir hasta
# que se deje de mostrar dicho mensaje.
contador_inicio_mostrar_mensaje = 0
contador_fin_mostrar_mensaje = 75
# Hacer que suene el sonido del juego.
canal1.play(sonido_juego, -1)
# Reloj para ejecutar el videojuego.
reloj = pygame.time.Clock()
# El juego se ejecutará a 60 frames por segundo.
fps = 60
while True:
# Ejecutar el siguiente frame.
reloj.tick(fps)
# Escuchar cada uno de los eventos de la lista de eventos de Pygame.
for evento in pygame.event.get():
# Finalizar el juego cuando se produzca el evento de terminación
# del programa (cierre de la ventana).
if evento.type == pygame.QUIT:
pygame.quit()
return
if jumpman_vivo:
# Comprobar si Jumpman colisiona con algún barril.
barril_colisionado = pygame.sprite.spritecollide(jumpman,
grupo_barriles,
False,
pygame.sprite.collide_mask)
if len(barril_colisionado) > 0:
colision_mask = True
canal2.play(sonido_morir)
jumpman.y += grupo_barriles.sprites()[0].rect.center[1] - \
jumpman.rect.center[1]
jumpman.morir()
jumpman_vivo = False
else:
# Obtener la lista de teclas pulsadas y gestión de las mismas.
teclas = pygame.key.get_pressed()
# Movimiento horizontal (tanto si Jumpman está en el suelo como
# si está saltando).
if en_suelo:
vel_x = 3
if teclas[pygame.K_LEFT]:
canal2.play(sonido_andar)
sentido = 'IZQUIERDA'
jumpman.mover('ANDAR', sentido, 2, -vel_x, 1)
finalizar_mover = True
elif teclas[pygame.K_RIGHT]:
canal2.play(sonido_andar)
sentido = 'DERECHA'
jumpman.mover('ANDAR', sentido, 2, vel_x, 0)
finalizar_mover = True
elif en_salto:
jumpman.mover('SALTAR', sentido, 2,
sentido_salto * 6, vel_y)
# Salto.
if teclas[pygame.K_SPACE] and en_suelo:
canal2.play(sonido_saltar)
vel_x = 0
vel_y = -altura_salto
jumpman.mover('SALTAR', sentido, 1, vel_x, -altura_salto)
en_suelo = False
en_salto = True
# Guardar el sentido en el que se inicia el salto.
if teclas[pygame.K_LEFT]:
sentido_salto = -1
elif teclas[pygame.K_RIGHT]:
sentido_salto = 1
else:
sentido_salto = 0
# Aplicar gravedad.
vel_y += gravedad
# Comprobar si colisiona con algún tramo del piso en el que
# está, tanto cuando anda como cuando salta.
tramos_colision = pygame.sprite.groupcollide(grupo_jumpman,
grupo_tramos_1,
False, False)
if len(tramos_colision) > 0:
vel_x = 0
# Colisión con el suelo.
if en_suelo:
jumpman.mover('ANDAR', sentido, 2, vel_x, -1)
elif en_salto:
jumpman.mover('SALTAR', sentido, 1, vel_x,
tramos_colision[jumpman][0].
rect.top -
jumpman.rect.bottom -
tramo.alto_tramo // 2 - 1)
finalizar_mover = True
vel_y = 0
en_suelo = True
en_salto = False
elif jumpman.rect.bottomright[0]<=grupo_tramos_1.sprites()[0].\
rect.topleft[0] or \
jumpman.rect.bottomleft[0]>=grupo_tramos_1.sprites()[-1]. \
rect.topright[0]:
vel_x = 0
jumpman.mover('ANDAR', sentido, 1, vel_x, 3)
# Comprobar si colisiona con algún tramo del piso
# inmediatamente inferior al que está.
tramos_colision = pygame.sprite.groupcollide(grupo_jumpman,
grupo_tramos_0,
False, False)
if len(tramos_colision) > 0:
canal2.play(sonido_morir)
jumpman.morir()
jumpman_vivo = False
elif jumpman.rect.bottom > 475:
canal2.play(sonido_morir)
grupo_jumpman.remove(jumpman)
jumpman_vivo = False
# Animar movimientos de Jumpman.
if finalizar_mover:
if contador_inicio_mover > contador_fin_mover:
jumpman.finalizar_mover()
contador_inicio_mover = 0
finalizar_mover = False
else:
contador_inicio_mover += 1
# Hacer que Donkey Kong tire barriles.
if contador_inicio_imagen_donkey_kong > \
contador_fin_imagen_donkey_kong:
donkey_kong.animar(imagen_donkey_kong)
imagen_donkey_kong += 1
if imagen_donkey_kong > 4:
imagen_donkey_kong = 2
barril_tirado = True
contador_inicio_imagen_donkey_kong = 0
else:
contador_inicio_imagen_donkey_kong += 1
# Comprobar si Donkey Kong ha tirado un barril para crear la
# instancia correspondiente.
if barril_tirado:
barril = Barril(2, 250, 200)
grupo_barriles.add(barril)
barril_tirado = False
# Animar barriles.
# Comprobar si los barriles colisionan con algún tramo de un
# piso.
for barril_arrojado in grupo_barriles:
tramos_colision_2 = pygame.sprite.groupcollide(
grupo_barriles,
grupo_tramos_2,
False, False)
tramos_colision_1 = pygame.sprite.groupcollide(
grupo_barriles,
grupo_tramos_1,
False, False)
if barril_arrojado in tramos_colision_2:
barril_arrojado.caer(3, 0)
elif barril_arrojado in tramos_colision_1:
barril_arrojado.piso = 1
barril_arrojado.caer(-3, 0)
else:
if barril_arrojado.piso == 2:
if barril.rect.center[0] <= 625:
barril_arrojado.caer(3, 1)
else:
barril_arrojado.caer(1, 3)
elif barril_arrojado.piso == 1:
if barril_arrojado.rect.center[0] <= 0:
grupo_barriles.remove(barril_arrojado)
else:
barril_arrojado.caer(-3, 1)
# Actualizar sprites.
grupo_tramos_0.update()
grupo_tramos_1.update()
grupo_tramos_2.update()
grupo_donkey_kong.update()
grupo_barriles.update()
grupo_jumpman.update()
# Copiar la imagen de fondo de la ventana en el canvas que la
# mostrará.
ventana.blit(fondo, (0, 0))
# Textos indicativos de si se ha producido la colisión de los
# rectángulos delimitadores de ambos sprites y de si se ha producido la
# colisión de sus máscaras.
if pygame.sprite.spritecollide(jumpman, grupo_barriles, False):
colision_rect = True
texto_colision_rect = font_emulogic_12.render(
'Colision rectangulos delimitadores.' +
'Jumpman NO MUERE.',
1, (255, 0, 0), (0, 0, 0))
else:
texto_no_colision_rect = font_emulogic_12.render(
'NO han colisionado los rectangulos ' +
'delimitadores.',
1, (34, 177, 76), (0, 0, 0))
if colision_rect:
if contador_inicio_mostrar_mensaje > \
contador_fin_mostrar_mensaje:
contador_inicio_mostrar_mensaje = 0
colision_rect = False
else:
ventana.blit(texto_colision_rect, (35, 340))
contador_inicio_mostrar_mensaje += 1
else:
ventana.blit(texto_no_colision_rect, (35, 340))
if colision_mask:
texto_colision_mask = font_emulogic_12.render(
'Colision mascaras de ambos sprites.' +
'Jumpman MUERE.',
1, (255, 0, 0), (0, 0, 0))
else:
texto_colision_mask = font_emulogic_12.render(
'NO han colisionado las mascaras de ' +
'ambos sprites.',
1, (34, 177, 76), (0, 0, 0))
ventana.blit(texto_colision_mask, (35, 365))
# Dibujar sprites.
grupo_tramos_0.draw(ventana)
grupo_tramos_1.draw(ventana)
grupo_tramos_2.draw(ventana)
grupo_donkey_kong.draw(ventana)
grupo_barriles.draw(ventana)
grupo_jumpman.draw(ventana)
# Mostrar la ventana dibujada (cambiar buffers, buffer pantalla a
# disponible para dibujar y viceversa.
pygame.display.flip()
else:
pygame.time.wait(3000)
canal1.play(sonido_game_over, -1)
imagen_donkey_kong = 5
while True:
# Ejecutar el siguiente frame.
reloj.tick(fps)
# Escuchar cada uno de los eventos de la lista de eventos de
# Pygame.
for evento in pygame.event.get():
# Finalizar el juego cuando se produzca el evento de
# terminación del programa (cierre de la ventana).
if evento.type == pygame.QUIT:
pygame.quit()
return
if contador_inicio_imagen_donkey_kong > \
contador_fin_imagen_donkey_kong:
donkey_kong.animar(imagen_donkey_kong)
imagen_donkey_kong += 1
if imagen_donkey_kong > 6:
imagen_donkey_kong = 5
contador_inicio_imagen_donkey_kong = 0
else:
contador_inicio_imagen_donkey_kong += 1
# Actualizar sprites.
grupo_donkey_kong.update()
# Copiar la imagen de fondo de la ventana en el canvas que la
# mostrará.
ventana.blit(fondo, (0, 0))
# Dibujar sprites.
grupo_tramos_0.draw(ventana)
grupo_tramos_1.draw(ventana)
grupo_tramos_2.draw(ventana)
grupo_donkey_kong.draw(ventana)
grupo_jumpman.draw(ventana)
# Visualizar texto 'Game Over'.
texto_game_over = font_emulogic_48.render('Game Over', 1,
(255, 0, 0), (0, 0, 0))
ventana.blit(texto_game_over, (115, 200))
# Mostrar la ventana dibujada (cambiar buffers, buffer pantalla
# a disponible para dibujar y viceversa.
pygame.display.flip()
pygame.quit()
if __name__ == '__main__':
donkey_kong()
En la ejecución del script que se muestra a continuación podemos observar que en el primer salto no se produce ningún tipo de colisión entre el sprite de Jumpman y el del barril que salta, mientras que en el segundo sólo se produce la colisión de los rectángulos delimitadores de ambos sprites, es decir, no se produce la colisión de sus máscaras (formas), por lo que Jumpan sigue vivo. Finalmente, cuando se encuentra parado, sí se produce la colisión de sus máscaras (lógicamente y de forma previa, también la de sus rectángulos delimitadores), por lo que Jumpman muere.
Quizás también te interese:
Comentarios
Publicar un comentario