HexagonExample

Using hexagon based maps is not as easy as using square maps. Mostly because it takes more effort to translate pixel locations (like mouse clicks) to map locations. When I search this issue on the net I found a really nice article explaining a technique to solve this matter with mathematics. But I wanted to have a pixel-perfect pixel-to-map algorithm so I used the article as a base and implemented a way to solve this.

Based on the assumption that if I needed this then probably someone else need it I wrote this little example to show how a pixel-to-hexagon-map can be implemented in python/pygame.

Example Instructions

When you move around the mouse pointer the application will translate the mouse location to a hexagon map location and show this with a yellow hexagon.

Press space to toggle the display of the help grid square. You can use this to get a better understanding of the source code.

Screenshot

Related links

Downloads

No License

This code is public domain.


Comment this project:

Your name:
Your email (hidden):
Message:
Enter the validation code :
Private! (visible for webmaster only)

Avirup 2012-12-11 18:09:06

Very nice!

Chuck 2012-09-30 00:26:52

Interesting approach. How would you handle the map if the hexes were rotated 90 degrees?

Ian Epperson 2009-11-28 06:09:23

I've been hacking on this now for a bit, and updated the program to dynamically size the hexagons, remove almost all the constants and pull out the hexagon calculations into a separate object. The static arrays have been replaced with a pair of methods that perform the same function.
-----------------------------------------
#/usr/bin/env python

import pygame, math
from pygame.locals import *
pygame.init()

class hexagon:
def init(self, side):
# dimensions of the hexagon
#
# / \ .
# / \ .
# / \ .
# | | . .
# | | . .
# | | s b
# | | . .
# | | . .
# \ / . .
# \ / h .
# \ / . .
# -----r----
#
# --------a----------
#
# h/r/s form a 30-60-90 triangle

# store the length of one side of the hexagon, then calculate the rest of the measurements
self._s = float(side)
# h is the height from the bottom (or top) of the hex to the vertical side
self._h = self._s / 2
# r is the width from the center point to the edge
self._r = math.sqrt( self._s*self._s - self._h*self._h )
# b is the overall height
self._b = self._s + self._h * 2
# a is the overall width
self._a = self._r * 2
# m is the hr slope used for various calculations
self._m = self._h / self._r

# define all points, starting with the top center and proceeding clockwise
self._points = [ (self._r, 0) , (self._a, self._h), (self._a, self._h + self._s),
(self._r, self._b), (0, self._h + self._s), (0, self._h) ]

# expose public values
self.height = int( round( self._b) )
self.width = int( round( self._a ) )
# cell size for the equivalent square. Used to convert grid coords to hex coords.
self.cell_height = int( round( self._h + self._s ) )
self.cell_width = int( round( self._a ) )
self.odd_row_offset = int( round( self._r ) )

def points(self, x=0, y=0):
'''Return a set of points describing each corner offset by the given x,y '''
newPoints = []
for point in self._points:
newPoints.append( (point0+x, point1+y) )
return(newPoints)


# gridEven and gridOdd are for mapping x,y coord to a grid. On even rows, gridEven is used to calculate
# if the pixel falls in the equivalent hex or if it should be in the upper left or right.
# On odd rows, gridOdd is used to calculate if the pixel falls in the left or upper cell.
# These allow a typical 2-dimensional array to map as a hexagonal grid.
def __gridEven(self, x, y): #A-Sections
#
# -1,-1 / \ 0,-1
# / \
# / \
# | |
# | 0, 0 |
# | |
# | |
# | |

if y < ( self._h - self._m * x ):
return (-1,-1) # upper left
elif y < ( -self._h + self._m * x):
return ( 0,-1) # upper right
else:
return ( 0, 0) # center

def __gridOdd(self, x, y): #B-Sections
#
# \ 0,-1 /
# \ /
# \ /
# |
# |
# -1, 0 | 0, 0
# |
# |

if x >= self._r: # right side
if y < ( self._s - self._m * x ):
return ( 0,-1) # upper center
else:
return ( 0, 0) # mid-lower right
else: # left side
if y < ( self._m * x ):
return ( 0,-1) # upper center
else:
return (-1, 0) # mid-lower left

def pixel2HexMap(self, x, y):
"""
Converts a pixel location to a location on the hexagon map.
"""
gridX = x / self.cell_width
gridY = y / self.cell_height
cellX = x self.cell_width

cellY = y
self.cell_height

if gridY & 1:
xMod,yMod = self.__gridOdd(cellX,cellY)
else:
xMod,yMod = self.__gridEven(cellX,cellY)

return ( gridX+xMod, gridY+yMod )

def hexMap2Pixel(self, mapX, mapY):
"""
Returns the top left pixel location of a hexagon map location.
"""
if mapY & 1:
# Odd rows will be moved to the right.
return ( mapX*self.cell_width + self.odd_row_offset, mapY*self.cell_height )
else:
return (mapX*self.cell_width, mapY*self.cell_height)

class HexagonExample:
def init(self, size=20):
self.hexDef = hexagon(size)
self.offset = 1
self.background = (150,150,150)
self.screenInit()

def updateSquare(self,x,y):
"""
Draw the logic square on the hex map.
"""

# Get the square location in our help grid.
gridX = x/self.hexDef.cell_width
gridY = y/self.hexDef.cell_height

rectX = gridX * self.hexDef.cell_width + self.offset
rectY = gridY * self.hexDef.cell_height + self.offset

# Update the gridRect to show the correct location in the grid
#self.gridRect.topleft = (gridX*self.hexDef.cell_width,gridY*self.hexDef.cell_height)
self.gridRect.topleft = (rectX, rectY)

def drawMap(self):
"""
Draw the tiles.
"""
fnt = pygame.font.Font(pygame.font.get_default_font(),12)

self.mapimg = pygame.Surface((640,480),1)
self.mapimg = self.mapimg.convert()
self.mapimg.fill(self.background)

for x in range(16):
for y in range(15):
# Get the top left location of the tile.
pixelX,pixelY = self.hexDef.hexMap2Pixel(x,y)
pixelX += self.offset # offset to allow for anti-aliasing
pixelY += self.offset

# draw the hexagon at the given position
pygame.draw.aalines(self.mapimg, (0x00, 0x00, 0x00), True, self.hexDef.points(pixelX,pixelY), True)

# Show the hexagon map location in the center of the tile.
location = fnt.render("%d,%d" % (x,y), 0, (0xff,0xff,0xff))
lrect=location.get_rect()
lrect.center = (pixelX+(self.hexDef.width/2),pixelY+(self.hexDef.height/2))
self.mapimg.blit(location,lrect.topleft)
#self.mapimg.scroll(-1,-1)

def createCursor(self):
"""
Create the cursor.
"""
self.cursor = pygame.Surface((self.hexDef.width+4, self.hexDef.height+4),1)
self.cursor.fill(self.background)
self.cursor.set_colorkey( self.background )

pygame.draw.aalines(self.cursor, (0xFF, 0xFF, 0x00), True, self.hexDef.points(self.offset,self.offset), True)
self.cursorPos = self.cursor.get_rect()

def screenInit(self):
"""
Setup the screen etc.
"""
self.screen = pygame.display.set_mode((640, 480),1)
pygame.display.set_caption('Press SPACE to toggle the gridRect display')

self.createCursor()
self.drawMap()

self.gridRect = pygame.Rect(0,0,self.hexDef.cell_width,self.hexDef.cell_height)

def setCursor(self,x,y):
"""
Set the hexagon map cursor.
"""
self.updateSquare(x,y)
mapX,mapY = self.hexDef.pixel2HexMap(x,y)
pixelX,pixelY = self.hexDef.hexMap2Pixel(mapX,mapY)
self.cursorPos.topleft = (pixelX,pixelY)

def mainLoop(self):
clock = pygame.time.Clock()

showGridRect = True

while 1:
clock.tick(30)

for event in pygame.event.get():
if event.type QUIT: return elif event.type KEYDOWN:
if event.key K_ESCAPE: return elif event.key K_SPACE:
showGridRect = not showGridRect

elif event.type == MOUSEMOTION:
self.setCursor(event.pos0,event.pos1)

# DRAWING
self.screen.blit(self.mapimg, (0, 0))
self.screen.blit(self.cursor,self.cursorPos)
if showGridRect:
pygame.draw.rect(self.screen, (0xff,0xff,0xff), self.gridRect, 1)

pygame.display.flip()

def main():
g = HexagonExample(20)
g.mainLoop()


#this calls the 'main' function when this script is executed
if name == 'main': main()

DR0ID 2007-02-21 19:05:43

Hi
wouldn't it be better to use an image with 3 different colors to determine in wich hexagon the cursor is instead of the list you are using? I ask, because if you want to change the size of the hexagons then your lists will get even bigger. Otherwise you could just use an scaled image. :-)

Its just a suggestion. :-)

~DR0ID