In this tutorial, I’ll help you to create levels for any game genre and make designing levels much easier. You’re going to learn how to create your first tile map engine for use in any of your future projects. I’ll use Haxe with OpenFL, but you should be able to follow along in any language.
Here’s what we’ll cover:
- What is a tile-based game?
- Finding or making your own tiles.
- Writing the code to display tiles on screen.
- Editing tile layouts for different levels.
You might get a good start on a new game idea, too!
What Is a Tile-Based Game?
Naturally, Wikipedia has an in-depth definition of what a tile-based game is, but to get the basic gist there are only a few things you need to know:
- A tile is a small image, usually rectangular or isometric, that acts like a puzzle piece of art for building larger images.
- A map is a grouping of tiles put together to create a (hopefully) visually appealing “section” (like a level or area).
- Tile-based refers to the method of building levels in a game. The code will layout tiles in specific locations to cover the intended area.
To get even more basic, I’ll put it like this:
A tile-based game lays out tiles in order to create each level.
In reference to the common tile types – rectangular and isometric – we will use rectangular tiles in this article for their simplicity. If you do decide to try out isometric levels some day, there is additional math involved to make it work. (Once you’re done here, check out this great primer on creating isometric worlds.)
There are some pretty cool benefits that you get from using a tile engine. The most apparent perk is that you won’t need to create massive images by hand for each individual level. This will cut down on development time and cut down on file sizes. Having 50 1280x768px images for a 50-level game vs having one image with 100 tiles makes a huge difference.
Another side-effect is that locating things on your map using code becomes a little easier. Instead of checking things like collision based on an exact pixel, you can use a quick and easy formula to determine which tile you need to access. (I’ll go over that a bit later.)
Finding or Making Your Own Tiles
The first thing you will need when building your tile engine is a set of tiles. You have two options: use someone else’s tiles, or make your own.
If you decide to use tiles that have already been made, you can find freely available art all over the web. The downside to this is that the art wasn’t made specifically for your game. On the other hand, if you’re just prototyping or trying to learn a new concept, free tiles will do the trick.
Where to Find Your Tiles
There are quite a few resources out there for free and open source art. Here are a few places to start your search:
Those three links should give you more than enough places to find some decent tiles for your prototypes. Before you go crazy grabbing up everything you find, make sure that you understand what license everything is covered under and what restrictions they come with. Many licences will allow you to use the art freely and for commercial use, but they might require attribution.
For this tutorial, I used some tiles from a The Open Game Art Bundle for platformers. You can download my scaled-down versions or the originals.
How to Make Your Own Tiles
If you haven’t taken the plunge of making art for your games yet, it might be a little intimidating. Luckily there are some amazing and simple pieces of software that get you into the thick of it so you can start practicing.
Many developers start out with pixel art for their games and here are a few great tools for just that:
- Aseprite
- Pyxel Edit
- Graphics Gale
- Pixen for the Mac users
These are some of the most popular programs for making pixel art. If you want something a bit more powerful, GIMP is an excellent option. You can also do some vector art with Inkscape and follow some amazing tutorials over at 2D Game Art For Programmers.
Once you grab the software you can start experimenting with making your own tiles. Since this tutorial is meant to show you how to create your tile map engine I won’t go into too much detail about making the tiles themselves, but there is one thing to always keep in mind:
Make sure your tiles fit together seamlessly, and add some variation to keep them interesting.
If your tiles show obvious lines between them when put together, your game won’t look very nice. Make sure you put some time into creating a nice “puzzle” of tiles by making them seamless and adding some variation.
Writing the Code to Display Tiles
Now we’ve got all of that art stuff out of the way, we can dive into the code to put your newly acquired (or created) tiles on screen.
How to Display a Single Tile on Screen
Let’s start with the very basic task of displaying a single tile on screen. Make sure that your tiles are all the same size and saved in separate image files (we’ll talk more about sprite sheets later).
Once you have the tiles in your assets folder of your project, you can write up a very simple Tile
class. Here is an example in Haxe:
import flash.display.Sprite; import flash.display.Bitmap; import openfl.Assets; class Tile extends Sprite { private var image:Bitmap; public function new() { super(); image = new Bitmap(Assets.getBitmapData("assets/grassLeftBlock.png")); addChild(image); } }
Since all we are doing now is putting a single tile on the screen, the only thing that the class does is import the tile image from the assets
folder and add it as a child to the object. This class will probably vary greatly based on the programming language you use, but you should be able to easily find a guide on how to display an image on the screen.
Now that we have a Tile
class, we need to create an instance of a Tile
and add it to our Main class:
import flash.display.Sprite; import flash.events.Event; import flash.Lib; class Main extends Sprite { public function new() { super(); var tile = new Tile(); addChild(tile); } public static function main() { Lib.current.addChild(new Main()); } }
The Main
class creates a new Tile
when the constructor (the new()
function, called when the game starts) is called, and adds it to the display list.
When the game is run, the main()
function will also be called, and a new Main
object will be added to the stage. You should now have your tile appearing at the very top left of the screen. So far, so good.
Tip: If none of that makes sense – don’t worry! It’s just standard boilerplate Haxe code. The key thing to understand is that this creates a Tile
object and adds it to the screen.
Using Arrays to Lay Out an Entire Screen of Tiles
The next step is to find a way to fill the entire screen with tiles. One of the easiest ways to do this is to fill up an array with integers that each represent a tile ID. Then you can iterate through the array to decide which tile to display and where to display it.
You have the option of using a typical array or using a 2-dimensional array. In case you aren’t familiar with 2D arrays, it’s basically a single array that is filled with multiple arrays. Most languages will denote this as nameOfArray[x][y]
where x
and y
are indices for accessing a single element.
Since x
and y
are also used for screen coordinates, it might make sense to use x
and y
as coordinates for our tiles. The trick with 2D arrays is understanding how the integers are organized. You might have to reverse the y
and x
when iterating through the arrays (example below).
private var exampleArr = [ [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0] ];
Notice that in this implementation, the “0th” element in the array is itself an array, of five integers. This would mean that you access each element with y
first, then x
. If you try to access exampleArr[1][0]
, you would be accessing the sixth tile.
If you don’t quite understand how the 2D arrays work right now, don’t worry. For this tutorial I will use a normal array in order to keep things simple and to make things easier to visualize:
private var exampleArr = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
The above example shows how a normal array can be a little bit simpler. We can visualize exactly where the tile will be and all we have to do is use a simple formula (don’t worry, it’s coming!) to get the tile we want.
Now let’s write up some code to create our array and fill it with ones. The number one will be the ID that represents our first tile.
First we need to create a variable inside of our Main class to hold our array:
private var map:Array<Int>;
This might look a little strange, so I’ll break it down for you.
The variable name is map and I’ve given it a very specific type: Array
. The <Int>
portion just tells our program that the array will only hold integers. Arrays can hold just about any type that you want, so if you are using something else to identify your tiles, feel free to change the parameter here.
Next we have to add some code to our Main
class’s constructor (remember, this is the new()
function) so that we can create an instance of our map:
map = new Array<Int>();
This line will create an empty array that we can soon fill with our ones for testing it out. First, let’s define some values that will help us with our math:
public static var TILE_WIDTH = 60; public static var TILE_HEIGHT = 60; public static var SCREEN_WIDTH = 600; public static var SCREEN_HEIGHT = 360;
I’ve made these values public static
because this will give us access to them from anywhere in our program (via Main.Tile_WIDTH
and so on). Also, you might have noticed that dividing SCREEN_WIDTH
by TILE_WIDTH
gives us 10
and dividing SCREEN_HEIGHT
by TILE_HEIGHT
gives us 6
. Those two numbers will be used to decide how many integers to store in our array.
var w = Std.int(SCREEN_WIDTH / TILE_WIDTH); var h = Std.int(SCREEN_HEIGHT / TILE_HEIGHT); for (i in 0...w * h) { map[i] = 1 }
In this block of code, we assign 10
and 6
to w
and h
, like I mentioned above. Then we need to jump into a for
loop to create an array that can fit 10 * 6
integers. This will account for enough tiles to fill the entire screen.
Now we have our basic map built, but how are we going to tell the tiles to get into their proper place? For that we need to go back to the Tile
class and create a function that will allow us to move tiles around at will:
public function setLoc(x:Int, y:Int) { image.x = x * Main.TILE_WIDTH; image.y = y * Main.TILE_HEIGHT; }
When we call the setLoc()
function, we pass in the x
and y
coordinates according to our map class (formula coming soon, I promise!). The function takes those values and translates them into pixel coordinates by multiplying them by TILE_WIDTH
and TILE_HEIGHT
, respectively.
The only thing left to do in order to get our tiles on screen is to tell our Main
class to create the tiles and put them in place based on their locations within the map. Let’s go back to Main
and implement that:
for (i in 0...map.length) { var tile = new Tile(); var x = i % w; var y = Math.floor(i / w); tile.setLoc(x, y); addChild(tile); }
Oh yeah! That’s right, we now have a screen full of tiles. Let’s break down what is happening above.
The Formula
The formula that I keep mentioning is finally here.
We calculate x
by taking the modulus (%
) of i
and w
(which is 10, remember).
The modulus is just the remainder after integer division: \(14 \div 3 = 4 \text{ remainder } 2\) so \(14 \text{ modulus } 3 = 2\).
We use this because we want our value of x
to reset back to 0
at the start of each row, so we draw the respective tile on the far left:
As for y
, we take the floor()
of i / w
(that is, we round that result down) because we only want y
to increase once we have reached the end of each row and moved down one level:
Math.floor()
when dividing integers; Haxe just handles integer division differently. If you are using a language that does integer division, you can just use i / w
(assuming they are both integers).Lastly, I wanted to mention a little bit about scrolling levels. Usually you won’t be creating levels that fit perfectly within your viewport. Your maps will probably be much larger than the screen and you don’t want to continue drawing images that the player won’t be able to see. With some quick and easy math, you should be able to calculate which tiles should be on screen and which tiles to avoid drawing.
For example: Your screen size is 500×500, your tiles are 100×100, and your world size is 1000×1000. You would simply need to do a quick check before drawing tiles to find out which tiles are on screen. Using the location of your viewport – let’s say 400×500 – you would only need to draw tiles from rows 4 to 9 and columns 5 to 10. You can get those numbers by dividing the location by the tile size, then offsetting those values with the screen size divided by the tile size. Simple.
It might not look like much yet since all the tiles are the same, but the foundation is almost there. The only things left to do are to create different types of tiles and design our map so that the they line up and create something nice.
Editing Tile Layouts for Different Levels
All right, we now have a map that is full of ones which covers the screen. By this point you should have more than one tile type, which means that we have to change our Tile
constructor to account for that:
public function new(id:Int) { super(); switch(id) { case 1: image = new Bitmap(Assets.getBitmapData("assets/grassLeftBlock.png")); case 2: image = new Bitmap(Assets.getBitmapData("assets/grassCenterBlock.png")); case 3: image = new Bitmap(Assets.getBitmapData("assets/grassRightBlock.png")); case 4: image = new Bitmap(Assets.getBitmapData("assets/goldBlock.png")); case 5: image = new Bitmap(Assets.getBitmapData("assets/globe.png")); case 6: image = new Bitmap(Assets.getBitmapData("assets/mushroom.png")); } addChild(image); }
Since I used six different tiles for my map, I needed a switch
statement that covers the numbers one through six. You’ll notice that the constructor now takes an integer as a parameter so we know what kind of tile to create.
Now, we have to go back to our Main
constructor and fix the tile creation in our for
loop:
for (i in 0...map.length) { var tile = new Tile(map[i]); var x = i % w; var y = Math.floor(i / w); tile.setLoc(x, y); addChild(tile); }
All we had to do was pass the map[i]
to the Tile
constructor in order to make it work again. If you try to run without passing an integer to Tile
, it will give you some errors.
Almost everything is in place, but now we need a way to design maps instead of just filling them with random tiles. First, remove the for
loop in the Main
constructor that sets each element to one. Then we can create our own map by hand:
map = [ 0, 4, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3 ];
If you format the array like I have above, you can easily see how our tiles will be organized. You can also just enter the array as one long line of numbers, but the former is nice because you can see 10 across and 6 down.
When you try to run the program now, you should be getting some null pointer exceptions. The problem is that we are using zeros in our map and our Tile
class doesn’t know what to do with a zero. First, we’ll fix the constructor by adding in a check before we add the image child:
if (image != null) addChild(image);
This quick check makes sure that we aren’t adding any null children to the Tile
objects we create. The last change for this class is the setLoc()
function. Right now we are trying to set the x
and y
values for a variable that hasn’t been initialized.
Let’s add another quick check:
public function setLoc(x:Int, y:Int) { if (image != null) { image.x = x * Main.TILE_WIDTH; image.y = y * Main.TILE_HEIGHT; } }
With those two simple fixes in place, and the tiles that I provided above, you should be able to run the game and see a simple platformer level. Since we left 0 as a “non-tile”, we can leave it transparent (empty). If you want to spice up the level, you can put a background in before you lay out the tiles; I just added a light blue gradient to look like a sky in the background.
That is pretty much everything you need to set up a simple way to edit your levels. Try to experiment a little with the array and see what designs you can come up with for other levels.
Third-Party Software for Designing Levels
Once you’ve got the basics down, you might consider using some other tools to help you design levels rapidly and see what they look like before you toss them into the game. One option is to create your own level editor. The alternative is to grab some software that is available for free to use. The two popular tools are Tiled Map Editor and Ogmo Editor. They both make level editing much easier with multiple export options.
- Introduction to Tiled Map Editor: A Great, Platform-Agnostic Tool for Making Level Maps
- Getting to Know Ogmo Editor: An Advanced and Robust Level Editor
You Just Created a Tile-Based Engine!
Now you know what a tile-based game is, how to get some tiles to use for your levels, and how to get your hands dirty and write the code for your engine, and you can even do some basic level editing with a simple array. Just remember that this is only the beginning; there is a lot of experimenting you can do to make an even better engine.
Also, I’ve provided the source files for you to check out. Here’s what the final SWF looks like:
…and here, again, is the array that it’s generated from:
map = [ 0, 4, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3 ];
Don’t Stop Here!
Your next task is to do some research and try out some new things to improve what you made here. Sprite sheets are a great way to improve performance and make life a bit easier. The trick is to get some solid code laid out for reading from a sprite sheet so that your game doesn’t have to read each image individually.
You should also start practising your art skills by making a few tilesets of your own. That way you can get the right art for your future games.
As always, leave some comments below about how your engine is working for you – and maybe even some demos so we can try out your next tile game.