2

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()
Qiu YU
  • 517
  • 4
  • 20
  • 1
    in `__blocks` you have only surface which has only `size` and and `get_rect()` gives you always rectangle with position `(0,0)` but real position you have in `__positions` - you should keep both in `pygame.Rect` and check collision with this rect. maybe better create class `Sprite` with `self.image` to keep surface and `self.rect` to keep size and position. – furas Aug 15 '20 at 21:15

2 Answers2

2

When you call .get_rect() on a Surface, it does not know its current position, because that is not Surface information. So you need to assign the location to the Rect before collision detection.

With your current code layout, you could do this during the construction. With the Canvass blocks position now held in the __rects list, the __positions list becomes superfluous.

class Canvass:
    
    def __init__(self):
        self.__blocks    = []
        self.__rects     = []

        for y in range( 30 ):
            for x in range( 61 ):
                self.__blocks.append(pygame.Surface((20, 20)).convert())
                self.__rects.append( self.__blocks[-1].get_rect() )
                self.__rects[-1].topleft = ( x, y )

        self.__color = False
        self.testBlock = 10

This gives you a simple test:

def collided(self, pos):
    hit = False
    for i in range( len( self.__rects ) ):
        if ( self.__rects[i].collidepoint( pos[0], pos[1] ) ):
            print( "Click on block %d" % ( i ) )
            hit = True
            break
    return hit, i
    
Kingsley
  • 14,398
  • 5
  • 31
  • 53
0

.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()
furas
  • 134,197
  • 12
  • 106
  • 148
  • I added example with `Sprite`, `Group` and `MOUSEBUTTONDOWN` to change any block when it clicked. – furas Aug 15 '20 at 22:29