pygamegame-developmenttileisometric

How to get tile selected with mouse?


New to pygame, and game development in general.

This is my main loop and I am trying to blit just a tile selector image on top of the current tile that the mouse is pointing to by using collisionpoint detection. However, as seen in the picture, it partially select everything around the tile I am pointing at. Attached pics are examples of what happens and the selector tile I am using. I am not sure how to adjust the mouse coordinates appropriately and would love advice on what to do with it.

image_list[3] points to the image class that contains details about the selector tile.

def loop(display, screen, image_list, map_data):

# initialise conditions used in loop
display_mouse_coords = False
player_in_grasslands = True

font = pygame.font.Font(pygame.font.get_default_font(), 13)
while True:
    display.fill(constants.COLOURS["BLACK"])
    
    # display map
    for y, row in enumerate(map_data):
        for x, tile in enumerate(row):
            if tile != 0:
                tile_rect = display.blit(image_list[tile - 1].img_surf, (150 + x * 10 - y * 10, 100 + x * 5 + y * 5))
                pos = pygame.mouse.get_pos()
                
                # take the mouse position and scale it, too
                screen_rect = screen.get_rect()
                display_rect = display.get_rect()
                ratio_x = (screen_rect.width / display_rect.width)
                ratio_y = (screen_rect.height / display_rect.height)
                scaled_pos = (pos[0] / ratio_x, pos[1] / ratio_y)
                
                if tile_rect.collidepoint(scaled_pos):
                    display.blit(image_list[3].img_surf, tile_rect)

Selector tile

What the game looks like


Solution

  • Here are the textures that I used for this example (respectively cursor.png, right.png, top.png, left.png:

    You can use the code below for a working example of pygame.mask:

    import pygame
    from pygame.locals import *
    
    class Map:
        def __init__(self, width, height, origin):
            self.top_image = pygame.image.load('top.png')
            self.left_image = pygame.image.load('left.png')
            self.right_image = pygame.image.load('right.png')
            self.cursor = pygame.image.load('cursor.png')
            # create the Mask for the top image
            # (only activate when the selected pixel is non-transparent)
            self.top_image_mask = pygame.mask.from_surface(self.top_image)
    
            origin[0] -= 20*width
            origin[1] += 5*height
            self.map = []
            for x in range(width):
                for y in range(height):
                    tile_x, tile_y = origin
                    tile_x += 20*x + 20*y
                    tile_y += 10*y - 10*x
                    self.map.append(Tile(tile_x, tile_y))
                    # draw the sides textures if needed
                    if not x:
                        self.map[-1].left = True
                    if y+1 == height:
                        self.map[-1].right = True
    
        def update(self):
            for tile in self.map:
                tile.update()
    
        def draw(self):
            for tile in sorted(self.map, key=lambda tile: tile.selected):
                tile.draw()
    
    class Tile:
        def __init__(self, x, y):
            self.x = x
            self.y = y
            # make rect to check for rough collision
            self.rect = pygame.Rect(x, y, 40, 48)
    
            self.left = False
            self.right = False
            self.selected = False
    
        def update(self):
            self.selected = False
            x, y = pygame.mouse.get_pos()
            if self.rect.collidepoint((x, y)):
                # the mask needs relative coordinates
                x -= self.rect.x
                y -= self.rect.y
                if map.top_image_mask.get_at((x, y)):
                    self.selected = True
    
        def draw(self):
            pos = self.rect.x, self.rect.y
            screen.blit(map.top_image, pos)
            if self.left:
                screen.blit(map.left_image, pos)
            if self.right:
                screen.blit(map.right_image, pos)
            if self.selected:
                screen.blit(map.cursor, pos)
    
    pygame.init()
    screen = pygame.display.set_mode((640, 480))
    clock = pygame.time.Clock()
    
    map = Map(15, 15, [320, 100])
    
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                exit()
    
        screen.fill((230, 250, 255))
    
        map.update()
        map.draw()
    
        pygame.display.flip()
        clock.tick(60) # limit the framerate
    

    Screenshot of the example:


    It namely uses classes to store the tiles, which are generated by Map.__init__ according to an original position, a width and height values. The code in Map.__init__ determines the position of each tile, stored as a Tile object. They sometimes get the order to draw the left or right textures, to make the map look more 3D-ish.


    But the most important part is the use of the Map.top_image_mask variable. This is a mask created from the Map.top_image Surface. Its get_at function is used, which returns True if the relative point in the mask is opaque, or False if not. For example, with this image, mask.get_at((0, 0)) will return False, because the pixel on the very top left of the image is transparent, but it will return True if you ask for mask.get_at((20, 10)) for example.

    I make sure to check for a pygame.Rect collision before checking the mask, because calling mask.get_at with a point outside of the texture will return an error. The first step is then to check if the absolute mouse position collides with the rect of the texture of a specific tile, then if it "touches" it, check if the touched pixel is not transparent by the use of relative mouse coordinates and the mask.get_at function.

    I hope that helps.