For Loop bloqueando outro For Loop

Nov 29 2020

Estou verificando o missileGroup para ver se alguma instância de míssil colidiu com alguma instância inimiga no grupo inimigo. Quando executado, ele imprime "Hit" para o primeiro loop, mas ignora o segundo loop for. Por que é que?

 #### 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")

Atualização : @ Rabbid76 afirmou que spritecollidenão funcionaria porque o spriteGroup enemyGroupé uma lista de sprites dentro de um grupo (inimigoGrupo <- inimigoList <- Inimigo (sprite)) ao invés de um grupo de sprites (inimigoGrupo <- Inimigo (sprite)). Como eu acessaria isso?

A atualização 2 @paxdiablo afirmou que o primeiro loop pode esvaziar o grupo após a iteração. Troquei os lugares dos loops e o 2º loop funcionou, enquanto o 1º não.

Atualização 3 No código completo, .reset()é executado o método .kill()que remove o sprite do grupo. Uma vez que o primeiro loop remove o sprite do míssil antes que o segundo loop não possa detectar nenhuma colisão:

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()

Respostas

Rabbid76 Nov 29 2020 at 21:21

Veja pygame.sprite.spritecollide():

Retorne uma lista contendo todos os Sprites em um Grupo que se cruzam com outro Sprite.

Portanto, os argumentos para spritecollide()devem ser um pygame.sprite.Spriteobjeto e um pygame.sprite.Groupobjeto.
Uma lista de pygame.sprite.Spriteobjetos em vez do Grupo não 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")

Além disso, leia sobre kill()

O Sprite é removido de todos os Grupos que o contêm.

Portanto, se você chamar kill()no 1º loop, o 2º loop não funcionará, pois o sprite é removido de todos os Grupos.

Você chama kill()os resetmétodos. missile.reset()respectivamente, eachEnemy.reset()faz com que o 2º loop falhe.

paxdiablo Nov 29 2020 at 07:26

Não há nenhuma razão óbvia, com base nas informações fornecidas (a) , pela qual a segunda verificação de colisão deva falhar. Se houver uma colisão entre (por exemplo) o inimigo # 7 e o míssil # 3, também deve haver uma colisão entre o míssil # 3 e o inimigo # 7.

Você não está usando nenhum recurso de caso extremo, como fornecer sua própria função de colisão (possivelmente não comutativa), então ele simplesmente usará o retângulo sprite para detectar isso.

Eu ficaria curioso para ver o comportamento quando você inverte a ordem dos dois loops no código.


Além disso, você deve especificar os tipos dessas variáveis ​​de grupo. Se enemyGroupfosse algo como um gerador esgotável em vez de uma lista, seria "esvaziado" pelo primeiro loop e, em seguida, o segundo loop não teria itens para iterar (b) (a spritecollidechamada será iterada sobre o grupo para verificar cada item contra o sprite).

Essa é praticamente a única maneira, exceto um bug em spritecollidesi, de ver os efeitos que está descrevendo.


A título de exemplo, aqui está um trecho de código que tenta iterar em um gerador duas vezes:

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)

A saída mostra que o segundo loop não faz nada:

A:
  0
  1
  2
B:

Por último, uma forma definitiva de verificar o estado dos grupos é simplesmente colocar o seguinte código antes de cada loop:

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

usando um valor adequado de Xpara distinguir entre os dois loops.


(a) Claro, sempre há a possibilidade de que a informação que você forneceu não seja totalmente precisa ou completa (nenhuma intenção maliciosa é suposta, mas às vezes as pessoas inadvertidamente ignoram o que consideram detalhes sem importância, que acaba sendo muito importante).

Exemplo: pode haver algo acontecendo entre esses dois loops que está causando o problema. Eu prefiro dar às pessoas o benefício da dúvida, mas você provavelmente deve nos informar se for o caso.


(b) Na verdade, ele seria esvaziado pela primeira iteração do primeiro loop, de modo que você descobriria que provavelmente só corresponderia ao primeiro míssil.

Kingsley Nov 29 2020 at 07:51

Aqui está um exemplo rápido que mostra (no PyGame 1.9.6) que esse comportamento relatado não acontece.

O exemplo cria dois grupos de sprites e , em seguida, os colide exatamente da mesma maneira que o código de exemplo do OP.

Além da impressão, os sprites mudam de contorno -> preenchido, dependendo se eles acreditam que participaram de uma colisão. Há um mapeamento 1: 1 de um inimigo colidindo com um míssil e vice-versa.

Desculpas pelo fraco frame-rate ...

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()