Ir al contenido principal

Python + Pygame (XXXI): Tutorial - Colisiones precisas (I)

En este blog ya he puesto algunos tutoriales sobre aspectos que posiblemente tengamos que implementar a la hora de programar un videojuego (arrastrar objetos mediante el ratón, el salto de personajes, etc.), y en este post pongo un pequeño tutorial respecto a quizá el aspecto más frecuente con el que nos encontraremos: las colisiones entre sprites, y, en concreto, me voy a referir a las colisiones precisas entre éstos.

En la mayoría de los videojuegos nos bastará con las colisiones entre los rectángulos que delimitan los sprites, sin mayor precisión, pero en ocasiones necesitaremos la detección de las colisiones respecto a la forma del sprite, es decir, necesitaremos una mayor precisión en las mismas.

En este tutorial voy a tomar como referencia dos de los videojuegos tipo arcade más famosos de todos los tiempos: 'Space invaders' y, al igual que en el último tutorial, 'Donkey Kong'.

Me explico: en muchos juegos la forma de los sprites encaja o difiere poco de una forma rectangular. Este es el caso, por ejemplo, de los sprites de los aliens y del disparo del cañón en el videojuego 'Space Invaders', cuyo desarrollo ya compartí en una entrada de este blog.

Si utilizamos sin más los métodos de detección de colisiones, se detectarían éstas, por ejemplo, en las tres situaciones siguientes:

Como se observa en la figura anterior, el programa considerará como colisiones las que se produzcan entre los rectángulos delimitadores de los sprites, y el disparo del cañon alcanzará al alien en cualquiera de las tres situaciones, aunque en la tercera no le ha alcanzado todavía. En este videojuego esto es poco o nada importante, puesto que en la última de las situaciones, aunque realmente todavía no hay colisión entre las formas de los sprites, esto será prácticamente inapreciable.

Pero pensemos en un videojuego en el que haya sprites cuya forma difiera bastante de un rectángulo:

En este caso, tal y como se puede ver, sólo sería realista la segunda de las situaciones, ya que en las otras dos sería claramente apreciable que el disparo del cañón todavía no ha alcanzado a la bomba.

Pero, vemos un ejemplo que nos servirá para poner scripts que nos permitan apreciar lo anterior: los saltos sobre barriles que 'Jumpman' realiza en 'Donkey Kong':

En la primera de la situaciones anteriores la colisión de los rectángulos delimitadores de los sprites es clara y, por tanto, 'Jumpman' debe perder una vida; cosa que no está tan clara en la segunda, porque, pese a producirse colisión de los rectángulos (incluso antes de lo que muestra la figura), no se ha producido todavía colisión de las formas de los sprites, por lo que 'Jumpman', al encontrarse saltando, aún podría superar al barril y salvar la vida. Por tanto, necesitamos una mayor precisión en las colisiones para poder gestionarlas convenientemente y obtener un efecto realista.

Y esto es precisamente lo que se consigue con el método pygame.sprite.collide_mask(), que verifica si dos sprites han colisionado de manera más precisa, comparando sus formas (máscaras) en lugar de usar las colisiones basadas sólo en los rectángulos delimitadores.

Una máscara es una representación binaria de un sprite, donde cada píxel puede ser transparente u opaco, es decir '0' o '1', y el método pygame.sprite.collide_mask() verifica la colisión entre dos sprites comprobando si hay solapamiento o superposición de bits a '1' de las máscaras correpondientes a ambos (sus formas), en lugar de si hay solapamiento de los rectángulos delimitadores de ambos sprites.

Los pasos a dar para la detección precisa de sprites son los siguientes:

a) Crear el atributo mask de cada uno de los sprites. Esto se realiza utilizando el método pygame.mask.from_surface(surface), que analiza la superficie de la imagen del sprite y crea una máscara basada en los píxeles con una opacidad mayor que cero (los marcará con un '1') y en los píxeles transparentes (los marcará con un '0'). Por tanto, los sprites deben tener canal alfa y ser transparentes en las zonas de la imagen que no se correspondan con una parte de su forma.

b) Verificar las colisiones entre las formas de los sprites utilizando el método pygame.sprite.collide_mask() junto con otro método de colisión de sprites, por ejemplo: sprite.spritecollide, que detecta las colisiones que se producen entre un sprite y todos los incluidos en un grupo.

En el script de ejemplo que pongo a continuación coloco un barril en el centro y 'Jumpman' seguirá al ratón, y de esta forma podremos comprobar las colisiones que se producen cuando 'Jumpman' se acerca al barril.

Al igual que en todos los desarrollos que comparto en este blog, los comentarios incluidos en el código fuente pretenden que éste sea autoexplicativo, es decir, que no se precisen más comentarios sobre su funcionamiento, por lo que en los posts sólo suelo poner comentarios que puedan aportar algo adicional a lo ya explicado en el código fuente, y en este caso sólo incluyo dos comentarios sobre los dos tipos de colisiones empleadas: rectángulos delimitadores y máscaras.

1.- Importar las librerías necesarias:

import pygame
import os

2.- Inicializar pygame, crear ventana e instancias del personaje y del barril:

def donkey_kong():
    # Inicializar módulos internos de pygame.
    pygame.init()
    # 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('Colisiones precisas')
    # 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.png')
    fondo = fondo.convert()
    # Cargar archivo de fuente.
    font_emulogic_12 = pygame.font.Font('recursos/fuentes/emulogic.ttf', 12)
    # Crear instancias de Jumpman y del barril.
    jumpman = Jumpman(0, 0)
    barril = Barril(337, 300)
    # Grupo individual para Jumpman e incluir la instancia creada en él.
    grupo_jumpman = pygame.sprite.GroupSingle()
    grupo_jumpman.add(jumpman)
    # Grupo individual para el barril e incluir la instancia creada en él.
    grupo_barril = pygame.sprite.GroupSingle()
    grupo_barril.add(barril)
    # Ocultar el cursor del ratón.
    pygame.mouse.set_visible(False)
    # Reloj para ejecutar el videojuego.
    reloj = pygame.time.Clock()
    # El juego se ejecutará a 60 frames por segundo.
    fps = 60

3.- Bucle principal del juego:

    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
        # Actualizar sprites.
        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_barril, False):
            texto_colision_rect = font_emulogic_12.render(
                                  'Han colisionado los rectangulos '          +
                                  'delimitadores.',
                                   1, (255, 0, 0), (0, 0, 0))
        else:
            texto_colision_rect = font_emulogic_12.render(
                                  'NO han colisionado los rectangulos '       +
                                  'delimitadores.',
                                   1, (34, 177, 76), (0, 0, 0))
        ventana.blit(texto_colision_rect, (40, 350))
        if pygame.sprite.spritecollide(jumpman, grupo_barril, False,
                                       pygame.sprite.collide_mask):
            texto_colision_mask = font_emulogic_12.render(
                                  'Han colisionado las mascaras de ambos '    +
                                  'sprites.',
                                   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, (40, 375))
        # Dibujar sprites.
        grupo_barril.draw(ventana)
        grupo_jumpman.draw(ventana)
        # Mostrar la ventana dibujada (cambiar buffers, buffer pantalla a
        # disponible para dibujar y viceversa.
        pygame.display.flip()

Como se observa, se utiliza el mismo método para ambos tipos de colisiones:  pygame.sprite.spritecollide(), que detecta colisiones entre un sprite y todos los pertenecientes a un grupo (entre el sprite de 'Jumpman' y los contenidos en el grupo barril, aunque en este caso el grupo es individual y, por tanto, sólo contiene un sprite).

La única diferencia entre la detección de ambos tipos de colisiones, es que en la colisión de máscaras se pasa como argumento el método pygame.sprite.collide_mask al método anterior. 

4.- Clases:

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_saltar_derecha.png') \
                                        .convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
        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 Jumpman mientras sigue al ratón.
        self.pos = pygame.mouse.get_pos()
        self.rect.center = (self.pos)


class Barril(pygame.sprite.Sprite):
    # El barril estará definido por la clase 'Barril', 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/barril_1.png') \
                                                        .convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
        self.mask = pygame.mask.from_surface(self.image)

Al final de los contructores de ambas clases se puede observar la creación del atributo mask de cada uno de los sprites mediante el método pygame.mask.from_surface(surface)

5.- Poniendo todo junto:

#!/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 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_saltar_derecha.png') \
                                        .convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
        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 Jumpman mientras sigue al ratón.
        self.pos = pygame.mouse.get_pos()
        self.rect.center = (self.pos)


class Barril(pygame.sprite.Sprite):
    # El barril estará definido por la clase 'Barril', 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/barril_1.png') \
                                                        .convert_alpha()
        self.rect = self.image.get_rect(center=(self.x, self.y))
        self.mask = pygame.mask.from_surface(self.image)


def donkey_kong():
    # Inicializar módulos internos de pygame.
    pygame.init()
    # 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('Colisiones precisas')
    # 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.png')
    fondo = fondo.convert()
    # Cargar archivo de fuente.
    font_emulogic_12 = pygame.font.Font('recursos/fuentes/emulogic.ttf', 12)
    # Crear instancias de Jumpman y del barril.
    jumpman = Jumpman(0, 0)
    barril = Barril(337, 300)
    # Grupo individual para Jumpman e incluir la instancia creada en él.
    grupo_jumpman = pygame.sprite.GroupSingle()
    grupo_jumpman.add(jumpman)
    # Grupo individual para el barril e incluir la instancia creada en él.
    grupo_barril = pygame.sprite.GroupSingle()
    grupo_barril.add(barril)
    # Ocultar el cursor del ratón.
    pygame.mouse.set_visible(False)
    # 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
        # Actualizar sprites.
        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_barril, False):
            texto_colision_rect = font_emulogic_12.render(
                                  'Han colisionado los rectangulos '          +
                                  'delimitadores.',
                                   1, (255, 0, 0), (0, 0, 0))
        else:
            texto_colision_rect = font_emulogic_12.render(
                                  'NO han colisionado los rectangulos '       +
                                  'delimitadores.',
                                   1, (34, 177, 76), (0, 0, 0))
        ventana.blit(texto_colision_rect, (40, 350))
        if pygame.sprite.spritecollide(jumpman, grupo_barril, False,
                                       pygame.sprite.collide_mask):
            texto_colision_mask = font_emulogic_12.render(
                                  'Han colisionado las mascaras de ambos '    +
                                  'sprites.',
                                   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, (40, 375))
        # Dibujar sprites.
        grupo_barril.draw(ventana)
        grupo_jumpman.draw(ventana)
        # Mostrar la ventana dibujada (cambiar buffers, buffer pantalla a
        # disponible para dibujar y viceversa.
        pygame.display.flip()


if __name__ == '__main__':
    donkey_kong()

Si lo ejecutamos:

- ¿Cuándo utilizar las colisiones precisas?: Cuando se disponga de sprites de forma irregular, para dotar a las colisiones de un mayor realismo.

- ¿Cuándo no utilizar las colisiones precisas?: Aunque es muy preciso, el método pygame.sprite.collide_mask() es más lento que las colisiones basadas en rectángulos, especialmente si se tienen muchos sprites en pantalla o si las imágenes son grandes. Si la precisión no es crucial en el juego, recomiendo usar las colisiones basadas en rectángulos, ya que son mas eficientes.

Y hasta aquí esta primera entrega del tutorial sobre colisiones precisas de sprites. En el segundo y último post sobre este asunto pondré un script de ejemplo basándome en el último script puesto en las entradas relativas al tutorial de implementación del salto de personajes.

Quizás también te interese:

Comentarios

Entradas populares de este blog

Criptografía (I): cifrado Vigenère y criptoanálisis Kasiski

Hace unos días mi amigo Iñaki Regidor ( @Inaki_Regidor ), a quien dedico esta entrada :), compartió en las redes sociales un post titulado "Criptografía: el arte de esconder mensajes"  publicado en uno de los blogs de EiTB . En ese post se explican ciertos métodos clásicos para cifrar mensajes , entre ellos el cifrado de Vigenère , y , al final del mismo, se propone un reto consistente en descifrar un mensaje , lo que me ha animado a escribir este post sobre el método Kasiski  para atacar un cifrado polialfabético ( conociendo la clave descifrar el mensaje es muy fácil, pero lo que contaré en este post es la forma de hacerlo sin saberla ). El mensaje a descifrar es el siguiente: LNUDVMUYRMUDVLLPXAFZUEFAIOVWVMUOVMUEVMUEZCUDVSYWCIVCFGUCUNYCGALLGRCYTIJTRNNPJQOPJEMZITYLIAYYKRYEFDUDCAMAVRMZEAMBLEXPJCCQIEHPJTYXVNMLAEZTIMUOFRUFC Como ya he dicho el método de Vigenère es un sistema de sustitución polialfabético , lo que significa que, al contrario que en un sistema...

¿Qué significa el emblema de la profesión informática? (I)

Todas o muchas profesiones tienen un emblema que las representa simbólicamente y en el caso de la  informática: " es el establecido en la resolución de 11 de noviembre de 1977  para las titulaciones universitarias superiores de informática, y  está constituido por una figura representando en su parte central  un  núcleo toroidal de ferrita , atravesado por  hilos de lectura,  escritura e inhibición . El núcleo está rodeado por  dos ramas : una  de  laurel , como símbolo de recompensa, y la otra, de  olivo , como  símbolo de sabiduría. La  corona  será la  de la casa real  española,  y bajo el escudo se inscribirá el acrónimo de la organización. ". Veamos los diferentes elementos tomando como ejemplo el emblema del COIIE/EIIEO (Colegio Oficial de Ingenieros en Informática del País Vasco/ Euskadiko Informatikako Ingeniarien Elkargo Ofiziala ) . Pero no sólo el COIIE/EIIEO adopta el emblem...

Criptografía (XXIII): cifrado de Hill (I)

En este post me propongo explicar de forma comprensible lo que he entendido sobre el cifrado de Hill , propuesto por el matemático Lester S. Hill , en 1929, y que se basa en emplear una matriz como clave  para cifrar un texto en claro y su inversa para descifrar el criptograma correspondiente . Hay tres cosas que me gustan de la criptografía clásica, además de que considero que ésta es muy didáctica a la hora de comprender los sistemas criptográficos modernos: la primera de ellas es que me "obliga" a repasar conceptos de matemáticas aprendidos hace mucho tiempo y, desgraciadamente, olvidados también hace demasiado tiempo, y, por consiguiente, que, como dice  Dani , amigo y coautor de este blog, me "obliga" a hacer "gimnasia mental"; la segunda es que, en la mayoría de las ocasiones, pueden cifrarse y descifrase los mensajes, e incluso realizarse el criptoanálisis de los criptogramas, sin más que un simple lápiz y papel, es decir, para mi es como un pasat...