Drawing tile maps in 2D games

2018-08-28

Some two weeks ago, I've participated in the Ludum Dare compo and created a 2D platforming game. Almost any 2D game that uses tile-based maps will face the issue of how to render them properly. This may seem trivial – just use texture X on tile X and texture Y on tile Y – but unless you're aiming for a very minimalistic style, you'll probably want edges and corners to look a bit differently than a "solid" tile.

While this requires some extra work on the graphics, it's surprisingly simple code-wise and leads to nice-looking results. Follow me as I describe my approach.

Dividing tiles into quarts

In the first step, we divide each tile into four quarters, which I shall henceforth call "quarts". A tile has four quarts: the top-left, top-right, bottom-right and bottom-left one. Each quart stores information about the tile's neighbours.

What this means is that, effectively, each quart is a three-field bitmask:

1. Whether there's a continuous tile on the X-axis.

2. Whether there's a continuous tile on the Y-axis.

3. Whether there's a continuous tile "in the corner" - e.g. for the top-left quart, this means the `[x-1][y-1]` tile.

Stickiness

An interesting aspect to consider when calculating quarts is what I call their "stickiness". Quarts are non-sticky if we only consider tiles of the same type as continuous (left image), and sticky if we consider all non-empty tiles to be continuous (right image).

Using sticky quarts typically leads to a smoother-looking map, where the elements of the terrain fade into each other (kind of) seamlessly. On the other hand, non-sticky quarts can be used to obtain sharp edges between different tile types. If needed, one might extend this approach into giving each tile type its own "stickiness" property, or dividing tile types into groups and only considering tiles of the same group to be continuous.

Creating the tile graphics

Before we can render quarts, we need to prepare the tile graphics. In my projects, I use the following format:

First, there's the solid tile; it's then followed by the horizontal and the vertical tile, finishing with two corner tiles – the outside-corner and inside-corner.

You may be thinking "wait a moment – there's eight possible quart values, yet only five tile graphics?". The reason is rather simple and becomes obvious when visualised:

• A quart with no XY neighbours is an outside-corner quart, no matter the "corner" bit.
• A quart with only one XY neighbour is a horizontal or a vertical quart, no matter the "corner" bit.
• Only when both XY neighbours are present, the "corner" bit is useful for differentiating between a solid tile and an inside-corner.

As such, if you're particularly lazy (or have tight constraints), you can merge inside-corners and solid tiles into one by just ignoring the "corner" bit in your code and only considering the XY neighbours – this will leave you with four graphics per tile instead of five.

Rendering the map

To render the map, we must first calculate quarts. This is rather simple – all we have to do is, for all tiles, assume the initial state as zero (no neighbours) and then check whether X/Y/corner neighbours are present – and if so, flip the appropriate bit.

In the example above, the state of the center tile's quarts is as follows:

• Top-left: `--C`, which means this is an outside-corner quart.

• Top-right: `X--`, which means this is a horizontal quart.

• Bottom-right: `XY-`, which means this is an inside-corner quart.

• Bottom-left: `-YC`, which means this is a vertical quart.

Once we calculate the quarts, all that's left to do is to draw them. One important thing to note is that since we're not rendering whole tiles anymore, but rather quarts, the number of draw calls will effectively quadruple. This can lead to a huge loss in performance; as such, it's usually a good idea to pre-render the map into an offscreen texture and then paste this texture onto the screen, instead of re-drawing the map from quarts on each frame.