Contact Me or check out the Producerism Blog

FlashPunk, DAME and Lua Tutorial (Part 6)

05/04/2011


In the last part, we covered most of the code in the tutorial, with exceptions to the TilemapGeneratorEntity and FlyingEntity classes.  Let's jump right in.

TilemapGeneratorEntity

To try and explain it simply, the TilemapGeneratorEntity basically parses an XML file for tilemap information, along with any sprites (like enemies or powerups) we've created within DAME.  It then creates a graphic to represent the tilemap, a graphic to represent the hit area to collide with, and optionally creates sprites (like enemies) and adds them to the game.  Take a look at how a new TilemapGeneratorEntity is instantiated on line 73 of TestWorld.as:

var testMap:TilemapGeneratorEntity = new TilemapGeneratorEntity(Assets.LEVEL_TEMPLATES_XML, Assets.BG_TILES, mapLayer, "level", _totalMapsWidth, 0, true, false);

As you can see, we are passing in a few different parameters to our TilemapGeneratorEntity:

Ok, let's take a look at the actual TilemapGeneratorEntity class.  Some of this will be redundant from the list above, but since this is a custom class, I want to explain it in detail:

public function TilemapGeneratorEntity($xmlAsset:*= null, $tilesetAsset:*= null, $layerName:String = null, $collisionType:String = null, $x:Number = 0.0, $y:Number = 0.0, $parseSprites:Boolean = true, $recycleMaps:Boolean = true )
		{
			_xmlAsset = $xmlAsset;
			_tilesetAsset = $tilesetAsset;
			_layerName = $layerName;
			_collisionType = $collisionType;
			_x = $x;
			_y = $y;
			_parseSprites = $parseSprites;
			_recycleMaps = $recycleMaps;
		}

So the constructor function isn't doing anything other than setting a bunch of variables that we pass into the constructor.  Technically all of the class variables (anything that starts with an _underscore) should be private, but for simplicity I left them all as public.  Notice that this class is actually extending the FlashPunk Entity class.  Therefore, there are a few methods that will be called automatically for us:

added() method:

		override public function added():void
		{
			/* DEVELOPER NOTE:
			 * all of this information is done within the added() function,
			 * because using FP.world.create() requires passing the class
			 * with no paramters.  So we call a new instance with default
			 * constructor paramters, then set each variable directly
			 * from wherever FP.world.add() was called.
			*/

			// let's focus on the map layer, based on the layer name that was passed in
			var _tileLayer:XMLList = _xmlAsset.tiles.(@layer == _layerName);

As noted in the comments starting on line 56, the reason most of the code in this class is within the added() method instead of the constructor, is because FlashPunk does not allow the passing of any parameters when using FP.world.create().  So with that explained, move down to line 65 where the code actually starts.

var _tileLayer:XMLList = _xmlAsset.tiles.(@layer == _layerName);

If you remember, _xmlAsset contains a reference to Assets.LEVEL_TEMPLATES_XML, from our Assets.as file.  Let's look at that real quick:

Assets.as

		// XML Assets
		[Embed(source = '../assets/levels/level_templates.xml', mimeType="application/octet-stream")]
		public static const LEVEL_TEMPLATES:Class;

		// lets convert the asset into an XML now, so we only have to do it once
		public static const LEVEL_TEMPLATES_XML:XML = new XML(new LEVEL_TEMPLATES);

		// and while we're at it, let's get the total count of templates too
		public static const LEVEL_TEMPLATES_COUNT:Number = LEVEL_TEMPLATES_XML.tiles.length();

First, on line 39 you can see we're embedding the XML file we got from DAME using the meta-tag (more details on that here).  This means that once we build our SWF file, everything will be embedded within.  The final SWF file will be much bigger, however once it's loaded, there won't be anything else to worry about (waiting for XML, audio, images, etc. to load).

Then, on line 43 we create a static constant with the actual data from the file.  If we d0n't do this, we won't be able to load the XML since it's still being recognized as a Class object (we need it to be an XML object).  There are lots of other ways to load XML, this was just my approach.

Finally, since we've already taken the time to put all of the XML data into a constant, why not create a constant for the total number of level templates (the number of <tiles> tags in the XML file) as well?  That's what we do on line 46, creating a constant for LEVEL_TEMPLATES_COUNT.

With that explained, let's go back to the tilemap generator

TilemapGeneratorEntity.as:

			// we set width/height/etc here for convenience, but we can't rely on having
			// the information instantly, which is why mapLayerWidth() getter function
			// was created (bottom).  It would be replaced with a width() getter function.
			_width = _tileLayer.@width;
			_height = _tileLayer.@height;
			_tileWidth = _tileLayer.@tileWidth;
			_tileHeight = _tileLayer.@tileHeight;

On lines 70-71, we store the width and height of the entire Tilemap that's being generated which is being pulled from the width attribute in the <tiles> tag in our embedded XML file.  Accessing this _width variable directly isn't reliable, so a mapLayerWidth() getter function was added as mentioned in the comments.  More on that in a second.  Next, on lines 72-73, we simple store the height and width of our tiles in this tilemap (in this example, it's 32 x 32 pixels).

Now let's give all this information to FlashPunk, and setup the Tilemap and Grid classes so that they will do most of the hard work:

// Build the empty tilemap and grid
			var _tiles:Tilemap = new Tilemap(_tilesetAsset, _width, _height, _tileWidth, _tileHeight);
			var _grid:Grid = new Grid(_width, _height, _tileWidth, _tileHeight);
			mask = _grid;

When creating the new Tilemap, just pass in the image asset (Assets.BG_TILES), along with the width and height of the tilemap, along with the width and height of the tiles. Next, we create a new instance of the Grid class, which is basically a helper for doing efficient collision detection against a Tilemap.  The last line (78) in this block of code is to set the mask (for collision detection) of our TilemapGeneratorEntity to the Grid that was just created.

About half way through this class, let's keep up the pace.  Our Tilemap and Grid is setup (width, height), but there's nothing in them yet.  Let's move on to actually creating the tilemap from the XML data:

// parse the map layer xml
			for each(var tile:XML in _tileLayer.tile)
			{

				// add the tile to the tilemap
				_tiles.setTile(
					(tile.@x / _tileWidth),
					tile.@y / _tileHeight,
					_tiles.getIndex(
						tile.@tx / _tileWidth, tile.@ty / _tileHeight
					)
				);
				// add the tile to the grid for collision detection
				_grid.setTile(
					tile.@x / _tileWidth,
					tile.@y / _tileHeight
				);
			}

Lines 81-97 run a loop which parses through each tile in our map (e.g. each of the <tile> tags within the current <tiles> tag).  On line 85, we set the tile within the tilemap.  Here is an illustration to explain what the setTile() method is doing.

This is an example of a <tile> node:

<tile tx="96" ty="0" x="256" y="0"/>

Explanation of each attribute:

So if our tx is 96, and ty is 0, then lines 85-91 for this tile would be:

_tiles.setTile(
					(256 / 32),
					0 / 32,
					_tiles.getIndex(
						96 / 32, 0 / 32
					)
				);

And reducing this further:

_tiles.setTile(8, 0, _tiles.getIndex(3, 0));

We're telling FlashPunk that the coordinates (8,0) within our Tilemap (which is at 256, 0 in pixels) should be represented by the graphic at (3,0), which is the fourth tile in the array (since 0 counts as the first one).  See the image below to see what's happening here.  The grid represents a small section in the top left corner of our map.  The tileset below the grid shows the first seven items in our array of tiles to select from.

Here is another illustration of how all of the XML attributes correlate to the settings in DAME, and the Tilemap we are creating.

Now that the graphics for the tilemap have been placed, we need to setup collision detection using our Grid (which was set as our Entity's mask).  You can think of the mask as a simple black and white (or transparent) image that represents our collidable "hit" area.

Since our tilemap image and mask have been created, we could technically return all of this and start our game.  However, DAME also allows the placement of sprites in maps - which don't need to be assigned to specific tiles, but rather any pixel values we want.  This is a really cool feature that the next block of code makes great use of.  Let's take it step by step, since there are a few levels of logic to consider.

Step 1 - check to see if we should even bother to parse the sprites in this instance.

			// only add sprites if flagged to do so
			if (_parseSprites)
			{

Step 2 - first we use E4X to search through the XML data for the <sprites> tag which has the layer attribute of _layerName (e.g. "template1").  Then we use a for each statement to parse through them.

				// now let's focus on any sprite layer with the same name as the map layer
				var _spriteLayer:XMLList = _xmlAsset.sprites.(@layer == _layerName);

				for each(var sprite:XML in _spriteLayer.sprite)
				{

Step 3 - check to see how many sprites we should spawn, based on the attribute spawnAmount in our XML file.  If it's undefined, the default is set to 1.

var spawnAmount:uint = (sprite.@spawnAmount == undefined) /? 1 : uint(sprite.@spawnAmount);

Step 4 - now we set a loop for creating sprites, based on our spawnAmount.

					// create the sprites
					for (var spawnCounter:uint = 0; spawnCounter < spawnAmount; spawnCounter++)
					{

Step 5 - check the XML attribute spriteClass, so see what type of sprite we should create.  If the class doesn't exist, then we just trace out an error and continue.  In testing, it would make sense to trigger an actual error here.

						// since we're referencing classes dynamically, we can't be sure they actually exist
						try
						{
							var spriteClass:Class = getDefinitionByName(String(sprite.@className)) as Class;
						} catch (err:Error)
						{
							trace(String(sprite.@className) + " class not found!");
							continue;
						}

Let's stop for just a second here, because this technique is using a method I wasn't very familiar with.

getDefinitionByName

If you are already familiar with the getDefinitionByName method, feel free to skip ahead.  This is a useful function that allows Flash to instantiate a class at run-time, which hasn't been imported in code.  To put it another way, normally if you want to use a TextField in your application, you need to call import flash.text.TextField; at the top of your code.  Using getDefinitionByName you wouldn't have to import anything - instead you could call getDefinitionByName("flash.text.TextField").  In our case, it's more like getDefinitionByName("FlyingHorse").  The reason that this is useful, is that we can just assign all of our classes in DAME, and then create the corresponding actionscript classes afterwards.

One of the gotchas of using this method, is that there needs to be a reference of some kind to all of these classes in your code somewhere.  I just added a list of them to the end of Assets.as:

		// To avoid errors from the compiler when calling getDefinitionByName
		// just list all of the classes that are not otherwise referenced in code:
		Balloon;
		Cloud;
		FlyingHorse;
		FlyingPig;
		UFO;
		Zeppelin;

NOTE: If you do not reference the classes you intend to use via getDefinitionByName somewhere in your code, you will get the following error:

[Fault] exception, information=ReferenceError: Error #1065: Variable Balloon is not defined. In this case, we tried calling getDefinitionByName("Balloon").

Back to TilemapGeneratorEntity.as:

Side-comment over, hopefully that was useful. At this point, we have decided if a sprite needs to be created, and if so, which one. Now let's actually create it, and place it within the game according to our level design in DAME.

						// by default, we're using FP.world.create(), and assuming that entities will be recycled
						// by passing false as the second paramter, we tell FlashPunk not to add the entity yet
						var spriteEntity:Entity = FP.world.create(spriteClass, false);

On line 125, we simply create our spriteClass (e.g. "FlyingHorse," Balloon," etc.) within the world using FP.world.create().  As it says in the comments, we pass false as the second parameter, which tells FlashPunk to create the sprite, but not to add it yet (same as creating a new TextField before adding it to the stage with addChild).  This allows us to place the sprite according to our XML data:

						// set the sprite position, offset by the map layer's x/y positions (this is a DAME workaround)
						if (sprite.@spawnShape == undefined)
						{
							spriteEntity.x = (Number(sprite.@x) - Number(_tileLayer.@x)) + _x;
							spriteEntity.y = (Number(sprite.@y) - Number(_tileLayer.@y)) + _y;
						}

If you remember back in part 2, I mentioned that we would also implement the shape feature of DAME, so that we could randomly spawn new objects within the area of that shape. Line 128 is where we check to see if that spawnShape has been assigned. If not, we just place the sprite according to the x and y attributes in the <sprite> node.  So that's what happens if spawnShape is undefined.  This is what happens if it has been defined:

						else
						{
							// if spawnShape was defined, use the area to generate random placement
							var spawnShape:XMLList = _xmlAsset.shapes.shape.(@guid == sprite.@spawnShape);
							var spawnMinX:Number = Number(spawnShape.@x) + Number(sprite.@width) / 2;
							var spawnMaxX:Number = Number(spawnShape.@x) + Number(spawnShape.@width) - Number(sprite.@width) / 2;
							var spawnMinY:Number = Number(spawnShape.@y) + Number(sprite.@height) / 2;
							var spawnMaxY:Number = Number(spawnShape.@y) + Number(spawnShape.@height) - Number(sprite.@height) / 2;

							spriteEntity.x = int((Math.random() * (spawnMaxX - spawnMinX + 1) + spawnMinX - int(_tileLayer.@x)) + _x);
							spriteEntity.y = int((Math.random() * (spawnMaxY - spawnMinY + 1) + spawnMinY - int(_tileLayer.@y)) + _y);
						}

First, on line 136, we find the shape that was passed in as our spawnShape.  Then, we find the bounds of the shape (left, right, top, bottom) while taking the size of the sprite into account.  Finally, on lines 142-143, we randomly place the sprite somewhere within those bounds.  And the last step in this big loop is to add the sprite to our world (since it was already created, then positioned, now we display it).

// we've already created the entity, now add it
						FP.world.add(spriteEntity);

That's the last thing that happens in each iteration of the loop that was started back on line 100. Once all of the sprites have been parsed, we're ready to create the tilemap, set the collision group, and then add the actual tilemap to the world.

			// set the graphic to the tilemap that was just created from xml
			graphic = _tiles;
			x = _x;
			y = _y;
			type = _collisionType;

			super.added();
		}

So close to the end of this class! I imagine it's been a process reading this far. There is one other function that will be called automatically by FlashPunk (the biggest hint is that this is an override function).

update()

		override public function update():void
		{
			// once the tilemap goes off camera, we discard it
			if (x < FP.camera.x - width)
			{
				if (_recycleMaps)
				{
					// recycling tells FlashPunk to re-use this entity
					// when calling FP.world.create() on the same class
					FP.world.recycle(this);
				} else {
					// removing tells FlashPunk to destroy the entity
					// this leaves it up to flash's garbage collection
					FP.world.remove(this);
				}
			}

			super.update();
		}

This function is called every frame, and simply checks to see if the position of the tilemap is off the screen. If so, we either recycle or remove the map from the world, depending on our _recycleMaps value (default is false, which uses FP.world.remove).  Last but not least, let's cover the aforementioned "getter" method.

get mapLayerWidth()

		// since we can't rely on _width property to have the information right away,
		// this getter function will retrieve the info directly from the xml asset
		public function get mapLayerWidth():int { return int(_xmlAsset.tiles.(@layer == _layerName).@width); }

This is the final code in the TilemapGeneratorEntity class. As mentioned before, we can't rely on the _width value of our TilemapGeneratorEntity to be accurate until all of this information has been parsed, so we create a helper function which checks the XML data for this value directly (again, using E4X).

Well, that concludes part 6 of this tutorial.  The next part we will cover the FlyingEntity class, which all of our enemies extend (FlyingPig, FlyingHorse, etc.).  Stay tuned!


Leave a Reply