0
\$\begingroup\$

I am trying to work out how to detect and resolve collisions between moving sprites in pygame, I followed the logic of this video: https://www.youtube.com/watch?v=VpSWuywFlC8

The basic idea is that I track the current rectangle position as well as the previous rectangle position for each sprite and use that to detect where collisions have come from.

For example, to check if the player has collided at the bottom I use: player.rect.bottom >= sprite.rect.top and player.old_rect.bottom < sprite.old_rect.top that way I know that the player's bottom is in the other sprite and that it was above it in the previous frame.

The problem I have is that the collision detection only works sporadically and thus also breaks the collision resolution. Although, if I only look at a collision on a single side it does work. I am just not sure what the problem is with the collision detection. So any help how to fix the collisions method would be greatly appreciated.

(Also, any resource on collision detection for moving rectangles/objects would be really appreciated. I struggle to find good resources on it)

Code so far:

import pygame, sys

class StaticObstacle(pygame.sprite.Sprite):
    def __init__(self,pos,size,groups):
        super().__init__(groups)
        self.image = pygame.Surface(size)
        self.image.fill('yellow')
        self.rect = self.image.get_rect(topleft = pos)
        self.old_rect = self.rect

class MovingVerticalObstacle(StaticObstacle):
    def __init__(self,pos,size,groups):
        super().__init__(pos, size, groups)
        self.image.fill('green')
        self.direction = pygame.math.Vector2((0,1))
        self.speed = 8

    def update(self):
        self.old_rect = self.rect.copy()
        
        if self.rect.bottom >= 600 or self.rect.top <= 120:
            self.direction.y *= -1

        self.rect.y += self.direction.y * self.speed

class MovingHorizontalObstacle(StaticObstacle):
    def __init__(self,pos,size,groups):
        super().__init__(pos, size, groups)
        self.image.fill('purple')
        self.direction = pygame.math.Vector2((1,0))
        self.speed = 6

    def update(self):
        self.old_rect = self.rect.copy()
        if self.rect.right >= 1000 or self.rect.left <= 600:
            self.direction.x *= -1
 
        self.rect.x += self.direction.x * self.speed 

class Player(pygame.sprite.Sprite):
    def __init__(self,groups,obstacles):
        super().__init__(groups)
        
        # image
        self.image = pygame.Surface((30,60))
        self.image.fill('blue')
        
        # position
        self.rect = self.image.get_rect(topleft = (640,360))
        self.old_rect = self.rect.copy()
        
        # movement
        self.direction = pygame.math.Vector2()
        self.speed = 5
        self.obstacles = obstacles

    def input(self):
        keys = pygame.key.get_pressed()

        # movement input
        if keys[pygame.K_UP]: 
            self.direction.y = -1
        elif keys[pygame.K_DOWN]: 
            self.direction.y = 1
        else: 
            self.direction.y = 0

        if keys[pygame.K_RIGHT]: 
            self.direction.x = 1
        elif keys[pygame.K_LEFT]: 
            self.direction.x = -1
        else: 
            self.direction.x = 0

    def collisions(self):
        collision_sprites = pygame.sprite.spritecollide(self,self.obstacles,False)
        if collision_sprites:
            for sprite in collision_sprites:
                if self.rect.top <= sprite.rect.bottom and self.old_rect.top > sprite.old_rect.bottom:
                    print('top collision')
                    self.rect.top = sprite.rect.bottom
                elif self.rect.bottom >= sprite.rect.top and self.old_rect.bottom < sprite.old_rect.top:
                    print('bottom collision')
                    self.rect.bottom = sprite.rect.top
                elif self.rect.right >= sprite.rect.left and self.old_rect.right < sprite.old_rect.left:
                    print('right collision')
                    self.rect.right = sprite.rect.left
                elif self.rect.left >= sprite.rect.right and self.old_rect.left < sprite.old_rect.right:
                    print('left collision')
                    self.rect.left = sprite.rect.right

    def update(self):
        self.old_rect = self.rect.copy()
        self.input()
        
        self.rect.topleft += self.direction * self.speed 
        self.collisions()

# general setup
pygame.init()
screen = pygame.display.set_mode((1280,720))
clock = pygame.time.Clock()

# group setup
all_sprites = pygame.sprite.Group()
collision_sprites = pygame.sprite.Group()

# sprite setup
StaticObstacle((100,300),(100,50),[all_sprites,collision_sprites])
StaticObstacle((800,600),(100,200),[all_sprites,collision_sprites])
StaticObstacle((900,200),(200,10),[all_sprites,collision_sprites])
MovingVerticalObstacle((200,300),(200,60),[all_sprites,collision_sprites])
MovingHorizontalObstacle((850,350),(100,100),[all_sprites,collision_sprites])
Player(all_sprites,collision_sprites)

# loop
while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    screen.fill('black')
    all_sprites.update()
    all_sprites.draw(screen)

    pygame.display.update()
    clock.tick(60)
\$\endgroup\$
2
  • \$\begingroup\$ Are you willing to detect collision between moving rectangles only with that method or you don't mind if there is another one that may do the work for you ? \$\endgroup\$
    – Salem
    Mar 15, 2022 at 10:44
  • 1
    \$\begingroup\$ I'd be happy with any method that works! :) \$\endgroup\$ Mar 15, 2022 at 11:07

1 Answer 1

1
\$\begingroup\$

You can use masks, they provide a pixel perfect collision and they're easy to use with both moving and stationary objects.

You can check this post for more information, but here is the method briefly :

First of all, you need to make a mask object for the objects you want to check collision between and there are 2 possibilities for each one:

I. If your object is a rect object, then you need pygame.mask.Mask() to create the mask object:

# In the constructor
self.mask = pygame.mask.Mask((self.width, self.height), True)

II. If your object is an image, simply use the pygame.mask.from_surface() method:

# In the constructor
self.mask = pygame.mask.from_surface(self.image)

Once you've done that, make a function that calculates the offset (You can put it in your self made classes if you want, as you'll need to call it everytime you perform a mask collision detection) :

def offset(mask1, mask2):
    return int(mask2.x - mask1.x), int(mask2.y - mask1.y)

Now, in the collision detection section, just check if the two masks collide with each other :

if obj1.mask.overlap(obj2.mask, offset(obj1, obj2)):
    print('Collision !!!')

Btw, if you use this overlap method for the obj1, then obj1 must be the first parameter of the offset function and vice versa (as seen in the code snippet)

IMPORTANT NOTE :

If any of your objects has multiple sprites, then you need to make a method for its mask object because it will vary as the current sprite varies :

@property
def mask(self):
    return pygame.mask.from_surface(self.sprites[self.current_sprite]))
\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .