Submit to StumbleUpon Share

Hello, and welcome back to my blog!

Its been a long time coming, but finally I've managed to get some time to work on the blog again...

In this first instalment of a series blog articles, I'm going to be explaining step by step how you make this 2d platform game:


Click the game to give it focus... Apologies for the programmer art, and my level design (not my best qualities!)

The language is actionscript 3.0, but the techniques are applicable to all languages.

Inspiration

As a kid I always used to love playing The Newzealand Story and Rainbow Islands by Taito

The NewZealand Story

Rainbow Islands

And of late my love of platformers was rekindled by this awesome work in progress Generic:

The video cannot be shown at the moment. Please try again later.

So I decided to write about the process of making one - you'll see influences from all three of these games in my bad graphics!

The background

Ok, one of the first things you will notice is that the game has a couple of layers of parallax in the background; this was relatively easy to achieve with the aid of a couple of tiles.

Background Tile

Mid-ground tile

Notice how the tiles are different sizes? This is important to stop the seams lining up too much as the camera moves about.

The most difficult thing was actually getting these designs to tile seamlessly when stacked next to each other, but that's because I'm a programmer not an artist! Anyway, I'll cover how I did that later when I talk about creating the tile set for the game.

Tiles follow camera

In order to get a seemingly infinite tiling background, the tiles are laid down by the renderer as they are needed, Wallace and Gromit style, as the camera moves around the level.

Gromit lays down the track just in time

In the picture above Gromit is laying down the track as the train moves him along; if you replace Gromit with the renderer and the train with the camera you should be able to see what I mean. The difference is that at the other end of the train, we would need another Gromit facing in the opposite direction picking up the track as the train passes over it, and then handing it back to the Gromit at the front!

I found that I needed a cache of N=Screen Width/Tile width + 2 in the X and N=Screen Height/Tile height + 2 in Y number of tiles in order to keep the screen full constantly.

Figure 1

Figure 1 shows the screen in blue moving around a grid of tiles; green tiles are added at the front of the motion and red tiles are removed (or rather recycled) at the rear. Here is the code used:

package Code.Graphics
{
	import flash.display.*;
	import Code.Geometry.*;
	import Code.Maths.*;
	import Code.Constants;
	import Code.Platformer;
 
	public class TileRenderer
	{
		private var m_tiles:Vector.<MovieClip>;
		private var m_numX:int;
		private var m_numY:int;
		private var m_tileWidth:int;
		private var m_tileHeight:int;
		private var m_camera:Camera;
		private var m_zDepth:Number;
 
		private var m_tempAabb:AABB;
 
		/// <summary>
		/// Constructor
		/// </summary>	
		public function TileRenderer( tileType:Class, width:int, height:int, camera:Camera, stage:Platformer, zDepth:Number )
		{
			m_tileWidth = width;
			m_tileHeight = height;
			m_camera = camera;
			m_zDepth = zDepth;
 
			m_tempAabb = new AABB( );
 
			m_numX = Constants.kScreenDimensions.m_x/width+2;
			m_numY = Constants.kScreenDimensions.m_y/height+2;
 
			m_tiles = new Vector.<MovieClip>( m_numX*m_numY );
 
			// run though and create all the tiles we need, this fuction takes
			// a closeure which actually does the work
			PositionLogic( function( index:int, xCoord:int, yCoord:int ):void
			{
				m_tiles[index] = new tileType( );
				m_tiles[index].x = xCoord;
				m_tiles[index].y = yCoord;
				m_tiles[index].cacheAsBitmap = true;
 
				// add the tile and send it to the back
				stage.addChild( m_tiles[index] );
				stage.setChildIndex( m_tiles[index], 0 );
			});
		}
 
		/// <summary>
		/// This function runs through and computes the position of each tile - it takes a closeure 
		/// so you can insert your own inner logic to run at each location
		/// </summary>
		private function PositionLogic( action:Function ):void
		{
			m_camera.GetWorldSpaceOnScreenAABB( m_tempAabb );
 
			var screenTopLeft:Vector2 = m_tempAabb.m_TopLeft;
 
			// stop the background from crawling around due to pixel trucation
			screenTopLeft.RoundTo( );
 
			// calculate the top left of the screen, scaled for z depth
			var scaledTopLeft:Vector2 = screenTopLeft.MulScalar( 1/m_zDepth );
			var tileX:int = Math.floor(scaledTopLeft.m_x / m_tileWidth);
			var tileY:int = Math.floor(scaledTopLeft.m_y / m_tileHeight);
 
			// this offset corrects for translation caused by the divide by z
			var offset:Vector2 = scaledTopLeft.Sub( screenTopLeft );
 
			// get the starting tile coords
			var startX:int = tileX*m_tileWidth - offset.m_x;
			var startY:int = tileY*m_tileHeight - offset.m_y;
			var xCoord:int = startX;
			var yCoord:int = startY;
 
			// run though and call the closure for each tile position
			for ( var j:int = 0; j<m_numY; j++ )
			{
				xCoord = startX;
				for ( var i:int = 0; i<m_numX; i++ )
				{
					var index:int = j*m_numX+i;
 
					action(index, xCoord, yCoord);
 
					xCoord += m_tileWidth;
				}
				yCoord += m_tileHeight;
			}
		}
 
		/// <summary>
		/// Update all the tiles to the new coordinates based on the camera's new position
		/// </summary>	
		public function Update( ):void
		{
			PositionLogic( function( index:int, xCoord:int, yCoord:int ):void
			{
				m_tiles[index].x = xCoord;
				m_tiles[index].y = yCoord;
			});
		}
	}
}

A couple of important caveats with this technique:

In order to stop the tiles crawling around the screen very slightly due to coordinate trucation it was important to make sure the reference point for the top left of the screen was correctly rounded to an integer pixel boundary:

// stop the background from crawling around due to pixel trucation
screenTopLeft.RoundTo( );

My function RoundTo() just rounds to the nearest integer.

Also, the parallax was achieved simply by dividing by the z value of the tile, and then correcting the position of the tiles so they started in the correct place on screen:

var scaledTopLeft:Vector2 = screenTopLeft.MulScalar( 1/m_zDepth );
var tileX:int = Math.floor(scaledTopLeft.m_x / m_tileWidth);
var tileY:int = Math.floor(scaledTopLeft.m_y / m_tileHeight);
var offset:Vector2 = scaledTopLeft.Sub( screenTopLeft );

So what's going on in the above code? What we're actually doing is taking the world-space position of the top left of the screen, dividing by the resolution of the tile to get the number of tiles required in each axis and then 'integerising' the result. Going back to the Gromit analogy, this stops Grommit from putting one piece of train track over the top of the last, or in front of it - he is forced to place them exactly one after the other, which means they tile without visual gaps. The last part is correcting for the offset which happens when we divide by the z depth of the tile layer - this forces the tiles to start at the top left of the screen.

Figure 2

Figure 2 shows a grab of the game where you can see how the tiles are being placed outside the area normally visible to the camera.

The Tiles

Central to any old-school platform game are the tiles which make up the static part of the world. All the tiles in the game are a fixed size; 64x64 pixels, but you can obviously choose whatever tile size is most appropriate.

Tiles are multi-purpose in that they not only provide the building blocks from which every level is constructed, but that they also make up the collision geometry which the player interacts with as they play the level. They are also used to place down enemies and other special non-visible markers which affect certain AI behaviours.

Figure 3

Figure 3 shows the complete tile-set from the game.

Each of the tiles above is assigned a unique integer which represents it, in this case starting from 0 and increasing left to right, top to bottom. The set of all of these integers for a particular level is called the Map. Maps must always be rectangular.

They are represented in code like this:

// map for the level
private var m_map:Vector.<uint> = Vector.<uint>
( 
	[
		04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,04,
		04,52,19,51,00,00,00,52,19,51,00,52,19,51,00,19,51,04,
		04,17,15,18,00,00,00,17,15,18,35,17,15,49,51,15,18,04,
		04,17,15,49,51,15,00,50,15,18,34,17,15,15,49,15,18,04,
		04,00,48,15,49,15,50,15,47,29,34,17,15,48,15,15,18,04,
		04,00,00,48,15,15,15,47,29,00,34,17,15,00,48,15,18,04,
		04,00,00,00,16,16,16,29,00,00,00,00,16,00,31,16,29,04,
		04,00,42,42,42,42,42,42,42,42,42,42,42,42,42,42,00,04,
		04,00,13,00,00,00,00,00,00,00,00,00,00,00,00,00,00,04,
		04,02,02,02,02,02,02,02,02,02,02,02,02,02,02,02,02,04
	]
);

There is a big enum which allows me to identify which integer corresponds to which tile:

public class eTileTypes
{
	// foreground
	static public const kEmpty:uint = 0;
	static public const kEarthGrassLeft:uint = 1;
	static public const kEarthGrass:uint = 2;
	static public const kEarthGrassRight:uint = 3;
	static public const kEarthMid:uint = 4;
	static public const kEarthTop:uint = 5;
	static public const kEarthRight:uint = 6;
	static public const kEarthLeft:uint = 7;
	static public const kEarthBottom:uint = 8;
        ...
}

At level construction time, I run through the Map picking out each integer and constructing the relevant tile which represents it. The position of each tile in the world relates exactly to its position in the Map.

Tile coordinate system

The total extent of the world is defined as 64 x Nx, 64 x Ny, where Nx and Ny are the number of tiles in X and Y axis in the Map. I've also added an offset to locate 0,0 in pixel coordinates to the middle of the Map, in tile coordinates.

Tile coordinates are simply the tile indices into the map, and world coordinates are in pixels (in this case, since I'm using flash and that's most convenient). Its quite common to want to be able to convert from tile coordinates into world coordinates and vice versa, I do so like this:

/// <summary>
/// calculate the position of a tile: 0,0 maps to Constants.kWorldHalfExtents
/// </summary>
static public function TileCoordsToWorldX( i:int ):Number
{
	return i*Constants.kTileSize - Constants.kWorldHalfExtents.m_x;
}
 
/// <summary>
/// go from world coordinates to tile coordinates
/// </summary>
static public function WorldCoordsToTileX( worldX:Number ):int
{
	return ( worldX+Constants.kWorldHalfExtents.m_x )/Constants.kTileSize;
}

There are corresponding versions for the other axis.

Tile psychology

How you actually design your tiles can have a massive effect on the number of them required to represent your game levels. Getting the tiles to seamlessly repeat is a quite irksome process if your not familiar with it.

Cheese

Consider this cheese tile for example (which was actually designed to look like stone, but ended up like cheese due to my fantastic design skill). Looks nice enough by itself, but when you try tiling it a few times, obvious empty spaces emerge:

Tiling cheese

This is due to the spaces in the original design which were too small to fit a decent sized hole into. The solution is to design holes which are cut away on one side and continue on the other, like this:

Side bits which tile

When combined with the original image we get this:

The final cheese

Which tiles much more pleasingly:

Final cheese tiling

But there is one important side effect which comes when designing the end pieces for each side of the tile - end pieces are tiles which represent the end of a piece of cheese, or earth and are there to make the design look less square and rigid.

Cheese plus 4 end pieces

As you can see the end pieces help break up the squareness of the tile, but there is a problem. Due to the choice I made to tile the holes in the corners of the cheese, it means I not only need end pieces but also corner pieces to fill in the holes you can see above in each corner!

This is not only a lot more work when designing the tiles, its also more work when it comes to mapping them, because you need to map many more actual tiles to tidy up the design.

Mapping

In order to actually map down the tiles for each level you don't want to have to manually enter all the integers for each tile type, so I used Mappy instead, which is freely available and even has a script to output an AS3 array of data.

A level in Mappy looks like this:

A level in mappy

You can see a selection of the tiles on the right and how they look once you've mapped them on the left.

Layers

If you look carefully at the game, you'll notice that there isn't just one single layer of tiles for each level. There are actually three!

  • The main foreground tiles, which consist of things the player collides with
  • The mid-ground tiles which represent characters and special controls
  • The background tiles, which are purely visual

Background tiles only

I purposefully made all the background tiles much darker than the foreground so they don't look confusing to the player.

Mid-ground tiles only

The arrow markers you can see either side of the ladybirds actually control their motion - I'll talk more about this in a coming article.

Foreground tiles only

The foreground also acts as the collision layer - everything you see in this layer is collidable.

All layers together

Putting all there together, we get the above.

The reason to have three layers is that is gives you much greater flexibility when creating the levels; if you just used one, you wouldn't be able to map the player on a ladder, for example, because there would be only one tile slot in which to map both player and ladder.

It also adds a predefined depth sorting order - background tiles are always behind everything else, mid-ground tiles come after (although, most are not actually visible), and then the foreground tiles are top-most of all.

Having multiple layers doesn't add much complexity to the system and it gives a lot of flexibility.

Camera

The camera system I used here is much the same as the one I've described previously in this article, so I won't repeat myself.

I will just point out one improvement I made since last time: because we are using tiles, its of the utmost importance that there not be any visual cracking between the tiles as the player moves around the level. Such cracking would destroy the illusion of a continuous world. In order to prevent this, its very important that the camera only ever be positioned on whole pixel boundaries.

In order to achieve this I've added the following bit of code to the camera system:

// this is essential to stop cracks appearing between tiles as we scroll around - because cacheToBitmap means
// sprites can only be positioned on whole pixel boundaries, sub-pixel camera movements cause gaps to appear.
m_worldToScreen.tx = Math.floor( m_worldToScreen.tx+0.5 );
m_worldToScreen.ty = Math.floor( m_worldToScreen.ty+0.5 );

m_worldToScreen is the matrix which ultimately gets attached to the main Sprite for the game, causing the world to be translated around as the player moves, giving the illusion that the player is moving around the world.

End of part 1

That's it for this instalment! Next time I'm going to starting talking about the collision detection of the player against the world, and also how the AI works.

As ever, if you want, you can buy the source-code for the entire game (or try a version for free), including all the assets and levels you see above. It will require Adobe Flash CS4+, the Adobe Flex Compiler 4.0+ and either Amethyst, or Flash Develop to get it to build. And you'll want Mappy or some alternative in order to create your own levels!

Following on from feedback from the Angry Birds article, I've included a Flash Develop project as well as an Amethyst project inside the .zip file, to help you get started more quickly, no matter which development environment you have.

You are free to use it for whatever purposes you see fit, even for commercial games or in multiple projects, the only thing I ask is that you don't spread/redistribute the source-code around. Please note that you will need some programming and design skills in order to make the best use of this!

Go to the source-code option page to choose the version you'd like - from completely free to the full version!

Subscribers can access the source here

Until next time, Have fun!

Cheers, Paul.

Continue reading in part 2

Submit to StumbleUpon Share