Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Parsing and Rendering Tiled TMX Format Maps in Your Own Game Engine

$
0
0

In my previous article, we looked at Tiled Map Editor as a tool for making levels for your games. In this tutorial, I’ll take you through the next step: parsing and rendering those maps in your engine.

Note: Although this tutorial is written using Flash and AS3, you should be able to use the same techniques and concepts in almost any game development environment.


Requirements


Saving in XML Format

Using the TMX specification we can store the data in a variety of ways. For this tutorial we will be saving our map in the XML format. If you plan on using the TMX file included in the requirements section you can skip to the next section.

If you made your own map you will need to tell Tiled to save it as XML. To do this, open your map with Tiled, and select Edit > Preferences…

For the “Store tile layer data as:” dropdown box, select XML, as shown in the image below:

Now when you save the map it will be stored in XML format. Feel free to open the TMX file with a text editor to take a peek inside. Here’s a snippet of what you can expect to find:

<map version="1.0" orientation="orthogonal" width="20" height="20" tilewidth="32" tileheight="32">
 <tileset firstgid="1" name="grass-tiles-2-small" tilewidth="32" tileheight="32">
  <image source="grass-tiles-2-small.png" width="384" height="192"/>
 </tileset>
 <tileset firstgid="73" name="tree2-final" tilewidth="32" tileheight="32">
  <image source="tree2-final.png" width="256" height="256"/>
 </tileset>
 <layer name="Background" width="20" height="20">
  <data>
   <tile gid="13"/>
   <tile gid="2"/>
   <tile gid="1"/>
   ...
  </data>
 </layer>
 <layer name="Top" width="20" height="20">
  <data>
   <tile gid="0"/>
   ...
  </data>
 </layer>
 <objectgroup name="Collision" width="20" height="20">
  <object x="287" y="354" width="127" height="60"/>
 </objectgroup>
</map>

As you can see, it simply stores all the map information in this handy XML format. The properties should mostly be straightforward, with the exception of gid – I will go into a more in-depth explanation of this later on in the tutorial.

Before we move on, I would like to direct your attention to the objectgroupCollision” element. As you may recall from the map creation tutorial, we specified the collision area around the tree; this is how it is stored.

You can specify power-ups or player spawn point in the same manner, so you can imagine how many possibilities there are for Tiled as a map editor!


Core Outline

Now here’s a brief rundown on how we will be getting our map into the game:

  1. Read in the TMX file.
  2. Parse the TMX file as an XML file.
  3. Load all the tileset images.
  4. Arrange the tileset images into our map layout, layer by layer.
  5. Read map object.

Reading in the TMX File

As far as your program is concerned, this is just an XML file, so the first thing we want to do is read it in. Most languages have an XML library for this; in the case of AS3 I will use the XML class to store the XML information and a URLLoader to read in the TMX file.

xmlLoader=new URLLoader();
xmlLoader.addEventListener(Event.COMPLETE, xmlLoadComplete);
xmlLoader.load(new URLRequest("../assets/example.tmx"));

This is a simple file reader for "../assets/example.tmx". It assumes that the TMX file is located in your project directory under the “assets” folder. We just need a function to handle when the file read is complete:

private function xmlLoadComplete(e:Event):void {
   xml = new XML(e.target.data);
   mapWidth = xml.attribute("width");
   mapHeight = xml.attribute("height");
   tileWidth = xml.attribute("tilewidth");
   tileHeight = xml.attribute("tileheight");
   var xmlCounter:uint = 0;

   for each (var tileset:XML in xml.tileset) {
      var imageWidth:uint = xml.tileset.image.attribute("width")[xmlCounter];
      var imageHeight:uint = xml.tileset.image.attribute("height")[xmlCounter];
      var firstGid:uint = xml.tileset.attribute("firstgid")[xmlCounter];
      var tilesetName:String = xml.tileset.attribute("name")[xmlCounter];
      var tilesetTileWidth:uint = xml.tileset.attribute("tilewidth")[xmlCounter];
      var tilesetTileHeight:uint = xml.tileset.attribute("tileheight")[xmlCounter];
      var tilesetImagePath:String = xml.tileset.image.attribute("source")[xmlCounter];
      tileSets.push(new TileSet(firstGid, tilesetName, tilesetTileWidth, tilesetTileHeight, tilesetImagePath, imageWidth, imageHeight));
      xmlCounter++;
   }
   totalTileSets = xmlCounter;
}

This is where the initial parsing is taking place. (There are a few variables we will keep track of outside this function since we will use them later.)

Once we have the map data stored, we move onto parsing each tileset. I’ve created a class to store each tileset’s information. We’ll push each of those class instances in an array since we will be using them later:

public class TileSet
{
   public var firstgid:uint;
   public var lastgid:uint;
   public var name:String;
   public var tileWidth:uint;
   public var source:String;
   public var tileHeight:uint;
   public var imageWidth:uint;
   public var imageHeight:uint;
   public var bitmapData:BitmapData;
   public var tileAmountWidth:uint;

   public function TileSet(firstgid, name, tileWidth, tileHeight, source, imageWidth, imageHeight)
   {
      this.firstgid = firstgid;
      this.name = name;
      this.tileWidth = tileWidth;
      this.tileHeight = tileHeight;
      this.source = source;
      this.imageWidth = imageWidth;
      this.imageHeight = imageHeight;
      tileAmountWidth = Math.floor(imageWidth / tileWidth);
      lastgid = tileAmountWidth * Math.floor(imageHeight / tileHeight) + firstgid - 1;
   }
}

Again, you can see that gid appears again, in the firstgid and lastgid variables. Let’s now look at what this is for.


Understanding “gid

For each tile, we need to somehow associate it with a tileset and a particular location on that tileset. This is the purpose of the gid.

Look at the grass-tiles-2-small.png tileset. It contains 72 distinct tiles:

We give each of these tiles a unique gid from 1-72, so that we can refer to any one with a single number. However, the TMX format only specifies the first gid of the tileset, since all of the other gids can be derived from knowing the size of the tileset and the size of each individual tile.

Here’s a handy image to help visualize and explain the process.

So if we placed the bottom right tile of this tileset on a map somewhere, we would store the gid 72 at that location on the map.

Now, in the example TMX file above, you will notice that tree2-final.png has a firstgid of 73. That’s because we continue counting up on the gids, and we don’t reset it to 1 for each tileset.

In summary, a gid is a unique ID given to each tile of each tileset within a TMX file, based on the position of the tile within the tileset, and the number of tilesets referred to in the TMX file.


Loading the Tilesets

Now we want to load all the tileset source images into memory so we can put our map together with them. If you aren’t writing this in AS3, the only thing you need to know is that we’re loading the images for each tileset here:

// load images for tileset
for (var i = 0; i < totalTileSets; i++) {
   var loader = new TileCodeEventLoader();
   loader.contentLoaderInfo.addEventListener(Event.COMPLETE, tilesLoadComplete);
   loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, progressHandler);
   loader.tileSet = tileSets[i];
   loader.load(new URLRequest("../assets/" + tileSets[i].source));
   eventLoaders.push(loader);
}

There’s a few AS3-specific things going on here, such as using the Loader class to bring in the tileset images. (More specifically, it is an extended Loader, simply so we can store the TileSet instances inside each Loader. This is so that when the loader completes we can easily correlate the Loader with the tileset.)

This may sound complicated but the code is really quite simple:

public class TileCodeEventLoader extends Loader
{
   public var tileSet:TileSet;
}

Now before we start taking these tilesets and creating the map with them we need to create a base image to put them on:

screenBitmap = new Bitmap(new BitmapData(mapWidth * tileWidth, mapHeight * tileHeight, false, 0x22ffff));
screenBitmapTopLayer = new Bitmap(new BitmapData(mapWidth*tileWidth,mapHeight*tileHeight,true,0));

We will be copying the tile data onto these bitmap images so that we can use them as a background. The reason I set up two images is so we can have a top layer and a bottom layer, and have the player move in-between them in order to provide perspective. We also specify that the top layer should have an alpha channel.

For the actual event listeners for the loaders we can use this code:

private function progressHandler(event:ProgressEvent):void {
   trace("progressHandler: bytesLoaded=" + event.bytesLoaded + " bytesTotal=" + event.bytesTotal);
}

This is a fun function since you can track how far the image has loaded, and can therefore provide feedback to the user about how fast things are going, such as a progress bar.

private function tilesLoadComplete (e:Event):void {
   var currentTileset = e.target.loader.tileSet;
   currentTileset.bitmapData = Bitmap(e.target.content).bitmapData;
   tileSetsLoaded++;
   // wait until all the tileset images are loaded before we combine them layer by layer into one bitmap
   if (tileSetsLoaded == totalTileSets)
   {
      addTileBitmapData();
   }
}

Here we store the bitmap data with the tileset associated with it. We also count how many tilesets have completely loaded, and when they’re all done, we can call a function (I named it addTileBitmapData in this case) to start putting the tile pieces together.


Combining the Tiles

To combine the tiles into a single image, we want to build it up layer by layer so it will be displayed the same way the preview window in Tiled appears.

Here is what the final function will look like; the comments I’ve included within the source code should adequately explain what’s going on without getting too messy into the details. I should note that this can be implemented in many different ways, and your implementation can look completely different than mine.

private function addTileBitmapData():void {
// load each layer
for each (var layer:XML in xml.layer) {
   var tiles:Array = new Array();
   var tileLength:uint = 0;
   // assign the gid to each location in the layer
   for each (var tile:XML in layer.data.tile) {
      var gid:Number = tile.attribute("gid");
      // if gid > 0
      if (gid > 0) {
         tiles[tileLength] = gid;
      }
      tileLength++;
   }
   // outer for loop continues in next snippets

What’s happening here is we’re parsing only the tiles with gids that are above 0, since 0 indicates an empty tile, and storing them in an array. Since there are so many “0 tiles” in our top layer, it would be inefficient to store all of them in memory. It’s important to note that we’re storing the location of the gid with a counter because we will be using its index in the array later.

var useBitmap:BitmapData;
var layerName:String = layer.attribute("name")[0];
// decide where we're going to put the layer
var layerMap:int = 0;
switch(layerName) {
   case "Top":
      layerMap = 1;
      break;
   default:
      trace("using base layer");
}

In this section we’re parsing out the layer name, and checking if it’s equal to “Top”. If it is, we set a flag so we know to copy it onto the top bitmap layer. We can be really flexible with functions like this, and use even more layers arranged in any order.

// store the gid into a 2d array
var tileCoordinates:Array = new Array();
for (var tileX:int = 0; tileX < mapWidth; tileX++) {
   tileCoordinates[tileX] = new Array();
   for (var tileY:int = 0; tileY < mapHeight; tileY++) {
      tileCoordinates[tileX][tileY] = tiles[(tileX+(tileY*mapWidth))];
   }
}

Now here we’re storing the gid, which we parsed at the beginning, into a 2D array. You’ll notice the double array initializations; this is simply a way of handling 2D arrays in AS3.

There’s a bit of math going on as well. Remember when we initialized the tiles array from above, and how we kept the index with it? We will now use the index to calculate the coordinate that the gid belongs to. This image demonstrate what’s going on:

So for this example, we get the gid at index 27 in the tiles array, and store it at tileCoordinates[7][1]. Perfect!

   for (var spriteForX:int = 0; spriteForX < mapWidth; spriteForX++) {
      for (var spriteForY:int = 0; spriteForY < mapHeight; spriteForY++) {          var tileGid:int = int(tileCoordinates[spriteForX][spriteForY]);          var currentTileset:TileSet;          // only use tiles from this tileset (we get the source image from here)          for each( var tileset1:TileSet in tileSets) {             if (tileGid >= tileset1.firstgid-1 && tileGid             {
               // we found the right tileset for this gid!
               currentTileset = tileset1;
               break;
            }
         }

         var destY:int = spriteForY * tileWidth;
         var destX:int = spriteForX * tileWidth;

         // basic math to find out where the tile is coming from on the source image
         tileGid -= currentTileset.firstgid -1 ;
         var sourceY:int = Math.ceil(tileGid/currentTileset.tileAmountWidth)-1;
         var sourceX:int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1;

         // copy the tile from the tileset onto our bitmap
         if(layerMap == 0) {
            screenBitmap.bitmapData.copyPixels(currentTileset.bitmapData, new Rectangle(sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileHeight), new Point(destX, destY), null, null, true);
         }
         else if (layerMap == 1) {
            screenBitmapTopLayer.bitmapData.copyPixels(currentTileset.bitmapData, new Rectangle(sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileHeight), new Point(destX, destY), null, null, true);
         }
      }
   }
}

This is where we finally get down to copying the tileset into our map.

Initially we start by looping through each tile coordinate on the map, and for each tile coordinate we get the gid and check for the stored tileset that matches it, by checking if it lies between the firstgid and our calculated lastgid.

If you understood the Understanding “gid section from above, this math should make sense. In the most basic terms, it’s taking the tile coordinate on the tileset (sourceX and sourceY) and copying it onto our map at the tile location we’ve looped to (destX and destY).

Finally, at the end we call the copyPixel function to copy the tile image onto either the top or base layer.


Adding Objects

Now that copying the layers onto the map is done, let’s look into loading the collision objects. This is very powerful because as well as using it for collision objects, we can also use it for any other object, such as a power-up, or a player spawn location, just as long as we’ve specified it with Tiled.

So at the bottom of the addTileBitmapData function, let’s put in the following code:

for each (var objectgroup:XML in xml.objectgroup) {
   var objectGroup:String = objectgroup.attribute("name");
   switch(objectGroup) {
      case "Collision":
         for each (var object:XML in objectgroup.object) {
            var rectangle:Shape = new Shape();
            rectangle.graphics.beginFill(0x0099CC, 1);
            rectangle.graphics.drawRect(0, 0, object.attribute("width"), object.attribute("height") );
            rectangle.graphics.endFill();
            rectangle.x = object.attribute("x");
            rectangle.y = object.attribute("y");
            collisionTiles.push(rectangle);
            addChild(rectangle);
         }
         break;
      default:
         trace("unrecognized object type:", objectgroup.attribute("name"));
   }
}

This will loop through the object layers, and look for the layer with the name “Collision“. When it finds it, it takes each object in that layer, creates a rectangle at that position and stores it in the collisionTiles array. That way we still have a reference to it, and we can loop through to check it for collisions if we had a player.

(Depending on how your system handles collisions, you may want to do something different.)


Displaying the Map

Finally, to display the map, we want to first render the background and then the foreground, in order to get the layering correct. In other languages, this is simply a matter of rendering the image.

// load background layer
addChild(screenBitmap);

// rectangle just to demonstrate how something would look in-between layers
var playerExample:Shape = new Shape();
playerExample.graphics.beginFill(0x0099CC, 1);
playerExample.graphics.lineStyle(2); // outline rectangle
playerExample.graphics.drawRect(0, 0, 100, 100 );
playerExample.graphics.endFill();
playerExample.x = 420;
playerExample.y = 260;
collisionTiles.push(playerExample);
addChild(playerExample);

// load top layer
addChild(screenBitmapTopLayer);

I’ve added a bit of code in between the layers here just to demonstrate with a rectangle that the layering does indeed work. Here’s the final result:

Thank you for taking the time to complete the tutorial. I’ve included a zip containing a complete FlashDevelop project with all of the source code and assets.


Additional Reading

If you’re interested in doing more things with Tiled, one thing I didn’t cover was properties. Using properties is a small jump from parsing the layer names, and it allows you to set a large number of options. For example, if you wanted an enemy spawn point, you could specify the type of enemy, the size, the colour, and everything, from inside the Tiled map editor!

Lastly, as you may have noticed, XML is not the most efficient format to store the TMX data. CSV is a nice medium between easy parsing and better storage, but there is also base64 (uncompressed, zlib compressed, and gzip compressed). If you’re interested in using these formats instead of XML, check out the Tiled wiki’s page on the TMX format.


Viewing all articles
Browse latest Browse all 728

Trending Articles