pythonpygamecollision-detectionhexagonal-tiles

Maximising Collidable area for a hexagonal "Button" in pygame


I am programming a strategy game that takes place on an Hexagonal grid (yes, a civ clone) and want to implement a system, which allows me to display some properties of the tile by hovering over it with my mouse. My current implementation is to create a rect for every Tile (1), this rect has the same dimensions and coordinates as the sprite of the Tile. Then, I use pygame.Rect.collidepoint (2) to detect collision. The problem with this is that the rects of the different Tiles overlap at the edges, causing multiple collisions to be detected (3)

(1)

for i in range (0, 100):
    Tiles_rects.append (pygame.Rect(Tiles_pos[i], (TILE_WIDTH, TILE_HEIGHT )))

(2)

for i in range (0, 100):
            # Takes one tile at a time and determines if mouse is on top of it
            mouse_over_tile = pygame.Rect.collidepoint( Tiles_rects[i] , mouse_pos )    
            if mouse_over_tile:
                print (i)    #placeholder

(3)

I tried to make the colliders (rects) smaller and adjusted their position to be centered. While this does fix the overlap, it also makes a considerable part of the hexagon not react when hovered over. A circle collider instead of a rect collider would likely not be of much more use, as my hexagons are somewhat streched on the x axis.


Solution

  • You can use the fact that the slope on the sides of the hexagon is 60°. If the y-distance to the center of the bounding rectangle is less than the x-distance from the outside of the bounding rectangle multiplied by the tangent of 60°, the point is inside the hexagon:

    def collideHexagon(bounding_rect, position):
        px, py = position
        if bounding_rect.collidepoint((px, py)):
            dx = min(px - bounding_rect.left, bounding_rect.right - px)
            dy = abs(py - bounding_rect.centery)
            if dy < dx * math.tan(math.radians(60)):
                return True
        return False
    

    Minimal example:

    import pygame, math
    
    pygame.init()
    screen = pygame.display.set_mode((400, 400))
    clock = pygame.time.Clock()
    
    
    len = 100
    sin_len = math.sin(math.radians(60)) * len
    cos_len = math.cos(math.radians(60)) * len
    pts = [
        (len, 0), (len-cos_len, sin_len), (cos_len-len, sin_len),
        (-len, 0), (cos_len-len, -sin_len), (len-cos_len, -sin_len)]
    pts = [(x + 200, y + 200) for x, y in pts]
    
    tile_heihgt = sin_len * 2
    tile_width = len * 2
    tiel_rect = pygame.Rect(0, 0, tile_width, tile_heihgt)
    tiel_rect.center = (200, 200)
    
    def collideHexagon(bounding_rect, position):
        px, py = position
        if bounding_rect.collidepoint((px, py)):
            dx = min(px - bounding_rect.left, bounding_rect.right - px)
            dy = abs(py - bounding_rect.centery)
            if dy < dx * math.tan(math.radians(60)):
                return True
        return False
    
    run = True
    while run:
        clock.tick(100)
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
    
        color = "white"
        if collideHexagon(tiel_rect, pygame.mouse.get_pos()):
            color = "red"
     
        screen.fill(0)
        pygame.draw.polygon(screen, color, pts)         
        pygame.display.flip() 
    
    pygame.quit()
    exit()