Search code examples
pythonpygamepygame-surface

Collision detection between pygame.Surface and mouse not working


I am trying to make a canvas for pixel art.

class Canvas:
    def __init__(self):
        self.__blocks = []
        self.__positions = []
        for i in range(1830):
            self.__blocks.append(pygame.Surface((20, 20)).convert())
        for y in range(30):
            y *= 20
            for x in range(61):
                x = x* 20
                self.__positions.append([x, y])
        self.__color = False

    def draw(self, window):
        for i in range(1830):
            self.__color = not self.__color
            if self.__color:
                self.__blocks[i].fill((200, 200, 200))
            else:
                self.__blocks[i].fill((50, 50, 50))
            window.blit(self.__blocks[i], (self.__positions[i][0]
                                , self.__positions[i][1]))

Here I am trying to generate and draw 1830 unique surfaces and this works. I then tried implementing collision detection between each block and the mouse and failed.

def collided(self, pos):
      for i in range(1380):
          block = self.__blocks[i].get_rect()
          if block.collidepoint(pos[0], pos[1]):
              print(block.x, block.y)

Then I did different tests on why it might be failing. Here is one of them. I will change a single block's color, in our case the 10th block self.__blocks[10].fill((255, 0, 0)) to red so we know which box to click on. Then we will try to check for collision for that particular block.

def testBlock(self, pos):
    block = self.__blocks[10].get_rect()
    if block.collidepoint(pos[0], pos[1]):
        print(block.x)

And it doesn't work, but the weird thing is it works for the first block(in the 0th index) and only the first block no matter which surface I test. Any idea on how to fix this would be appreciated. The following is copy and paste code.

import pygame
pygame.init()

win = pygame.display
D = win.set_mode((1220, 600))

class Canvas:
    def __init__(self):
        self.__blocks = []
        self.__positions = []
        for i in range(1830):
            self.__blocks.append(pygame.Surface((20, 20)).convert())
        for y in range(30):
            y *= 20
            for x in range(61):
                x = x* 20
                self.__positions.append([x, y])
        self.__color = False
        self.testBlock = 10

    def draw(self, window):
        for i in range(1830):
            self.__color = not self.__color
            if self.__color:
                self.__blocks[i].fill((200, 200, 200))
            else:
                self.__blocks[i].fill((50, 50, 50))
            self.__blocks[self.testBlock].fill((255, 0, 0)) # Changing the color for testing 
                                                
            window.blit(self.__blocks[i], (self.__positions[i][0]
                                , self.__positions[i][1]))


    def test(self, pos):
        block = self.__blocks[self.testBlock].get_rect()
        if block.collidepoint(pos[0], pos[1]):
            print(block.x, block.y)


canvas = Canvas()
while True:
    D.fill((0, 0, 0))
    pygame.event.get()
    mousepos = pygame.mouse.get_pos()
    canvas.draw(D)
    canvas.test(mousepos)
    win.flip()

Solution

  • .get_rect() gives rect with block's size but with position (0, 0)

    you have real position in __positions and you would need

      .get_rect(topleft=self.__positions[self.testBlock])
    

    def test(self, pos):
            block = self.__blocks[self.testBlock].get_rect(topleft=self.__positions[self.testBlock])
            if block.collidepoint(pos[0], pos[1]):
                print(block.x, block.y)
    

    But it would be better to get rect and set its position at start and later not use get_rect().

    You could also create class Pixel similar to class Sprite with self.image to keep surface and self.rect to keep its size and position. And then you could use Group to check collision with all pixels.


    EDIT:

    Example which uses class pygame.sprite.Sprite to create class Pixel and it keeps all pixels in pygame.sprite.Group

    It also handle events (MOUSEBUTTONDOWN) to change color in any pixel when it is clicked.

    enter image description here

    import pygame
    
    # --- classes ---
    
    class Pixel(pygame.sprite.Sprite):
        
        def __init__(self, x, y, color, width=20, height=20):
            super().__init__()
            self.color_original = color
            
            self.color = color
    
            self.image = pygame.Surface((20, 20)).convert()
            self.image.fill(self.color)
    
            self.rect = pygame.Rect(x, y, width, height)
            
        def handle_event(self, event):
            if event.type == pygame.MOUSEBUTTONDOWN:
    
                if self.rect.collidepoint(event.pos):
                    if self.color != self.color_original:
                        self.color = self.color_original
                    else:
                        self.color = (255,0,0)
                    self.image.fill(self.color)
                    # event handled
                    return True
                
            # event not handled
            return False
             
    class Canvas:
        
        def __init__(self):
            # create group for sprites
            self.__blocks = pygame.sprite.Group()
    
            # create sprites
            self.__color = False
        
            for y in range(30):
                y *= 20
                for x in range(61):
                    x *= 20
                    self.__color = not self.__color
                    if self.__color:
                        color = (200, 200, 200)
                    else:
                        color = (50, 50, 50)
                    self.__blocks.add(Pixel(x, y, color))
                        
            # changing the color for testing 
            self.testBlock = 10
            
            all_sprites = self.__blocks.sprites()
            block = all_sprites[self.testBlock]
            
            block.image.fill((255, 0, 0))
    
        def draw(self, window):
            # draw all sprites in group
            self.__blocks.draw(window)
    
        def test(self, pos):
            # test collision with one sprite
            all_sprites = self.__blocks.sprites()
            block = all_sprites[self.testBlock]
            if block.rect.collidepoint(pos):
                print(block.rect.x, block.rect.y)
                
        def handle_event(self, event):
            for item in self.__blocks:
                if item.handle_event(event):
                    # don't check other pixels if event already handled
                    return True
                
    # --- main ---
    
    pygame.init()
    
    win = pygame.display
    D = win.set_mode((1220, 600))
    
    canvas = Canvas()
    while True:
    
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                exit()
            canvas.handle_event(event)
            
        #mousepos = pygame.mouse.get_pos()
        #canvas.test(mousepos)
    
        # draws (without updates, etc)
        #D.fill((0, 0, 0)) # no need clean screen if it will draw all elements again
        canvas.draw(D)
        win.flip()