Tile Selection (Mouse Picking)
Now comes the tricky part, what if you need to select a tile, or a character on a tile? Or more technically–how can you determine which tile your mouse is currently over? I should also mention that this process may sometimes be referred to as a “screen-to-map” conversion, taking the mouse’s screen coordinates, and converting them to map coordinates. In a regular map with rectangular tiles it’s easy, just take the x and y, divide them by tileWidth and tileHeight, then do the conversion formula: tileID = y * numCols + x (unless you use a 2D array).
For isometric tiles, since we have overlapping rectangles and slopes instead of neatly aligned perpendicular tiles, we have to do some extra work. There are a couple different ways to do this that I have found in my research. One involves the use of the sqrt, sin and cos functions and the other is a sort of “lookup table” as they call it. I will go over the lookup table version, as it is easier to follow what is actually going on. The other version is definitely shorter code-wise, but is not as effecient. OK, so where to start?
Basically, this method is a divide and conquer approach. First you divide the map up into rectangular tiles, getting the tile ID as if it were a rectangular map. Next you determine which section of the rectange it’s in, out of four…I called these four sections “quadrants”. Once you determine this, you adjust the tile ID by a certain amount depending on the quadrant. That’s just an overview, now I’ll get into the actual code. Keep in mind that this is not a fully optimized version of the method, but is simply laid out so as to be as straightforward as possible. The example code is written in C#.
After we adjust for any scrolling, we have the screen position of the mouse. So we need to get the basic tile ID as if we had a map of rectangular tiles:
// declare and set variables for half cell width and height // m_ptCurrMouseTileID is a class member of type Point int halfWidth, halfHeight; halfWidth = (cellWidth / 2); halfHeight = (cellHeight / 2); // first find which rectangle the point's in: int rectIDx, rectIDy; m_ptCurrMouseTileID.X = rectIDx = mouse.X / cellWidth; m_ptCurrMouseTileID.Y = rectIDy = mouse.Y / cellHeight; if (m_ptCurrMouseTileID.Y > 0) m_ptCurrMouseTileID.Y = (m_ptCurrMouseTileID.Y * 2);
The m_ptCurrMouseTileID variable will be used later on to determine the final tile ID. The last two lines are there to adjust the y coordinate due to the fact that there is actually an added row in between row zero and row 1 in an isometric map (row 1 is actually row 2 in the iso map), which is not accounted for in the rectangular tile formula. For example, if your mouse was over the third row (row[2]), the first part of the equation would return y = 1, when in fact it should be 2. You can think of it as making sure we get a non-indented row ID every time, which will help simplify the process.
// now find which quadrant it's in based upon the // point's relative position in this rect int adjX, adjY; adjX = mouse.X - (rectIDx * cellWidth); adjY = mouse.Y - (rectIDy * cellHeight); int quadrant = (int)QUADRANT.TOP_LEFT; if (adjX >= hWidth) quadrant = (int)QUADRANT.TOP_RIGHT; if (adjY >= hHeight) { if (quadrant > 0) quadrant = (int)QUADRANT.BTM_RIGHT; else quadrant = (int)QUADRANT.BTM_LEFT; }
As you can see, we have three more variables declared: an adjusted x, y, and quadrant. This part is first taking the mouse position and adjusting it to be local to that tile. For example, if the mouse position was x = 500, y = 300, and the tile’s width = 64, height = 32, the mouse point is now confined to be x = 0 -> 64, y = 0 -> 32. This will allow us to determine which quadrant of that tile we’re in. We start with the top-left by default and check for the other three. If we’re at least halfway across (>= 32 in the previous example), then we know we’re either in the top-right, or bottom-right quadrant. Then, we see if we’re in the bottom half. Hopefully it’s pretty simple so far…if not, I made yet another image to help you visualize what’s going on here.
Now, all we have left is to determine if the point is inside the tile or outside. To do this, we use a slightly modified slope of a line formula. This formula is simple in computations, using only subtraction, multiplication, and division–as opposed to the other method which requires the use of sin, cos, and sqrt. Essentially, what it does is return zero if the point is on the line, < zero if it’s on one side of the line, and > zero if it’s on the other side of the line. Here’s the formula:
In case you hadn’t figured it out, the line being used in this formula is the edge of the isometric tile that cuts the quadrant in half diagonally. x1,y1 is one end of the line, x2,y2 is the other. You plug in your adjusted mouse position and the result will tell you which side of the line you are on. Then, depending on which quadrant you’re in, and what the result is, you have to adjust the tile xID and/or tile yID appropriately. Here is the rest of the code.. NOTE: if the “if(result…)” check does not return true, the correct tile IDs have already been calculated and no further changes need to be made.
// now determine if the point is inside or outside // the actual tile based upon which quadrant it's in int result = -1; switch (quadrant) { // for bottoms, if result > 0, the point is outside the tile case (int)QUADRANT.BTM_RIGHT: { float d = (float)(hHeight - cellHeight) / (float)(cellWidth - hWidth); result = adjY - cellHeight - (int)(d * (float)(adjX - hWidth)); if (result > 0) ++m_ptCurrMouseTileID.Y; } break; case (int)QUADRANT.BTM_LEFT: { float d = (float)(hHeight - cellHeight) / (float)(-hWidth); result = adjY - cellHeight - (int)(d * (float)(adjX - hWidth)); if (result > 0) { --m_ptCurrMouseTileID.X; ++m_ptCurrMouseTileID.Y; } } break; // for tops, if result is < 0, the point is outside the tile case (int)QUADRANT.TOP_RIGHT: { float d = (float)hHeight / (float)(cellWidth - hWidth); result = adjY - (int)(d * (float)(adjX - hWidth)); if (result < 0) --m_ptCurrMouseTileID.Y; } break; case (int)QUADRANT.TOP_LEFT: { float d = (float)hHeight / (float)(-hWidth); result = adjY - (int)(d * (float)(adjX - hWidth)); if (result < 0) { --m_ptCurrMouseTileID.X; --m_ptCurrMouseTileID.Y; } } break; } break;
Yes, there are four casts happening every time through the switch, but that’s alright, maybe you can figure out how to optimize that
. It is important to note that you don’t want to lose the precision during division. Bad things may happen if you do…
Well, I think that pretty much covers the core of making an isometric map and how to interact with it. Of course there are some advanced topics to get into with isometric maps such as height maps, character movement, and pathfinding, but this should at least give you a foundation. The above code was taken from a tile editor that I am continually working on. If you want to check it out, head over to subtle-brilliance and download it!
Peace.
Resources
GameDev.net – an article explaining isometric tiles. And a ton more articles from there as well.
A pretty good Screen-to-map conversion tutorial on SoapDragon, covers Diamond maps.
A great site for really good free graphics, including tiles and animations, check out Reiner’s site.
A great reference book: Isometric Game Programming with DirectX 7, a bit dated on the DX version, but the concepts live on.
- Intro/Rendering
- Tile Selection (Mouse Picking)
