Creating_Tilemaps_Axel_featured

Designing a level for a simple tile-based game doesn’t have to be a painful process of concatenating hardcoded Strings or writing text-files.

Instead, you can draw the levels out in a graphical tool, export them and read the pixels in your game to feed them into a Tilemap class.

In this tutorial, I’ll be using:

  • Photoshop as my graphic editing program;
  • FlashDevelop as my ActionScript IDE, and;
  • Axel as my rendering engine (with… you guessed it! Stage3D support!);

Final Preview

This area requires
Adobe FlashPlayer version 11.2 or above.
iOS Devices are not currently supported.

Sources Available here (ZIP)

Creating the Project

To get started, go ahead and create a new AS3 Project:

Choose a name for the project. In my case, I entered “AxelTilemap_Tutorial_01“:

Once created, you will need to configure the project to publish for Flash Player 11.2 like so:

While you’re at it, set the dimensions and framerate of your project:

If it isn’t done automatically, verify that you have the correct Flex SDK selected to publish your project for Flash Player 11. The Flex SDK version 4.6 will work, as long the FP11.2 playerglobals.swc file exists in it (for more info on this, please enter your question in the comment form at the bottom of the page).

Now, in order to use Axel in your project, you will need to grab the source files here first. I recommend dumping those files in a folder where you will reuse many AS3 frameworks to come over the years. Once you downloaded the Axel sources, add it to your classpath like so:

Bam! All done configuring the project!

Next, let’s start drawing our Tile Types in one image file.

Drawing Tile Types in a SpriteSheet

Depending on the resolution of your game, you may want to choose different settings to create crisper graphics. Since I’ll be demonstrating mostly how to draw 8-bit graphics, I will be using an image size of 16px by 128px:

Notice how the tiles are arranged. Because of the way Axel’s Tilemap class works, it is recommended to place all solid tiles at the end  of the spritesheet. In doing it this way, you are telling the Axel game engine that all tiles beyond a certain tile ID can have collision-detection applied on them. I’ll get back to this later.

Note: You can download the original PNG file here if you would like to reuse the same assets in your project.

Once you have created a set of tiles you’re happy with, let’s move on to designing the actual level maps!

Choosing a Map size

One thing you have to realize when creating a map, is that each individual pixels will be converted to an actual tile from the set you’ve drawn in the previous step. On top of that, if you use a “zoom” value greater than 1 for your Axel camera, that will also be multiplied to your width and height. So if one single pixel is transformed to a 16px by 16px tile size with a 2x zoom world, your whole map will be 32x wider and 32x higher than the map dimensions.

So knowing this, I’ll simply design mine as a 64px by 64px map, which will be rendered in the game as 2048px by 2048px Tilemap world. It is normal that these final dimensions goes beyond the stage size, as you want to be able to let the player explore the level progressively.

Setting a color palette

Limiting yourself to a set of colors that represent your “tiles palette” will greatly help you focus and select the color for each respective tile types you have defined in your spritesheet. Sticking to strong Web colors should do the trick. Feel free to create your own preset of color swatches from scratch if you will be using it on a regular basis.

Drawing the Background

As you can see, drawing the background is pretty straightforward. Using the Pencil tool, we can draw fully opaque solid pixels of a given color swatch.

Separating Foreground Elements from the Background

In order to draw certain tiles on the foreground (on top of the player, enemies, or other game entities), you will need to separate those tiles on an additional layer. Also, just to stay organized, rename the layers “FG_LAYER” and “BG_LAYER”:

Exporting each separate layers as PNGs

Once you are ready to export your layers to separate PNGs, click on “File >; Scripts >; Export Layers to Files… “.

Once the panel opens, you can decide on a filename prefix, meaning that your exported layer images will all share a common beginning filename, followed by a 4-digit number and the layer-name.

Note: You must select PNG-24 as the file format, with Transparency enabled and un-check “Trim Layers”. Leaving the “Trim Layers” option checked can cause some layers to export differently in size, which will result incorrect reading in the game at runtime.

Bringing the map in the game

It’s a good idea to move the exported images in a sub-directory part of the AS3 Project you have created. Drop them into a new folder named “assets” that you can create under the “src” folder:

To embed those graphical assets in our game, we need to designate a Class file that will use the [Embed]  metadata tag to bind a reference to our images with some static constants. Here’s what the code looks like:

//Assets.as file
package assets 
{
	/**
	 * ...
	 * @author Pierre Chamberlain
	 */
	public class Assets 
	{
		[Embed(source="tile_types.png")]
		public static const IMG_TILE_TYPES:Class;
		
		[Embed(source="tile_level_01_0001_BG.png")]
		public static const IMG_TILE_LEVEL_01_BG:Class;
		
		[Embed(source="tile_level_01_0000_FG.png")]
		public static const IMG_TILE_LEVEL_01_FG:Class;
	}
}

Before we use those static constants, let’s add a few more Classes that will be used in our game. Let’s add:

  • Player: This will be our controllable character on the screen (a white block, for now!);
  • StateGame: The game’s first state, which instantiates our map, and Player character;
  • MyTileMap: A custom Tilemap class to read our images and handle the foreground and background Axel  Tilemaps;

Here’s how I structured the classes in the project:

Parsing the bitmap to CSV (comma separated values)

To read the images from BitmapData objects, we need to loop through each pixels of the image. Knowing the width and height of the BitmapData, we can achieve this like so:

//MyTileMap.as (adding a new private method...)

/**
 * Generates an AxTilemap object from a Bitmap image and a SpriteSheet of tiles.
 * 
 * @param	pBitmapCls		The world map image to read the pixels from.
 * @param	pTileTypesCls	The spritesheet to convert the pixels to tiles.
 * @return
 */
private function generateMap(pBitmapCls:Class, pTileTypesCls:Class):AxTilemap
{
	var tileMap:AxTilemap =	new AxTilemap();
	
	//Instantiate the Bitmap, and store it's BitmapData
	var theBitmap:BitmapData =	new pBitmapCls().bitmapData;
	var theData:Array =			[];
	var rowData:Array =			[];
	
	var x:int, y:int;
	for (y = 0; y <; theBitmap.height; y++) {
		for (x = 0; x <; theBitmap.width; x++) {
			rowData[rowData.length] = convertToTypeID(theBitmap.getPixel(x, y));
		}
		
		theData[theData.length] = rowData.join(&quot;,&quot;);
		rowData.length = 0;	//Reset row data for next loop
	}
	
	theBitmap.dispose();	//No longer required
	
	//Joined data, spritesheet image, tile width, tile height, and start ID of solid tiles
	tileMap.build(theData.join(&quot;\n&quot;), pTileTypesCls, 16, 16, 5);
	
	return tileMap;
}

The above method will be used to return either a foreground or a background Tilemap depending on which image it is supplied in the arguments. Notice there is a method named “convertToTypeID() ” used to lookup the pixel-value and convert it to the spritesheet’s tile-ID. There’s a bit of “hardcoding ” involved to bind the level colors to the tile-ID, but this works fine for this small project.

Here’s the entire code for the MyTileMap class:

package game 
{
	import assets.Assets;
	import flash.display.BitmapData;
	import flash.utils.Dictionary;
	import game.states.StateGame;
	import org.axgl.Ax;
	import org.axgl.AxEntity;
	import org.axgl.tilemap.AxTilemap;
	
	/**
	 * ...
	 * @author Pierre Chamberlain
	 */
	public class MyTileMap 
	{
		public static var KNOWN_TYPES:Dictionary;
		
		public var mapBackground:AxTilemap;
		public var mapForeground:AxTilemap;
		
		public function MyTileMap(pBGClass:Class, pFGClass:Class) 
		{
			initClass();
			
			mapBackground =	generateMap(pBGClass, Assets.IMG_TILE_TYPES);
			mapForeground = generateMap(pFGClass, Assets.IMG_TILE_TYPES);
			
			mapForeground.alpha = .8;
			mapForeground.getTile(1).callback =	onWaterOut;
			mapForeground.getTile(3).callback =	onWaterIn;
		}
		
		public static function initClass():void {
			if (KNOWN_TYPES) {
				return;
			}
			
			KNOWN_TYPES =	new Dictionary();
			KNOWN_TYPES[0x00ffff] =	1;
			KNOWN_TYPES[0x00ff00] =	6;
			KNOWN_TYPES[0x603913] =	2;
			KNOWN_TYPES[0x8c6239] =	8;
			KNOWN_TYPES[0x754c24] =	7;
			KNOWN_TYPES[0xff0000] =	4;
			KNOWN_TYPES[0xff8800] =	5;
			KNOWN_TYPES[0xffffff] =	0;
			KNOWN_TYPES[0x0000ff] =	3;
		}
		
		/**
		 * Generates an AxTilemap object from a Bitmap image and a SpriteSheet of tiles.
		 * 
		 * @param	pBitmapCls		The world map image to read the pixels from.
		 * @param	pTileTypesCls	The spritesheet to convert the pixels to tiles.
		 * @return
		 */
		private function generateMap(pBitmapCls:Class, pTileTypesCls:Class):AxTilemap
		{
			var tileMap:AxTilemap =	new AxTilemap();
			
			//Instantiate the Bitmap, and store it's BitmapData
			var theBitmap:BitmapData =	new pBitmapCls().bitmapData;
			var theData:Array =			[];
			var rowData:Array =			[];
			
			var x:int, y:int;
			for (y = 0; y <; theBitmap.height; y++) {
				for (x = 0; x <; theBitmap.width; x++) {
					rowData[rowData.length] = convertToTypeID(theBitmap.getPixel(x, y));
				}
				
				theData[theData.length] = rowData.join(&quot;,&quot;);
				rowData.length = 0;	//Reset row data for next loop
			}
			
			theBitmap.dispose();	//No longer required
			
			//Joined data, spritesheet image, tile width, tile height, and start ID of solid tiles
			tileMap.build(theData.join(&quot;\n&quot;), pTileTypesCls, 16, 16, 5);
			
			return tileMap;
		}
		
		private function convertToTypeID(pixel:uint):int {
			if (KNOWN_TYPES[pixel] == null) {
				KNOWN_TYPES[pixel] =	0;
			}
			
			return KNOWN_TYPES[pixel];
		}
		
		private function onWaterIn(pA:AxEntity, pB:AxEntity):void {
			stateGame.player.isInWater = 		true;
		}
		
		private function onWaterOut(pA:AxEntity, pB:AxEntity):void {
			stateGame.player.isInWater = 		false;
		}
		
		public function get stateGame():StateGame { return Ax.state as StateGame; }
	}
}

You will notice that the player has a property that flags it as being inside a water tile. The way this works, is by assigning a callback method on the water  tile and the empty  tile of the foreground AxTilemap object. Since those are the only two types of tiles that exists on our foreground, we know the callback methods are guaranteed to be called.

Note: This may not be the cleanest way to change the player’s swim-state  depending on which tile it overlaps, but as long we’re only dealing with two tile types in the entire foreground layer, this technique shouldn’t cause any trouble.

Detecting collision on solids

To have our player collide against the tiles we have designated as “solid“, we need to call Ax.collide() on the game update cycle.

Here’s how:

//StateGame.as file
package game.states 
{
	import assets.Assets;
	import game.entities.Player;
	import game.MyTileMap;
	import org.axgl.Ax;
	import org.axgl.AxGroup;
	import org.axgl.AxState;
	/**
	 * ...
	 * @author Pierre Chamberlain
	 */
	public class StateGame extends AxState
	{
		private var _map:MyTileMap;
		
		public var entities:AxGroup;
		public var player:Player;
		
		public override function create():void 
		{
			super.create();
			
			entities =	new AxGroup();
			
			_map = new MyTileMap(Assets.IMG_TILE_LEVEL_01_BG, Assets.IMG_TILE_LEVEL_01_FG);
			
			add(_map.mapBackground);
			add(entities);
			add(_map.mapForeground);
			
			Ax.camera.bounds.x =		0;
			Ax.camera.bounds.y =		0;
			Ax.camera.bounds.width =	_map.mapBackground.width;
			Ax.camera.bounds.height =	_map.mapBackground.height;
			
			player =	new Player(100, 100);
			entities.add( player );
		}
		
		public override function update():void 
		{
			super.update();
			
			player.isInWater = false;
			
			Ax.collide(entities, _map.mapBackground);
			Ax.overlap(player, _map.mapForeground);
		}
	}
}

To simply check for tiles overlapping an entity, we use Ax.overlap(). This triggers the water tiles’ callback method if they overlap with the player object.

Notice the camera boundaries is also set to our map’s width and height.

The order we add the layers (foreground, entities, background) is important. Always start by what should be shown in the far back first, and make your way up with what should be shown topmost. Making AxGroups can simplify the job of placing objects between the background and foreground – all game entities can be inserted in it with the peace of mind they won’t appear on top of the foreground, or below the background!

Note: You can create more sub-groups to classify your game entities in more specific object types (enemies, pickup items, bullets, etc.) and make those groups a child of the existing “entities ” AxGroup above.

Controlling the Player object

Our Player character is controllable with the arrow-keys, and reacts differently to the environment while in water tiles. Let’s see the code inside!

//Player.as file
package game.entities {
	import org.axgl.Ax;
	import org.axgl.AxEntity;
	import org.axgl.AxSprite;
	import org.axgl.input.AxKey;
	
	/**
	 * ...
	 * @author Pierre Chamberlain
	 */
	public class Player extends AxSprite {
		private var _isInWater:Boolean;
		
		public function Player(x:Number, y:Number, graphic:Class=null, frameWidth:uint=0, frameHeight:uint=0) {
			super(x, y, graphic, frameWidth, frameHeight);
			
			//Creates a White block graphic (placeholder for now)
			create(8, 8, 0xffffffff);
			
			//Make the default camera follow this object:
			Ax.camera.follow(this);
			
			acceleration.y =	1000;
			maxVelocity.x = 	100;
			maxVelocity.y =		500;
			
			//Prevent the Player to go outside the camera's bounds
			worldBounds =		Ax.camera.bounds;
		}
		
		public override function update():void {
			super.update();
			
			if (Ax.keys.down(AxKey.LEFT)) {
				acceleration.x = -500;
			} else if (Ax.keys.down(AxKey.RIGHT)) {
				acceleration.x = 500;
			} else {
				acceleration.x = 0;
				velocity.x *= .5;
			}
			
			if (Ax.keys.pressed(AxKey.UP)) {
				if (isOnGround || isInWater) {
					y-=5;
					velocity.y = -600;
				}
			}
		}
		
		public function get isOnGround():Boolean {
			return wasTouching(AxEntity.DOWN);
		}
		
		public function get isInWater():Boolean { return _isInWater; }
		public function set isInWater(value:Boolean):void {
			_isInWater = value;
			
			maxVelocity.y =		_isInWater ? 50 : 500;
			acceleration.y =	_isInWater ? 50 : 1000;
		}
		
	}

}

Setting up the Main class

Finally, our main class makes use of the StateGame object to start up our game.

//Main.as file
package 
{
	import game.states.StateGame;
	import org.axgl.Ax;
	
	/**
	 * ...
	 * @author Pierre Chamberlain
	 */
	[SWF(frameRate=&quot;60&quot;,width=&quot;640&quot;,height=&quot;480&quot;)]
	public class Main extends Ax 
	{
		
		public function Main():void 
		{
			super(StateGame, 640, 480, 2, 60);
		}
	}
}

Conclusion

Hopefully this tutorial was not too confusing to follow! If you had any problems, or have any suggestions / feedback at all… let me know in the comments form below!

Stay cool and have fun with Axel! ;)