For Loop bloqueando otro For Loop

Nov 29 2020

Estoy revisando missileGroup para ver si alguna instancia de misil colisionó con alguna instancia enemiga en enemigoGroup. Cuando se ejecuta, imprime "Hit" para el primer ciclo, pero ignora el segundo ciclo for. ¿Porqué es eso?

 #### Imagine this is in a game loop ####
    for missile in missileGroup:
        if pygame.sprite.spritecollide(missile, enemyGroup, False) :
            print("Hit")

    
    for enemy in enemyGroup:
        if pygame.sprite.spritecollide(enemy, missileGroup, False):
            print("HI")

Actualización : @ Rabbid76 declaró que spritecollideno funcionaría porque spriteGroup enemyGroupes una lista de sprites dentro de un grupo (EnemyGroup <- EnemyList <- Enemy (sprite)) en lugar de un grupo de sprites (EnemyGroup <- Enemy (sprite)). ¿Cómo accedería a eso?

La actualización 2 @paxdiablo indicó que el primer ciclo tal vez vaciará el grupo después de iterar. Cambié los lugares de los bucles y el segundo bucle se ejecutó, mientras que el primero no.

Actualización 3 En el código completo, .reset()se ejecuta el método .kill()que elimina el objeto del grupo. Dado que el primer ciclo elimina el sprite del misil antes de que el segundo ciclo no pudiera detectar ninguna colisión:

for missile in missileGroup:
    if pygame.sprite.spritecollide(missile, enemyGroup, False) :
        missile.reset()

for eachEnemy in enemyGroup: 
        if pygame.sprite.spritecollide(eachEnemy, missileGroup, False):
            eachEnemy.reset()

Respuestas

Rabbid76 Nov 29 2020 at 21:21

Ver pygame.sprite.spritecollide():

Devuelve una lista que contiene todos los Sprites de un grupo que se cruzan con otro Sprite.

Por lo tanto, los argumentos de spritecollide()deben ser un pygame.sprite.Spriteobjeto y un pygame.sprite.Groupobjeto.
Una lista de pygame.sprite.Spriteobjetos en lugar del Grupo no funciona.

missileGroup = pygame.sprite.Group()
enemyGroup = pygame.sprite.Group()
for missile in missileGroup:
    if pygame.sprite.spritecollide(missile, enemyGroup, False):
        print("Hit")

for enemy in enemyGroup:
    if pygame.sprite.spritecollide(enemy, missileGroup, False):
        print("HI")

Además lea sobre kill()

El Sprite se elimina de todos los Grupos que lo contienen.

Por lo tanto, si llama kill()en el primer ciclo, el segundo ciclo no funcionará, porque el sprite se elimina de todos los grupos.

Llamas kill()a los resetmétodos. missile.reset()respectivamente, eachEnemy.reset()hace que falle el segundo bucle.

paxdiablo Nov 29 2020 at 07:26

No hay una razón obvia, basada en la información proporcionada (a) , por la que la segunda verificación de colisión deba fallar. Si hay una colisión entre (por ejemplo) enemigo # 7 y # 3 misiles, no debería también ser una colisión entre misil enemigo # 3 y # 7.

No está utilizando ningún elemento de caso de borde como proporcionar su propia función de colisión (posiblemente no conmutativa), por lo que simplemente usará el rectángulo de sprite para detectar esto.

Tendría curiosidad por ver el comportamiento cuando inviertes el orden de los dos bucles en el código.


Además, debe especificar los tipos de esas variables de grupo. Si enemyGroupfuera algo así como un generador agotable en lugar de una lista, sería "vaciado" por el primer ciclo y luego el segundo ciclo no tendría elementos sobre los que iterar (b) (la spritecollidellamada iterará sobre el grupo para verificar cada elemento contra el sprite).

Esa es la única forma, salvo un error en spritecollidesí mismo, de ver los efectos que está describiendo.


A modo de ejemplo, aquí hay un fragmento de código que intenta iterar sobre un generador dos veces:

class gen3(object):
    def __init__(self): self._num = 0
    def __iter__(self): return self
    def __next__(self):
        if self._num == 3: raise StopIteration()
        self._num += 1
        return self._num - 1

gen = gen3()
print("A: ")
for i in gen: print(" ", i)
print("B: ")
for i in gen: print(" ", i)

La salida muestra que el segundo ciclo no hace nada:

A:
  0
  1
  2
B:

Por último, una forma definitiva de comprobar el estado de los grupos es simplemente poner el siguiente código antes de cada bucle:

print("loop X enemy  ", len(enemyGroup),   enemyGroup)
print("loop X missile", len(missileGroup), missileGroup)

utilizando un valor adecuado de Xpara distinguir entre los dos bucles.


(a) Por supuesto, siempre existe la posibilidad de que la información que ha proporcionado no sea del todo precisa o completa (no se supone ninguna intención malintencionada, pero a veces las personas se saltan sin darse cuenta detalles que consideran sin importancia, que terminan siendo muy importantes).

Ejemplo: puede haber algo entre esos dos bucles que está causando el problema. Preferiría darles a las personas el beneficio de la duda, pero probablemente debería informarnos si ese es el caso.


(b) En realidad, se vaciaría con la primera iteración del primer bucle, por lo que probablemente solo coincidiría con el primer misil.

Kingsley Nov 29 2020 at 07:51

Aquí hay un ejemplo rápido que muestra (en PyGame 1.9.6) que este comportamiento informado no ocurre.

El ejemplo crea dos grupos de sprites , luego los colisiona exactamente de la misma manera que el código de ejemplo del OP.

Además de imprimir, los sprites cambian de contorno -> relleno, dependiendo de si creen que han participado en una colisión. Hay un mapeo 1: 1 de un Enemigo chocando con un Misil, y viceversa.

Disculpas por la baja velocidad de fotogramas ...

import pygame
import random

# Window size
WINDOW_WIDTH    = 800
WINDOW_HEIGHT   = 800

DARK_BLUE = (   3,   5,  54 )
RED       = ( 200,   0,   0 )
YELLOW    = ( 240, 250,   0 )
BLACK     = (   0,   0,   0 )
GREY      = ( 200, 200, 200 )
GREEN     = ( 250,   0,   0 )
TRANSPARENT=( 0,0,0,0 )


class Shape(pygame.sprite.Sprite):
    def __init__(self, width=48, height=48):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface( ( width, height ), pygame.SRCALPHA)
        self.rect  = self.image.get_rect()
        # Start position is randomly across the screen, and a little off the top
        self.rect.center = ( random.randrange( 0, WINDOW_WIDTH ), random.randrange( 0, WINDOW_HEIGHT ) )
        # Movement
        self.dx = random.randrange( -2, 2 )
        self.dy = random.randrange( -2, 2 )
        # Looks like
        self.filled = 2
        self.last_fill = -1
        self.render()

    def setFilled( self, value ):
        if ( value == True ):
            self.filled = 0
        else:
            self.filled = 2

    def update( self ):
        if ( self.last_fill != self.filled ):
            self.last_fill = self.filled
            self.render()

        self.rect.move_ip( self.dx, self.dy )

        if ( self.rect.left > WINDOW_WIDTH ):
            self.rect.x = -self.rect.width
        elif ( self.rect.right < 0 ):
            self.rect.left = WINDOW_WIDTH
        if ( self.rect.y > WINDOW_HEIGHT ):
            self.rect.y = 0
        elif ( self.rect.y < 0 ):
            self.rect.y = WINDOW_HEIGHT


class Square( Shape ):
    def render( self ):
        # Something to draw

        if ( self.filled == 0 ):
            self.image.fill( RED )
        else:
            border=3
            x, y = border, border
            width = self.rect.width - border -1
            height = self.rect.height - border -1
            self.image.fill( TRANSPARENT )
            pygame.draw.rect( self.image, RED, (x,y,width,height), self.filled )

class Circle( Shape ):
    def render( self ):
        self.image.fill( TRANSPARENT )
        pygame.draw.circle( self.image, YELLOW, (self.rect.width//2, self.rect.height//2), self.rect.width//2, self.filled )




### initialisation
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ) )

### Some sprite groups
missileGroup = pygame.sprite.Group()
for i in range( 3 ):
    new_missile = Circle()
    new_missile.render()
    missileGroup.add( Circle() )

enemyGroup = pygame.sprite.Group()
for i in range( 12 ):
    new_enemy = Square()
    new_enemy.render()
    enemyGroup.add( Square() )


### Main Loop
clock = pygame.time.Clock()
done = False
while not done:

    # Handle user-input
    for event in pygame.event.get():
        if ( event.type == pygame.QUIT ):
            done = True
        elif ( event.type == pygame.MOUSEBUTTONUP ):
            # On mouse-click
            pass
    # Move * collide the sprites
    missileGroup.update()
    enemyGroup.update()

    # Test Collisions
    for missile in missileGroup:
        if pygame.sprite.spritecollide(missile, enemyGroup, False) :
            print("Missile " + str(missile) + " Hits Enemy")
            missile.setFilled( True )
        else:
            missile.setFilled( False )

    for enemy in enemyGroup:
        if pygame.sprite.spritecollide(enemy, missileGroup, False):
            print("Enemy  " + str(enemy) + " Hits Missile")
            enemy.setFilled( True )
        else:
            enemy.setFilled( False )


    # Paint the window, but not more than 60fps
    window.fill( DARK_BLUE )
    enemyGroup.draw( window )
    missileGroup.draw( window )
    pygame.display.flip()


    # Clamp FPS
    clock.tick(60)


pygame.quit()