Map Tiling in AS3 was the first post of my short-lived AS3 Game Engine series before I found Starling. While the core tile drawing code remains mostly the same, there have been some changes. We saw from yesterday’s post that the Play State creates an instance of the Map class and passes it the map JSON data Object. Let’s take a look at the JSON file that holds the map data, then we’ll look at the Map class and see what it does.

[toc]

Feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

I’m not sure if I’ve mentioned this yet, but I am using TexturePacker to create my spritesheets/tilesheets (is there a difference? semantics?). TexturePacker makes creating these tilesheets stupid easy and quick. If you can save your png images into a folder, and if you can remember that folder so you can find that very same folder again… you can make tilesheets using TexturePacker. They have tons of tutorials for using the app, and Lee Brimlow makes a three-part excellent spritesheet/basics-of-blitting tutorials over at GotoAndLearn (part1, part2, part3)

Map JSON Data

We’re going to look at the map data from the second map I made. Here’s a screenshot of the map actually in the game, all drawn out, tile-by-tile.

map2.jpg

These are all tiles I made out of 24×24 tiles in Photoshop. Here’s a sample of the tiles that are in my tile sheet.

tiles.png

This has clearly been enlarged and is a little pixelated, but it’s 24×24 pixel art, there’s really not a whole lot of room for smooth lines. This image is here to show that I’ve basically grouped my tiles in groups of 3. For example, the first three tiles are all the same road piece, left-to-right, but with varying “dirt” sorts of pixels along the road piece. This lets me have the long straight roads for the enemies to travel down, and every tile doesn’t look like the exact same tile. Sure, only 3 variations isn’t really a lot of difference. You could make curved pieces, or “broken road” sorts of pieces, anything really.

Then there’s the single grass tile. Hey, it works right? The single green tile, as we’ll see in the JSON data is our background tile. I set the Map tiler up to take a single background image and make a whole “layer” of this same tile as the background layer. Any further layers are kept in the JSON array below in “tiles” each “layer” consists of however many “row”s there are on the screen. So looking at the “row” info below you can clearly see how it syncs up with the actual drawn map image above. This is a “row” of data, the first two tiles are {} empty, so we draw nothing for this layer in those tiles. The third tile in this row is a vertical road piece. If you go back to the map image above and look in the top left corner, 2 green tiles in, you see a vertical road piece. I’ve structured my JSON data and set up my Map class so that if there is nothing in a tile (“{}”) we skip that tile so we do not overwrite the background in any way. This lets me add layers and layers of detail if I wanted. Which I clearly don’t take advantage of here in this tutorial as there is just a single “layer” on top of the background that just shows a simple road. Rock tiles, trees, level details could all be easily added in subsequent layers to provide an infinite amount of customization to each map.

I won’t go into too much more detail about the JSON data, but I just want to make sure you can look at the JSON here and visualize at least “something” of a structure that you should expect to see in the actual drawn out map. Here are the first three rows of the map, you can look at the image above and come back to the data. The first row has a single vertical road piece in it. The second row has a single vertical road piece in it. Then the third row has the connecting road pieces for the vertical road to connect with the long horizontal road we’re building here.

Quick note about the way I’ve named my textures and images here. You’ll notice the “src” attribute contains something that looks like ‘roads/road1_202’. If you’ve downloaded the project zip file, or checked it out from my bitbucket repo you’ll see that you can go to src/assets/images/tiles and I’ve got all my tile images that I want to go in my spritesheet. So when it references ‘roads/road1_202’ if you go to src/assets/images/tiles/roads/road1_202.png you’ll find the single 24x24px image tile that I’m referencing, and that tile gets drawn into the src/assets/images/atlas.png spritesheet. I named these files this way for a reason, so all of these brown dirt roads in this set belong to my “road1” (“road2” might be snow roads?) set and they’re numbered 1-7 for the different directions so road1_1 for the first direction and the last number is a variation number. So ‘roads/road1_103’ would be the road1 set, the first direction, and the 3rd shading variation of that horizontal tile. That’s just the way I did it, and why I did it that way. Feel free to adopt your own file naming conventions.

{
"mapName": "Second Map!",
"numRows": 22,
"numCols": 22,
"numLayers": 1,
"bkgdSrc": "misc/grass1_001",
"drawBkgd": 1,
"tileWidth": 24,
"tileHeight": 24,
"startHP": 10,
"startGold": 15,
"enemyWaveData": { ....removed.... },
"enemyData": { ....removed.... },
"tiles": [{
  "layer": [{
    "row": [
      {},
      {},
      { "src": "roads/road1_202", "isWalkable": 1, "wp": 1, "wpGroup": "wpGroup1", "wpIndex": 1, "sp": 1, "spDirection": "up" },
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {}
    ]},
    {
    "row": [
      {},
      {},
      { "src": "roads/road1_201", "isWalkable": 1, "groupStartIcon": "t" },
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {},
      {}
    ]},
    {
    "row": [
      {},
      {},
      { "src": "roads/road1_503", "isWalkable": 1, "wp": 1, "wpGroup": "wpGroup1", "wpIndex": 2 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_101", "isWalkable": 1 },
      { "src": "roads/road1_103", "isWalkable": 1 },
      { "src": "roads/road1_102", "isWalkable": 1 },
      { "src": "roads/road1_302", "isWalkable": 1, "wp": 1, "wpGroup": "wpGroup1", "wpIndex": 3 },
      {},
      {}
    ]},

So that’s an except from the src/assets/json/maps/map2.json file, feel free to check it out in its entirety in the project. Let’s move on to how that data is actually used.

Man, quick formatting note, my need for properly indented code is fighting with the fact that the site is only so wide. In the actual files in the project, the public class Map extends Sprite line is properly a tab inside of the package. But as that’d add even more tabs to the actual code (that we really actually care about) I’ve just dropped everything back a tab or two. /OCD-Disclaimer

Map.as

package com.zf.objects.map
{
   import com.zf.core.Assets;
   import com.zf.core.Config;
   import com.zf.managers.WaypointManager;

   import flash.geom.Point;
   import flash.geom.Rectangle;

   import starling.display.Image;
   import starling.display.Sprite;
   import starling.textures.Texture;
   import com.zf.objects.tower.Tower;

public class Map extends Sprite
{
   public var halfTileWidth:Number;
   public var halfTileHeight:Number;
   public var mapWidth:Number;
   public var mapHeight:Number;
   public var tileWidth:int;
   public var tileHeight:int;
   public var mapOffsetX:int;
   public var mapOffsetY:int;

   private var _mapData:Object;
   private var _tileData:Array;
   private var _wpMgr:WaypointManager;

   // Tile data flattened down without layers, aggregating isWalkable data
   private var _flatTileData:Array;

   private var _mapWidthOffset:Number;
   private var _mapHeightOffset:Number;

   public function Map(data:Object, wpMgr:WaypointManager, mOffsetX:int, mOffsetY:int) {
      _mapData = data;
      _wpMgr = wpMgr;

      mapOffsetX = mOffsetX;
      mapOffsetY = mOffsetY;
      tileWidth = _mapData.tileWidth;
      tileHeight = _mapData.tileHeight;
      halfTileWidth = _mapData.tileWidth >> 1;
      halfTileHeight = _mapData.tileHeight >> 1;
      mapWidth = _mapData.tileWidth * _mapData.numCols;
      mapHeight = _mapData.tileHeight * _mapData.numRows;
      _mapWidthOffset = mapWidth + mapOffsetX;
      _mapHeightOffset = mapHeight + mapOffsetY;

      // initial layer level
      _tileData = [];
      _flatTileData = [];

      _parseMapDataIntoTileData();
      _drawMap();
   }

   private function _parseMapDataIntoTileData():void {
      var rowCt:int = 0,
          colCt:int = 0,
          layerCt:int = 0,
          mapLayerCt:int = 0;

      if(_mapData.drawBkgd) {
         var data:Object = {"src": _mapData.bkgdSrc, "isWalkable": false };
         _createBkgdLayerTileData(data);
         layerCt++;
      } else {
         mapLayerCt = layerCt;
      }

      for(layerCt; layerCt <= _mapData.numLayers; layerCt++)
      {
         _tileData[layerCt] = [];
         for(rowCt = 0; rowCt < _mapData.numRows; rowCt++)
         {
            _tileData[layerCt][rowCt] = [];
            for(colCt = 0; colCt < _mapData.numCols; colCt++)
            {
               // map data doesnt have the bkgd layer
               var tileData2:TileData = new TileData(_mapData.tiles[mapLayerCt].layer[rowCt].row[colCt]);

               _tileData[layerCt][rowCt][colCt] = tileData2;

               if(_flatTileData[rowCt][colCt] is TileData)
               {
                  if(tileData2.isWalkable)
                  {
                     // if any tiledata is walkable, set the position to true
                     _flatTileData[rowCt][colCt].isWalkable = true;
                     _flatTileData[rowCt][colCt].srcImageName = tileData2.srcImageName;
                  }
               }
               else
               {
                  _flatTileData[rowCt][colCt] = tileData2;
               }
            }
         }
         mapLayerCt++;
      }
   }

   private function _createBkgdLayerTileData(data:Object):void {
      var rowCt:int = 0,
          colCt:int = 0;

      // create the first empty row array
      _tileData[0] = [];

      for(rowCt; rowCt < _mapData.numRows; rowCt++)
      {
         _tileData[0][rowCt] = [];

         if(_flatTileData[rowCt] == undefined) {
            _flatTileData[rowCt] = [];
         }

         for(colCt = 0; colCt < _mapData.numCols; colCt++)
         {
            var bkgdTile:TileData = new TileData(data);
            _tileData[0][rowCt][colCt] = bkgdTile;

            // get another tile for _flatTileData
            bkgdTile = new TileData(data);
            _flatTileData[rowCt][colCt] = bkgdTile;
         }
      }
   }

   

Taking a quick break so we can keep the scrolling down a little... Ok continuing on in Map.as

   private function _drawMap():void {
      var currentLayer:int = 0,
          maxLayers:int = _mapData.numLayers;

      if(_mapData.drawBkgd) {
         maxLayers++;
      }

      for(currentLayer; currentLayer < maxLayers; currentLayer++) {
         _drawLayer(currentLayer);
      }

      // flatten this map once it has been drawn
      this.flatten();

      // Map is done drawing and adding waypoints, so lets clean up WaypointManager
      _wpMgr.handleEndpointAndSort();
   }

   private function _drawLayer(layer:int):void {
      var srcImage:Image,
          destPt:Point = new Point(),
          destRect:Rectangle = new Rectangle(0, 0, _mapData.tileWidth, _mapData.tileHeight),
          rowCt:int = 0,
          colCt:int = 0,
          tileData:TileData,
          pt:Point = new Point();

      for(rowCt; rowCt < _mapData.numRows; rowCt++)
      {
         for(colCt = 0; colCt < _mapData.numCols; colCt++) 
         {
            tileData = _tileData[layer][rowCt][colCt];
					
            // if this tile does not have a source image name for this layer, skip drawing it
            if(tileData.srcImageName == '') {
               continue;
            }

            var tmpTexture:Texture = Assets.ta.getTexture(tileData.srcImageName),
		xOffset:int = (_mapData.tileWidth - tmpTexture.width) >> 1,
                yOffset:int = (_mapData.tileHeight - tmpTexture.height) >> 1,
                rect:Rectangle = new Rectangle(-xOffset, -yOffset, _mapData.tileWidth, _mapData.tileHeight),
                texture:Texture = Texture.fromTexture(tmpTexture, null, rect);

            srcImage = new Image(texture);
            srcImage.x = pt.x = (colCt * _mapData.tileWidth);
            srcImage.y = pt.y = (rowCt * _mapData.tileHeight);

            addChild(srcImage);

            if(tileData.isWaypoint) {
               pt.x += mapOffsetX;
               pt.y += mapOffsetY;
               _wpMgr.addWaypoint(tileData, pt, halfTileWidth, halfTileHeight);
            }

            if(tileData.groupStartIconDir != '') {
               // Adding mapOffsets again here because isWaypoint and groupStartIconDir
               // will never exist on the same tile
               pt.x += mapOffsetX;
               pt.y += mapOffsetY;
               _wpMgr.addGroupStartPosition(tileData.groupStartIconDir,
               pt, halfTileWidth, halfTileHeight);
            }
         }
      }
   }

   public function destroy():void {
      removeFromParent(true);
   }

   public function checkCanPlaceTower(p:Point):Object {
      var retObj:Object = {};
      retObj.canPlace = false;
      retObj.point = new Point(-1, -1);
      // make sure we clicked inside the actual map before further checks
      if(clickedInsideMap(p)) {
         // p is a reference to coordinates from the stage, we want to remove the offset
         // to get just the coordinates relative to the map itself for the array keys
         var rowCt:int = int((p.y - mapOffsetX) / _mapData.tileHeight),
             colCt:int = int((p.x - mapOffsetY) / _mapData.tileWidth),
             t:TileData = _flatTileData[rowCt][colCt];

         // If isWalkable == true or isTower == true, then canPlace = false, 
         // we cannot place tower on walkway/tower
         retObj.canPlace = ((_flatTileData[rowCt][colCt].isWalkable == false) 
                           && (_flatTileData[rowCt][colCt].isTower == false));

         if(retObj.canPlace) {
            retObj.point.x = colCt * _mapData.tileWidth;
            retObj.point.y = rowCt * _mapData.tileHeight;
         }
      }
      return retObj;
   }

   public function placedTower(t:Tower):void {
      var rowCt:int = int((t.y - mapOffsetX) / _mapData.tileHeight),
          colCt:int = int((t.x - mapOffsetY) / _mapData.tileWidth);

      _flatTileData[rowCt][colCt].isTower = true;
   }

   public function removeTower(t:Tower):void {
      var rowCt:int = int((t.y - mapOffsetX) / _mapData.tileHeight);
          colCt:int = int((t.x - mapOffsetY) / _mapData.tileWidth);

      _flatTileData[rowCt][colCt].isTower = false;
   }

   public function clickedInsideMap(p:Point):Boolean {
      var clickedMap:Boolean = false;
      if(p.x > 0 && p.x < _mapWidthOffset && p.y > 0 && p.y < _mapHeightOffset)  
      {
         clickedMap = true;
      }
      return clickedMap;
   }

   public function get paddedBounds():Rectangle {
      var paddedBoundsRect:Rectangle = new Rectangle();
      paddedBoundsRect.x -= tileWidth - mapOffsetX;
      paddedBoundsRect.y -= tileHeight - mapOffsetY;
      paddedBoundsRect.width = this.width + tileWidth + mapOffsetX;
      paddedBoundsRect.height = this.height + tileHeight + mapOffsetY;
      return paddedBoundsRect;
   }

   public function get startHP():int {
      return _mapData.startHP;
   }

   public function get startGold():int {
      return _mapData.startGold;
   }
}
}

So there's the Map class, that's how I handle map tile data from JSON to "blitting" from a tilesheet in Starling.

As always, feel free to check out the finished product of my AS3 Starling TD Demo.
You can also find all of the code used in srcview.
Or you can download the whole project zipped up.
Or check out the repo on Bitbucket

Until next time, thanks for reading and I hope this was helpful

-Travis

Categories:

3 Comments

Leave a Reply

Avatar placeholder