Jump to content

* * * * * 2 votes

Isometric Map in SFML

isometric rpg 2D SFML
4: Adsense

Implementing the Map

In order to implement a map, we need to clarify what exactly a map cell or box is. The map itself is very simple, just a 2D allocated array of map cells. Outside of rendering the map, the map cells do most of the work of storing objects, etc... Here is the map cell format we are going to use:


As you can see here, a map cell is merely a set of lists of things to draw. These lists hold pointers to IsometricMapSprites (the actual instances need to be kept elsewhere; the map is just an aid to rendering, not an object management device, although we will "cheat" with some objects, as you'll see in a bit.) You might be wondering why I have a list for things like floor, walls and roof. This is to give us flexibility in decorating a map. You can add as many floor graphics, wall graphics or roof graphics to a cell as you want, and the graphics will be layered in a First-In-First-Out manner. In this way, graphics can be layered using alpha-blending to apply decorations. You can set a floor graphic of a plain dungeon stone floor, then add an alpha blended smear of blood on top of it. Same with walls. You can draw a plain-Jane wall, then add a torch in a sconce as an overlay.

In a way, this kind of layered decoration is a bit simpler in a pure 2D system than in a 3D system, since in true 3D you often need to worry about Z-fighting artifacts when rendering multiple images onto the same plane. In 2D, since we don't check depth, it's not an issue. Just be sure that you add graphics in the order in which they are to be drawn, and all should be well.

You can also use the layered floors to implement transitions between terrain types. I talk about this more in the section on creating artwork.


NodeSize, or size of a cell, has direct bearing upon the isometric view. NodeSize determines the image dimensions of the graphic files that are drawn for the roof, floor and walls sections. The dimensions of a floor or roof image are equal to (4*NodeSize, 2*Nodesize). A wall piece isn't bounded in height, but the image width is equal to (2*NodeSize). So if you set NodeSize to 32, then your diamond floor tiles will be sized (128,64). If you set NodeSize to 64, your floor tiles will be (256,128). Thus, you must find a balance. Larger tiles allow greater detail and help to reduce the appearance of repetition in the tiles, since fewer tiles are visible onscreen at one time. However, this comes with the tradeoff of inflated memory usage. I usually use a NodeSize of either 32 or 64. Smaller than 32, the tiles tend to be very small and repetition is very obvious; larger than 64, the tiles tend to use a lot of memory. Also, NodeSize should be limited to powers-of-2 to ensure that the images used are powers of 2, unless you can be sure that the hardware allows non-power-of-2 textures. SFML will upload images to texture memory, and I believe it will resize textures to the nearest power of 2 greater than the image dimensions, so you can reduce texture wastage by using power-of-2 images to start.

Map Cell Coordinates

It is necessary to be able to find which cell a given World(x,y) lies within. This is done by simply taking the floor of the coordinate divided by NodeSize.
cellx = math.floor(worldx/NodeSize)
celly = math.floor(worldy/NodeSize)

The map uses a bucketing system, where dynamic objects are added to the map and placed into buckets. The object world location is used to calculate which cell bucket, or list, the object should be placed in, so that when the map is drawn, only the objects residing in visible cells are drawn. The cell coordinate space is referred to as Cell(x,y).

Drawing Order

2D isometric games come with some oft-times tricky layering problems, since the lack of depth information can make sorting properly a tad difficult. Some of the problems are circumvented via the drawing order. The drawing order I use is this:

1) Draw all floors of all visible cells
2) Iterate the cells from top of screen to bottom (more on this in a bit) and for each cell draw Objects followed by Roofs, followed by Walls

This order has some requirements. When a dynamic object is bucketed into a cell's object list, it needs to be sorted into the list so that the list is ordered back-to-front. The value used for comparison to perform this sorting is obtained by: WorldPosition.X + WorldPosition.Y + WorldPosition.Z. This value is consistent with the depth of the various coordinates within a cell relative to the conceptual camera; nevertheless, there are still constraints placed upon objects. Objects should be no larger than a cell in size; larger objects should be broken up into cell-sized chunks. This pertains to walls as well as to dynamic objects. Isometric systems traditionally do not like objects that cover more than one cell; all manner of posts in the Isometric Games forum attempt to deal with this problem, which becomes somewhat less of a problem if the objects are broken up. Breaking them up will also help with graphic reuse; you can reduce memory footprint by using large structures built up out of reusable smaller pieces, rather than having lots of special use large structures.

Drawing The Map

This part is where the magic happens. The brain-dead solution of course is to just iterate the entire 2D map array and draw each cell, and let the hardware cull non-visible objects, but this can be bad performance-wise if you have a large map with lots of objects. A better plan is to just figure out which parts of the map are visible and draw only those cells. In a traditional top-down tile game, where the camera is not rotated around the Up Axis by 45 degrees, this task is trivial, since the visible area will just be a sub-rectangle of the world cell array. In an isometric game, though, the visible shape is different. The following graphic shows a map with a possible visible area highlighted.

Attached Image

In this graphic, the rectangle of the screen view is represented by the grey rectangle; all cells that the view rectangle intersects are highlighted in blue. You can see that the pattern is not a regular rectangle, but a much more complex shape. In order to render back to front, we need to render this complex shape row by row. Here is a graphic showing rendering order:

Attached Image

Here you can see some of the squares numbered. This is the drawing order. (The numbering should continue down the whole sequence, but I'm lazy.) If drawn in this order the cells should be drawn with basically correct overlap, as long as the constraints of the system are maintained, as far as object sorting, etc... There are probably going to still be a few edge-cases; this comes with the territory, and most of those can be handled through the collision system, by keeping objects far enough from walls and other solid things so that weird overlap doesn't occur.

Despite the seeming complexity of drawing rows like this, it's actually pretty easy. You can calculate the coordinates of the upper left corner of the chunk by reverse-projecting the upper-left corner of the viewport into World Space. You can calculate the number of columns and rows to draw similarly by dividing the size of the viewport by the on-screen size of a cell. Here, the Height of a cell comes into play, since you need to extend the viewport down to accommodate. You need to draw an arbitrary number of rows below the bottom edge of the viewport, to account for tall objects that might be visible on-screen even though the floor of the cell they lie in might not be. You also need to pad the top and sides by one cell in each direction to account for partially visible cells. But once you have constructed your suitably padded bounds, it becomes a simple matter of iterating the rows.

You start from the top row, which is an Even row. If the number of Nodes you need to draw across the screen (Screen Width / (NodeSize*4)) is NumNodes, then on Even rows you need to draw NumNodes+1 nodes, whereas on odd rows you draw NumNodes cells. (You can see in the image that the rows alternate in length). To advance along a row by one cell, you add 1 to cellx and subtract 1 from cell y. To advance to the next row from an even row, you add 1 to x; to advance to the next row from an odd row, you add 1 to y. And that's basically it. Here is an excerpt from the IsometricMap::draw() method, the part that renders all of the floors:


This excerpt does a number of things besides just drawing. A map has a specific center specified as an (x,y) coordinate. The center of the map corresponds to the point, in World Space, that aligns with the center of the screen. By changing the center, you can scroll the map. The drawing code presented takes the current View of the passed RenderWindow and uses the dimensions of it, as well as the map center, to calculate all of the parameters necessary for drawing. It also calculates the new Screen Space view necessary to draw the requisite piece of map, centering it upon the map center point. Once the view is set, the rows and columns of nodes in the visible chunk are iterated as described above. This iteration process is actually performed 2 times: once for the floor lists, and once for objects, roofs and walls. A variation of it is also performed before the drawing calls, in order to update the lighting of the visible region. And while we are on the topic of lighting...

Attached Thumbnails

  • Attached Image
  • Attached Image
  • Attached Image
  • Attached Image

Attached Files

Jun 23 2011 06:07 PM
Hardcore. Cheers. Interesting read. I totally agree that it would be preferrable these days to just do this in 3D, but I've found most iso-tutorials to be horribly overcomplex so this is a great addition to teh interwebz anyway.
Jun 27 2011 01:26 PM
Originally published 6/23 - date bumped for featuring purposes
Jun 27 2011 02:10 PM
Nice work!
Jun 28 2011 08:21 AM
SFML is good stuff! but yeah, traditional 2D Iso has gone the way of the dodo for all but niche, edutainment and very casual type applications.
Jun 28 2011 10:44 AM
This is really great. I dont use SFML, I use Java and Slick2d/LWJGL.. but since thats all build on openGL,..I should be able to apply some of this stuff directly. I really want to try out a simple lighting system like youve done. WE NEED MORE ARTICLES LIKE THIS!
Jul 24 2011 02:05 PM
Im not sure what you(and comments) mean by going 3D preferably..Its not a question of programming choice, but art style choice you know..
Unless you mean something like Disgaea, with 2D sprites on top of a 3D environment, but if you want 2D environment, 2D tile'ing' on top of 3D..Is it even possible?(probaly even more complicated)
A good exemple of 2D iso game is the mmorpg Dofus, the art is just awsome, and its not an old school game.

That comes of the top of my head, cause Im a fan of iso games, and I plan to make my iso cubic engine someday.

Good article, thanks for sharing.
Mar 27 2015 12:55 AM

Hello, nice tutorial, thank you!
But please, could you, or someone else, explain with more details the equation to convert world space to screen space?
I'm failing to understand it. if world x equals world y then screen x will always be zero? Why is that? Also the last point in world space, as in your example (4096,4096) would be (0, 8192)? i don't get it... 

Mar 27 2015 02:46 PM

Consider this isometric tile:


The green lines indicate the X and Y axes of the screen coordinate system. The origin of the screen system is at 0, where the green axes cross. The X coordinate increases to the right, the Y increases up. Overlaid on this grid is an isometric tile. In the isometric system, the X axis increases up and right, the Y axis increases up and left. The tile represents a square cell 16x16 units in size, as you can see by the alternating color patterns. The black and blue areas represent grid lines, the red areas are "in between" areas. The blue lines run on the X=0, X=3, X=7, etc.. and Y=0, Y=3, Y=7 etc... coordinates.

The system I uses overlays the isometric grid onto the screen grid with corresponding origins. That is, the origin of the isometric world, (0,0), corresponds with the origin of the screen plane, ie (0,0). On this overlay, you can see that on the tile, as the X coordinate increases (up and rightward) the corresponding screen coordinate increases +2 in the screen X direction and +1 on the screen Y direction. As the Y coordinate increases, the screen X decreases by -2 in the screen X direction, and +1 in the screen Y direction. So by this token, if isometric X==16, corresponding to the right-hand corner of the diamond tile, the screen coordinates will be equal to X=32, Y=16. Now, if you increase isometric Y from that point, you will begin decreasing screen X and continue increasing screen Y, so that at Y==16 the screen X coordinate will be reduced back to 0, the screen Y coordinate will now be at 32. And this spot corresponds to the location (16,16) on the isometric tile, or the top point of the diamond. You can see that indeed any isometric coordinate where X==Y will, in fact, lie along the screen X=0 axis.

Note: GameDev.net moderates comments.