Polygon picking for hexagonal tiles in T2D
by Simon Love · 02/22/2014 (10:40 am) · 13 comments
In the past few weeks I've been working on an isometric hexagonal-tiled game.
The Cause ? Fallout 1 & 2
One thing I really wanted was a more discerning picking function than the ones found in stock T2D.
This reminded me that one of the questions that came up countless times on the forums was
Quote:"How can I determine if my mouse click falls within an isometric or hexagonal tile shape?"
The Problem
-----------------------
We already have picking functions for the CompositeSprite object (fixed by Paul Jan, practicing01 and myself) but they all concern themselves with the OOBB of the queried SpriteBatchItem/tile. This works fine for square tiles but not for isometric or hexagonal tiles.
In the case of an hexagonal tile (squashed halfway in the Y dimension), what happens if we click in the corners of the tile? Several tiles are chosen as their OOBB overlap.
As all OOBBS are rectangular, we need to verify if the point clicked falls within the hexagonal shape or not.
Cut to the chase already!!!
-----------------------
The logic here is pretty simple
1. Take your already existing CompositeSprite.pickPoint Console function here. Allow it to determine if we fall within the OOBB of a tile.
2. If we do find viable results, run this code :
...
U32 bufferCount = 0;
//NEW CODE STARTS HERE
b2PolygonShape Hexagon;
b2PolygonShape Bounds;
const Vector2* oobb = queryResults[n].mpSpriteBatchItem->getLocalOOBB();
Bounds.Set(oobb, 4);
Vector2 polypoints[6];
polypoints[0].Set(Bounds.m_centroid.x, Bounds.m_vertices[0].y);
F32 quarterheight = (Bounds.m_centroid.y - Bounds.m_vertices[0].y) / 2;
polypoints[1].Set(Bounds.m_vertices[0].x, Bounds.m_centroid.y - quarterheight);
polypoints[2].Set(Bounds.m_vertices[0].x, Bounds.m_centroid.y + quarterheight);
polypoints[3].Set(Bounds.m_centroid.x, Bounds.m_vertices[1].y);
polypoints[4].Set(Bounds.m_vertices[2].x, Bounds.m_centroid.y + quarterheight);
polypoints[5].Set(Bounds.m_vertices[2].x, Bounds.m_centroid.y - quarterheight);
Hexagon.Set(polypoints, 6);
b2Transform xf;
xf.p = b2Vec2(0.0f,0.0f);
xf.q = b2Rot(0.0f);
bool result = Hexagon.TestPoint(xf, point);
if(result)
{
//NEW CODE ENDS HERE
bufferCount += dSprintf( pBuffer + bufferCount, maxBufferSize-bufferCount, "%d ", queryResults[n].mpSpriteBatchItem->getBatchId() );
//Don't forget to close that if(result){ with a }
...All we are doing is manually creating a squashed hexagon inside the bounds of the OOBB and then testing if the picked point is within that polygon.
Remember that polygons in Box2d (and thus T2D) are winded counter-clockwise.
This function can then be easily modified to accommodate any shape you can geometrically describe.
I'm not saying this is the best way to do things or the most efficient but it works exceptionally well for my purposes.
And since I love you, community, I wanted to share it with you.
About the author
I am here to help. I've worked at every imaginable position in game development, having entered the field originally as an audio guy.
#2
Game is mostly a map editor without an interface so far.
I have mock-ups but they're made in Inkscape so that doesn't count!
As soon as I'll have something worth showing you can be sure I'll spam these pages!
02/22/2014 (9:21 pm)
Thanks for the great words yurembo!Game is mostly a map editor without an interface so far.
I have mock-ups but they're made in Inkscape so that doesn't count!
As soon as I'll have something worth showing you can be sure I'll spam these pages!
#3
02/24/2014 (9:30 pm)
Interesting solution Simon, thanks for sharing! How did you come up with the Box2D idea? A quick google search on the subject often points to using a bunch of math to map world space into isometric/hexagon space for picking.
#4
02/24/2014 (11:00 pm)
@Mike : The other option I've seen was to use color masks but I had no idea on how to implement that. I just went back to my early tests with CompositeSprite's picking functions and remembered that creating shapes and testing whether points fell within them was pretty simple.
#5
What I've got so far is a functional Hexagonal Tile Map editor
- Saves to / Loads from disk
- Features multiple tile heights
03/05/2014 (7:45 pm)
For those still listening...What I've got so far is a functional Hexagonal Tile Map editor
- Saves to / Loads from disk
- Features multiple tile heights
#6
Cool stuff Simon. Are you using the custom CompositeSprite layout mode or did you add hex support to it?
03/06/2014 (9:29 am)
I'm listening!Cool stuff Simon. Are you using the custom CompositeSprite layout mode or did you add hex support to it?
#7
My take on it is that adding a Hex_mode to it would defeat the purpose, as your hexagonal tile implementation might differ wildly from how I do it. Custom Layout seems to be the best compromise.
I've spent the most of two weeks writing my own object and sorting code.
THEN, I've looked into the batching system
wiki entry and noticed that T2D already handles everything I want out of the box!
A few additional Console functions were necessary, notably :
- CompositeSprite::getLogicalPosition // This is not exposed to script but is already kept in a variable on the object so...let's use it!
- PickhexPoint (as listed in the original post)
- ListNeighbors // Lists all tiles around the selected tile. To display the 'underwater' tiles, I simply look if there is a neighbor in position 2 and 3. If any of them is empty, display the blue-tinted sprite.
Notes
- CompositeSprite's logical position can take up to 5 parameters. X Y Z, etc. You can use these for any purpose you wish, as long as in the end, you get X/Y coordinates to place the sprite.
- Z is treated as depth within the layer and can be any fraction that you want. If each row of hexagonal tiles has the same Zdepth, adding a sprite in the Z direction (adding the cliff walls at a higher altitude) simply means using Zdepth.x. So an object sitting on top of a tile of Zdepth 32 will be located at Zdepth 32.01, 32.02, 32.03, etc..
I love T2D. I love T2D. I LOVE T2D. I also love its batching system.
I will eventually write a detailed guide on this but anyone who takes the time to think of the algorithm can figure it out fairly easily.
03/06/2014 (11:21 am)
Using the custom layout. The only real trick is to use a modulo operator (%).Quote:
If (TILE_LOGICALPOSITION_Y % 2) results as a remainder (meaning that it is an odd number), we offset the X placement of the tile by half-a-tile's width.
My take on it is that adding a Hex_mode to it would defeat the purpose, as your hexagonal tile implementation might differ wildly from how I do it. Custom Layout seems to be the best compromise.
I've spent the most of two weeks writing my own object and sorting code.
THEN, I've looked into the batching system
wiki entry and noticed that T2D already handles everything I want out of the box!
A few additional Console functions were necessary, notably :
- CompositeSprite::getLogicalPosition // This is not exposed to script but is already kept in a variable on the object so...let's use it!
- PickhexPoint (as listed in the original post)
- ListNeighbors // Lists all tiles around the selected tile. To display the 'underwater' tiles, I simply look if there is a neighbor in position 2 and 3. If any of them is empty, display the blue-tinted sprite.
Notes
- CompositeSprite's logical position can take up to 5 parameters. X Y Z, etc. You can use these for any purpose you wish, as long as in the end, you get X/Y coordinates to place the sprite.
- Z is treated as depth within the layer and can be any fraction that you want. If each row of hexagonal tiles has the same Zdepth, adding a sprite in the Z direction (adding the cliff walls at a higher altitude) simply means using Zdepth.x. So an object sitting on top of a tile of Zdepth 32 will be located at Zdepth 32.01, 32.02, 32.03, etc..
I love T2D. I love T2D. I LOVE T2D. I also love its batching system.
I will eventually write a detailed guide on this but anyone who takes the time to think of the algorithm can figure it out fairly easily.
#8
I also want to experiment with the set/getUserData methods that are not exposed either. In one of my projects, I use data objects, but it is a bit silly to have a ScriptObject for each individual tile when all I want to do is store a couple variables tied to each tile. Using data objects works, but for whatever reason it doesn't feel right to double the amount of object ids in the game (I was getting into the hundreds of thousands since the AI needed to assess all possible outcomes).
03/06/2014 (1:12 pm)
Yeah, funny enough I noticed last week as well that getLogicalPosition was not exposed to TorqueScript. It's on the to-do list to add to the dev branch.I also want to experiment with the set/getUserData methods that are not exposed either. In one of my projects, I use data objects, but it is a bit silly to have a ScriptObject for each individual tile when all I want to do is store a couple variables tied to each tile. Using data objects works, but for whatever reason it doesn't feel right to double the amount of object ids in the game (I was getting into the hundreds of thousands since the AI needed to assess all possible outcomes).
#9
What I mean is that the user data is set as a void*: we have to make sure that when we save it and retrieve this data, we use the same type (String, int, float, etc.).
I'll get to it eventually, just haven't needed it so far.
Here ya go, might save you a few seconds :
03/06/2014 (1:37 pm)
Yup, the setuserdata is there in C++ but the problem is that if we expose it to script, we have to decide how to interpret it.What I mean is that the user data is set as a void*: we have to make sure that when we save it and retrieve this data, we use the same type (String, int, float, etc.).
I'll get to it eventually, just haven't needed it so far.
Here ya go, might save you a few seconds :
ConsoleMethodWithDocs(CompositeSprite, getSpriteLogicalPosition, ConsoleString, 2, 2, ())
{
SpriteBatchItem* tempSprite = object->getSelectedSprite();
SpriteBatchItem::LogicalPosition LogicalPosition = tempSprite->getLogicalPosition();
const U32 maxBufferSize = 4096;
// Create Returnable Buffer.
char* pBuffer = Con::getReturnBuffer(maxBufferSize);
// Set Buffer Counter.
U32 bufferCount = 0;
for(int counter = 0; counter < LogicalPosition.getArgCount(); counter++)
{
bufferCount += dSprintf( pBuffer + bufferCount, maxBufferSize-bufferCount, "%d ", LogicalPosition.getIntArg(counter) );
}
return pBuffer;
}
#10
03/06/2014 (1:59 pm)
Is the buffer that gets created from the .scriptThis method too small for custom logical positions?
#11
I based my implementation on the fact that the string can return a string of 1 to 5 words, depending on the user's preferences.
03/06/2014 (2:25 pm)
.scriptThis only works with Vector2's (x/y).I based my implementation on the fact that the string can return a string of 1 to 5 words, depending on the user's preferences.
#12
That's roughly what is happening when writing out to TAML.
03/07/2014 (7:50 am)
Thanks for the info Simon. Your implementation made me curious. Instead of iterating through the logical position arg count, couldn't you just dump the contents of mLogicalPosition withreturn object->getLogicalPosition().getString();
That's roughly what is happening when writing out to TAML.
#13
03/07/2014 (12:16 pm)
It should definitely work indeed. I just went with a method which I've seen used in the engine a gallizion times. 
yurembo
Very Interesting!