pilas-engine

Figuras de colisión

Tengo dos diferentes tipos de casos respecto de la figura de colisión.

  1. El primer caso es la necesidad de agregar más de una figura de colisión. Por el momento lo he solucionado agregando actores invisibles al actor, sin embargo sería más interesante si se pudieran agregar más de una figura de colisión al actor. Tal vez puede hacerse y no sé como.
  2. El segundo caso es la posición de la figura de colisión. Sin importar donde establezca los valores de x e y la figura de colisión siempre se sitúa en el centro del actor y no es siempre lo que se quiere, en muchas ocasiones la figura de colisión no coincide con el centro del sprite. Nuevamente, la “solución” sería crear un actor invisible y posicionarlo en el lugar deseado, pero esto no es muy elegante.

Desde ya, agradezco cualquier esclarecimiento sobre estas dudas!

Hola @jetspydragon!!!

Se pueden agregar varias figuras de colisión por actor, pero el posicionamiento de la figura se tiene que hacer a mano… por ejemplo, acá hice una pequeña demo agregando dos areas de colisión a un actor personalizado: Una figura de colisión rectangular en la cabeza y otra en los pies:

En el código del método “actualizar” vas a notar la parte en la que hago el posicionamiento de la figura de colisión. Fijate que la posición de la caja que va en la cabeza va a tener siempre la misma coordenada del actor, pero 40 píxeles más arriba. Y los pies 40 píxeles más abajo:

import pilasengine

pilas = pilasengine.iniciar()

class UnActor(pilasengine.actores.Actor):

    def iniciar(self):
        self.imagen = "mono.png"
        self.aprender("seguirAlMouse")

        self.figura_de_colision_pie = self.pilas.fisica.Rectangulo(0, 0, 90, 25)
        self.figura_de_colision_pie.dinamica = False
        self.figura_de_colision_pie.sensor = True

        self.figura_de_colision_cabeza = self.pilas.fisica.Rectangulo(0, 0, 50, 15)
        self.figura_de_colision_cabeza.dinamica = False
        self.figura_de_colision_cabeza.sensor = True


    def actualizar(self):
        self.figura_de_colision_pie.x = self.x
        self.figura_de_colision_pie.y = self.y - 40

        self.figura_de_colision_cabeza.x = self.x
        self.figura_de_colision_cabeza.y = self.y + 40


pilas.actores.vincular(UnActor)
pilas.actores.UnActor()

pilas.ejecutar()

Avisame cualquier cosa, no se si comprendí bien tu pregunta.

Exacto @hugoruscitti, esa es la idea, sin embargo no logro entender ahora como chequear las colisiones usando etiquetas. Tengo esto:

(cualquier semejanza con algún juego conocido es mera coincidencia jeje)
El tema es que, por lo que veo, ahora las etiquetas asignadas a los actores no funcionan para detectar la colisión y, por lo que veo, tengo que asignar etiquetas a las figuras de colisión. Pero entonces, ¿como detecto colisión entre actores? Como seguro imaginarás quiero que el jugador no pueda moverse fuera de los límites de la habitación. Si bien hay muchas maneras de hacer lo mismo sin usar los sensores creo que es una buena oportunidad para entender como funciona esto. Utilizar la función colisiona_con no funciona. Entonces, ¿como podría indicar que se detenga si hay contacto entre las figuras de colisión de la habitación y del jugador? Puse las etiquetas “obstaculo” a las cuatro figuras de la habitación y la etiqueta “jugador” a la del jugador. En ambos casos la propiedad figura_de_colision de ambos actores es None ya que las verdaderas figuras están en otras variables.

Te agradeceré cualquier luz que puedas arrojarme sobre esto, gracias!!!

import pilasengine

pilas = pilasengine.iniciar()
pilas.fondos.Color(pilas.colores.negro)

class Room(pilasengine.actores.Actor):
    def iniciar(self):
        self.imagen = 'assets/room.png'
        self.escala = .45
        self.etiquetas.agregar("obstaculo")
        self.figura_de_colision = None
        self.limites = {
            "arriba": self.pilas.fisica.Rectangulo(0, 0, 600, 20, sensor = True, dinamica = False),
            "abajo": self.pilas.fisica.Rectangulo(0, 0, 600, 20, sensor = True, dinamica = False),
            "derecha": self.pilas.fisica.Rectangulo(0, 0, 20, 300, sensor = True, dinamica = False),
            "izquierda": self.pilas.fisica.Rectangulo(0, 0, 20, 300, sensor = True, dinamica = False)
        }
        self.limites["arriba"].etiquetas.agregar("obstaculo")
        self.limites["abajo"].etiquetas.agregar("obstaculo")
        self.limites["derecha"].etiquetas.agregar("obstaculo")
        self.limites["izquierda"].etiquetas.agregar("obstaculo")
    
    def actualizar(self):
        self.limites["arriba"].y = 160
        self.limites["abajo"].y = -160
        self.limites["derecha"].x = 300
        self.limites["izquierda"].x = -300
        
class Isaac(pilasengine.actores.Actor):
    def iniciar(self):
        self.velocidadx = 0
        self.velocidady = 0
        self.imagen = 'assets/Isaac.png'   
        self.escala = .25
        self.etiquetas.agregar('jugador')
        self.figura_de_colision = None
        self.figura_de_colision_pie = pilas.fisica.Rectangulo(0, 0, 30, 10, sensor = True, dinamica = False)
        self.figura_de_colision_pie.etiquetas.agregar("jugador")
        self.pilas.eventos.pulsa_tecla.conectar(self.al_pulsar_tecla)
        self.pilas.eventos.suelta_tecla.conectar(self.al_soltar_tecla)
        self.pilas.colisiones.agregar("jugador", "obstaculo", self.al_tocar_obstaculo) 
    
    def actualizar(self):      
        # Posicion
        self.x += self.velocidadx
        self.y += self.velocidady
        
        # Figura de colision
        self.figura_de_colision_pie.x = self.x
        self.figura_de_colision_pie.y = self.y - 16
    
    def al_pulsar_tecla(self, evento):
        if evento.codigo == pilas.simbolos.DERECHA:
            self.velocidadx = 2
        elif evento.codigo == pilas.simbolos.IZQUIERDA:
            self.velocidadx = -2
        if evento.codigo == pilas.simbolos.ARRIBA:
            self.velocidady = 2
        elif evento.codigo == pilas.simbolos.ABAJO:
            self.velocidady = -2
    
    def al_soltar_tecla(self, evento):
        if evento.codigo == pilas.simbolos.DERECHA:
            self.velocidadx = 0
        elif evento.codigo == pilas.simbolos.IZQUIERDA:
            self.velocidadx = 0
        if evento.codigo == pilas.simbolos.ARRIBA:
            self.velocidady = 0
        elif evento.codigo == pilas.simbolos.ABAJO:
            self.velocidady = 0
    
    def al_tocar_obstaculo(self, jugador, obstaculo):
        print("colision")
        jugador.velocidadx = 0
        jugador.velocidady = 0
        

pilas.actores.vincular(Room)
pilas.actores.vincular(Isaac)
pilas.actores.Room()
pilas.actores.Isaac()
                        
pilas.ejecutar()

Buenisimo @jetspydragon, lo que se me ocurre que te podría servir es usar el atributo “figuras_en_contacto” que tienen las figuras físicas. El atributo “figuras_en_contacto” es una lista que se carga automáticamente cada vez que una figura colisiona con otra.

Por ejemplo, mirando tu código se me ocurrió esto: en el método actualizar el actor se mueve incrementando las posiciones x e y, lo que se podría hacer ahí es revertir el movimiento si el actor ingresa en una colisión con un obstáculo:

import pilasengine

pilas = pilasengine.iniciar()
pilas.fondos.Color(pilas.colores.negro)

class Room(pilasengine.actores.Actor):
    def iniciar(self):
        #self.imagen = 'assets/room.png'
        self.escala = .45
        self.etiquetas.agregar("obstaculo")
        self.figura_de_colision = None
        self.limites = {
            "arriba": self.pilas.fisica.Rectangulo(0, 0, 600, 20, sensor = True, dinamica = False),
            "abajo": self.pilas.fisica.Rectangulo(0, 0, 600, 20, sensor = True, dinamica = False),
            "derecha": self.pilas.fisica.Rectangulo(0, 0, 20, 300, sensor = True, dinamica = False),
            "izquierda": self.pilas.fisica.Rectangulo(0, 0, 20, 300, sensor = True, dinamica = False)
        }
        self.limites["arriba"].etiquetas.agregar("obstaculo")
        self.limites["abajo"].etiquetas.agregar("obstaculo")
        self.limites["derecha"].etiquetas.agregar("obstaculo")
        self.limites["izquierda"].etiquetas.agregar("obstaculo")
    
    def actualizar(self):
        self.limites["arriba"].y = 160
        self.limites["abajo"].y = -160
        self.limites["derecha"].x = 300
        self.limites["izquierda"].x = -300
        
class Isaac(pilasengine.actores.Actor):
    def iniciar(self):
        self.velocidadx = 0
        self.velocidady = 0
        #self.imagen = 'assets/Isaac.png'
        self.imagen = "mono.png"   
        self.escala = .25
        self.etiquetas.agregar('jugador')
        self.figura_de_colision = None
        self.figura_de_colision_pie = pilas.fisica.Rectangulo(0, 0, 30, 10, sensor = True, dinamica = False)
        self.figura_de_colision_pie.etiquetas.agregar("jugador")
        self.pilas.eventos.pulsa_tecla.conectar(self.al_pulsar_tecla)
        self.pilas.eventos.suelta_tecla.conectar(self.al_soltar_tecla)
        #self.pilas.colisiones.agregar("jugador", "obstaculo", self.al_tocar_obstaculo) 
    
    def actualizar(self):      
        # Posicion
        self.x += self.velocidadx
        self.y += self.velocidady
        
        # Figura de colision
        self.figura_de_colision_pie.x = self.x
        self.figura_de_colision_pie.y = self.y - 16
            
        ## Si colisiona con alguna figura
        if self.figura_de_colision_pie.figuras_en_contacto:
            primer_figura = self.figura_de_colision_pie.figuras_en_contacto[0]
            es_obstaculo = 'obstaculo' in primer_figura.etiquetas.lista
            
            if es_obstaculo:
                self.y -= self.velocidady
                self.x -= self.velocidadx
                
    
    def al_pulsar_tecla(self, evento):
        if evento.codigo == pilas.simbolos.DERECHA:
            self.velocidadx = 2
        elif evento.codigo == pilas.simbolos.IZQUIERDA:
            self.velocidadx = -2
        if evento.codigo == pilas.simbolos.ARRIBA:
            self.velocidady = 2
        elif evento.codigo == pilas.simbolos.ABAJO:
            self.velocidady = -2
    
    def al_soltar_tecla(self, evento):
        if evento.codigo == pilas.simbolos.DERECHA:
            self.velocidadx = 0
        elif evento.codigo == pilas.simbolos.IZQUIERDA:
            self.velocidadx = 0
        if evento.codigo == pilas.simbolos.ARRIBA:
            self.velocidady = 0
        elif evento.codigo == pilas.simbolos.ABAJO:
            self.velocidady = 0
    
    #def al_tocar_obstaculo(self, jugador, obstaculo):
    #    print("colision")
    #    jugador.velocidadx = 0
    #    jugador.velocidady = 0
        

pilas.actores.vincular(Room)
pilas.actores.vincular(Isaac)
pilas.actores.Room()
pilas.actores.Isaac()
                        
pilas.ejecutar()

Lo único malo de este enfoque es que el actor queda “bloqueado” al hacer una diagonal. Bah, la colisión le impide ir en diagonal.

Para corregir una opción es separar los obstáculos verticales de los horizontales usando etiquetas, y al momento de detectar una colisión con el obstáculo solo revertir el eje que corresponda.

Mirá esta otra versión, es mucho más interesante el movimiento que hace el personaje cuando entra en contacto con la pared en diagonal:

import pilasengine

pilas = pilasengine.iniciar()
pilas.fondos.Color(pilas.colores.gris)

class Room(pilasengine.actores.Actor):
    def iniciar(self):
        #self.imagen = 'assets/room.png'
        self.escala = .45
        self.etiquetas.agregar("obstaculo")
        self.figura_de_colision = None
        self.limites = {
            "arriba": self.pilas.fisica.Rectangulo(0, 0, 600, 20, sensor = True, dinamica = False),
            "abajo": self.pilas.fisica.Rectangulo(0, 0, 600, 20, sensor = True, dinamica = False),
            "derecha": self.pilas.fisica.Rectangulo(0, 0, 20, 300, sensor = True, dinamica = False),
            "izquierda": self.pilas.fisica.Rectangulo(0, 0, 20, 300, sensor = True, dinamica = False)
        }
        self.limites["arriba"].etiquetas.agregar("obstaculo")
        self.limites["abajo"].etiquetas.agregar("obstaculo")
        self.limites["derecha"].etiquetas.agregar("obstaculo")
        self.limites["izquierda"].etiquetas.agregar("obstaculo")
        
        self.limites["arriba"].etiquetas.agregar("horizontal")
        self.limites["abajo"].etiquetas.agregar("horizontal")
        self.limites["derecha"].etiquetas.agregar("vertical")
        self.limites["izquierda"].etiquetas.agregar("vertical")
    
    def actualizar(self):
        self.limites["arriba"].y = 160
        self.limites["abajo"].y = -160
        self.limites["derecha"].x = 300
        self.limites["izquierda"].x = -300
        
class Isaac(pilasengine.actores.Actor):
    def iniciar(self):
        self.velocidadx = 0
        self.velocidady = 0
        #self.imagen = 'assets/Isaac.png'
        self.imagen = "mono.png"   
        self.escala = .25
        self.etiquetas.agregar('jugador')
        self.figura_de_colision = None
        self.figura_de_colision_pie = pilas.fisica.Rectangulo(0, 0, 30, 10, sensor = True, dinamica = False)
        self.figura_de_colision_pie.etiquetas.agregar("jugador")
        self.pilas.eventos.pulsa_tecla.conectar(self.al_pulsar_tecla)
        self.pilas.eventos.suelta_tecla.conectar(self.al_soltar_tecla)
        #self.pilas.colisiones.agregar("jugador", "obstaculo", self.al_tocar_obstaculo) 
    
    def actualizar(self):      
        # Posicion
        self.x += self.velocidadx
        self.y += self.velocidady
        
        # Figura de colision
        self.figura_de_colision_pie.x = self.x
        self.figura_de_colision_pie.y = self.y - 16
            
        ## Si colisiona con alguna figura
        if self.figura_de_colision_pie.figuras_en_contacto:
            primer_figura = self.figura_de_colision_pie.figuras_en_contacto[0]
            es_obstaculo = 'obstaculo' in primer_figura.etiquetas.lista
            es_vertical = 'vertical' in primer_figura.etiquetas.lista
            es_horizontal = 'horizontal' in primer_figura.etiquetas.lista
            
            if es_obstaculo:
                if es_horizontal:
                    self.y -= self.velocidady
                if es_vertical:
                    self.x -= self.velocidadx
                
    
    def al_pulsar_tecla(self, evento):
        if evento.codigo == pilas.simbolos.DERECHA:
            self.velocidadx = 2
        elif evento.codigo == pilas.simbolos.IZQUIERDA:
            self.velocidadx = -2
        if evento.codigo == pilas.simbolos.ARRIBA:
            self.velocidady = 2
        elif evento.codigo == pilas.simbolos.ABAJO:
            self.velocidady = -2
    
    def al_soltar_tecla(self, evento):
        if evento.codigo == pilas.simbolos.DERECHA:
            self.velocidadx = 0
        elif evento.codigo == pilas.simbolos.IZQUIERDA:
            self.velocidadx = 0
        if evento.codigo == pilas.simbolos.ARRIBA:
            self.velocidady = 0
        elif evento.codigo == pilas.simbolos.ABAJO:
            self.velocidady = 0
    
    #def al_tocar_obstaculo(self, jugador, obstaculo):
    #    print("colision")
    #    jugador.velocidadx = 0
    #    jugador.velocidady = 0
        

pilas.actores.vincular(Room)
pilas.actores.vincular(Isaac)
pilas.actores.Room()
pilas.actores.Isaac()
                        
pilas.ejecutar()

Por último, las zonas en donde se forman esquinas horizontales se produce un efecto raro… porque en ese instante el protagonista va a estar colisionando con dos obstáculos al mismo tiempo. Lo ideal es chequear todas las colisiones juntas, probá con un método actualizar como este si llega a ser un caso para tu juego:

    def actualizar(self):      
        # Posicion
        self.x += self.velocidadx
        self.y += self.velocidady
        
        # Figura de colision
        self.figura_de_colision_pie.x = self.x
        self.figura_de_colision_pie.y = self.y - 16
            
        ## Si colisiona con alguna figura
        if self.figura_de_colision_pie.figuras_en_contacto:
            for figura in self.figura_de_colision_pie.figuras_en_contacto:
                es_obstaculo = 'obstaculo' in figura.etiquetas.lista
                es_vertical = 'vertical' in figura.etiquetas.lista
                es_horizontal = 'horizontal' in figura.etiquetas.lista
            
                if es_obstaculo:
                    if es_horizontal:
                        self.y -= self.velocidady
                    if es_vertical:
                        self.x -= self.velocidadx

Abrazo!!!

PD: como no tenía las imágenes del juego no me quedó otra que volver a poner al actor del monito de nuevo…

PD 2: Ah, por cierto, seguramente te va a servir agregarle una figura de colisión adicional al actor, por si colisiona con otro tipo de objetos. Mirá el ejemplo de plataformas de pilas, es un buen ejemplo de un actor con figuras de colisión mixtas:

Horacio, mil gracias por el tiempo invertido en la respuesta. Ya había visto ese arreglo pero temía, y temo, que en realidad la actualización de figuras_en_colisión se hace al final del frame y no en el momento en que la colisión se produce. Con lo cual si yo muevo la figura de colisión y luego chequeo el arreglo en realidad estoy viendo lo que sucedía con la anterior posición, las figuras de colisión que colisionan con la nueva posición las veré en el siguiente frame. Modifiqué el método actualizar de la siguiente manera y, creo, confirma lo que pienso:

def actualizar(self):      
        # Verifico si la posición actual genera colisiones,
        # si es así vuelvo a la posición anterior
        if not self.sin_obstaculos():
            self.x = self.anterior_x
            self.y = self.anterior_y
        
        # Ajusto posición
        self.anterior_x = self.x
        self.anterior_y = self.y
        self.x += self.velocidadx
        self.y += self.velocidady     
        
        # Ajusto la figura de colisión   
        self.figura_de_colision_pie.x = self.x
        self.figura_de_colision_pie.y = self.y - 16
    
    def sin_obstaculos(self):
        for figura in self.figura_de_colision_pie.figuras_en_contacto:
            if 'obstaculo' in figura.etiquetas.lista:
                return False
        return True

Pienso que lo que resultaría en este caso es poder utilizar un método de tipo late_update que se ejecutara al final del frame, luego que los cálculos de físicas están hechos pero antes que se muestren en pantalla porque en mi solución queda el problema visible de que durante un frame el sprite está en colisión y es al inicio del siguiente frame que se revierte la posición. Es más, si dejo presionada la tecla forzando la colisión se ve claramente el efecto de entrar en colisión y salir de ella al soltar la tecla. Además en mi código no puedo detectar si la colisión es vertical u horizontal (aunque para subsanar esto podría usar un truco similar al tuyo, dando etiquetas “vertical” y “horizontal” adicionales a las figuras que correspondan).

Otra posibilidad sería que se pudiera forzar el llenado del arreglo figuras_en_colision… aunque no creo que esto sea posible pues pienso que todo el cálculo de físicas estará en un bloque aparte que se ejecuta en conjunto al finalizar el frame.

Como ves en mi código utilizo ni más ni menos que el principio básico que me mostrás en los ejemplos, pero lo que no me convence es que el arreglo figuras_en_colision no se actualiza al momento de mover la figura sino que está disponible en el siguiente frame.

Nuevamente, mil gracias por la respuesta y si tenés alguna otra idea o vez algún error en mi razonamiento avisame. Abrazo!

Un comentario más, disculpame que lo haga en una entrada aparte. En el ejemplo que me comentás de la plataforma el sensor del suelo sirve para detectar el suelo, pero no es lo que le impide atravesarlo, al parecer la aceituna tiene otra figura esférica dinámica que es la que realmente colisiona con el suelo y le impide atravesarla, hacer uso de la gravedad, etc. Pero a mi me gustaría manejar este tipo de juego visto desde arriba con sensores porque permite más control, a diferencia de uno de plataformas donde el uso de físicas se hace más necesario.

Abrazo!!

mmmm… es cierto, lo ideal sería poder tener un método como luego_de_actualizar() en el actor, o al menos un evento en el que podamos conectarnos para actualizar luego de todos los cálculos de física…

¿Te serviría si agrego ese arreglo en pilas y armo una versión nueva?

Disculpame que me demoré en contestarte, pero arranca la semana y se complica con el laburo.

Creo que me serviría. Lo que haría es: en actualizar me muevo normalmente y en luego_de_actualizar verifico si hay colisión, si hay colisión entonces usando la geometría de los sensores involucrados me voy moviendo pixel a pixel hasta acercar uno al otro lo máximo posible sin solaparse. Eso sí, luego debería volver a calcularse la física porque estoy movimiendo el sensor.

¿En los fuentes todo esto está hecho en python? ¿Donde debería mirar? Digo, para darte una mano (y de paso mirar un poco el motor por dentro). No es que sea experto, pero dos ojos ven más que uno.

Cualquier cosa avisame! Abrazo!

Hola @jetspydragon!! Por el momento solo armé un issue especificando los cambios que habría que hacer y dónde:

https://github.com/hugoruscitti/pilas/issues/300

Igualmente dame unos días que le comenté a un colaborador nuevo de pilas llamado David Virgolini que vea ese issue primero, si se le llega a complicar o algo ahí te voy a pedir una mano, pero esperemos a que lo pueda resolver david antes, te parece bien?

Genial, dale! Voy siguiendo el issue y a ver como sigue.