Contact Me or check out the Producerism Blog

FlashPunk, DAME and Lua Tutorial (Part 4)

04/16/2011


In the fourth part of this tutorial, I'll finally get into the Lua code.  Since this was my first experience with Lua so far, I'll also include some thoughts and other tutorials I discovered in the process.  Here are links for the previous parts in the tutorial, 0, 1, 2 and 3.  As always, if you haven't already downloaded the project files, grab them here.

Since I'm still brand new to Lua, I won't go into the language itself, since the syntax is very similar to ActionScript.  What is important to read over though, are the DAME functions to make use of within the Lua scripts.  These custom functions are what makes everything happen, and I spent a few hours reading over the complete list of them in creating this exporter.

First of all, you will need to copy both of the Lua files in this project (FPDameTemplatesTutorial.lua and FPDameTemplatesTutorial_settings.lua) into the lua/Exporters folder within the DAME installation directory (mine was C:\Program Files\DAME\lua\Exporters).

The settings file (FPDameTemplatesTutorial_settings.lua) is only used for displaying a description and additional options within the DAME export window. For a reference, this is what the export window will look like when using the FPDameTemplatesTutorial_settings.lua file:

Here is the code for FPDameTemplatesTutorial_settings.lua:

-- Display the settings for the exporter.
DAME.AddHtmlTextLabel("An XML exporter made by T (producerism.com) for the FPDameTemplatesTutorial... tutorial.")
DAME.AddTextInput("Relative Export Path", "", "RelativeExportPath", true, "The path (relative to the .dam project file) to export the xml." )
DAME.AddTextInput("Level Name", "", "LevelName", true, "The name you wish to call this level." )
return 1

You can see that only 3 of those lines are doing anything interesting, specifically the last two.  These are DAME functions which add additional controls to the export window.  The two fields I've added are both TextInputs.  From the second parameter you can see the names of these fields are "RelativeExportPath" and "LevelName."  Those will be made available to the main script which actually parses the tile/sprite/map information and exports a map (in our case, XML).

Here is the code for FPDameTemplatesTutorial.lua.  I decided not to show it all at once, since it will be easier to explain step by step.

Lines 1-23 define a function for initializing the exporter:

function init()
	-- Sets the number of decimal places to be displayed after any floating-point numbers in your output text
	DAME.SetFloatPrecision(0)

	-- Setup tab strings for convenience
	tab1 = "\t"
	tab2 = "\t\t"
	tab3 = "\t\t\t"
	tab4 = "\t\t\t\t"

	-- This is the string where we will store all of the text to be exported as XML
	fileText = '<level>\n'

	-- create empty arrays for map, sprite, and shape layers
	mapLayers = {}
	spriteLayers = {}
	shapeLayers = {}

	-- generate a default properties string
	propertiesString = "%%proploop%% %propname%=\"%propvalue%\"%%proploopend%%"
end

First, we set the number of decimal places to be exported (in our case, 0) using DAME.SetFloatPrecision(0). The tab1-4 variables defined just below that make the exported XML file easier to read within a text editor. The fileText variable is a string which will store all of the text to be exported as an XML file. Finally, the propertiesString is a specially formatted string which DAME will use to find all of the custom properties of any object within the DAME editor. A full explanation of these properties is explained here.

Now lets look at the parseLayers() function on lines 23-64, which accounts for most of the code in this file.

-- parse all of the layers in the DAME project, and sort them into the arrays for mapLayers, spriteLayers and shapeLayers
function parseLayers()
	local groups = DAME.GetGroups()
	local groupCount = as3.tolua(groups.length)-1
	-- parse through the groups
	for groupIndex = 0,groupCount do
		local group = groups[groupIndex]
		local groupName = string.gsub(as3.tolua(group.name), " ", "_")
		local layerCount = as3.tolua(group.children.length) - 1

		-- Go through each layer and store some tables for the different layer types.
		for layerIndex = 0,layerCount do
			local layer = group.children[layerIndex]
			local isMapLayer = as3.tolua(layer.map)~=nil
			local isSpriteLayer = as3.tolua(layer.IsSpriteLayer())
			local isShapeLayer = as3.tolua(layer.IsShapeLayer())
			local layerName = string.gsub(as3.tolua(layer.name), " ", "_")

			if isMapLayer then
				table.insert(
					mapLayers,
					{
						layer=layer,
						layerName=layerName,
						xOffset=as3.tolua(layer.map.x),
						yOffset=as3.tolua(layer.map.y),
						mapWidth=as3.tolua(layer.map.width),
						mapHeight=as3.tolua(layer.map.height),
						tileWidth=as3.tolua(layer.map.tileWidth),
						tileHeight=as3.tolua(layer.map.tileHeight),
						mapWidthInTiles=as3.tolua(layer.map.widthInTiles),
						mapHeightInTiles=as3.tolua(layer.map.heightInTiles)
					}
				)
			elseif isSpriteLayer then
				table.insert(spriteLayers,{groupName,layer,layerName})
			elseif isShapeLayer then
				table.insert(shapeLayers, {groupName,layer,layerName})
			end
		end
	end
end

In this function, we first create a variable to store all of the group layers within our DAME project, using the DAME.GetGroups() function. Remember, we created 3 groups in our project, maps, sprites and shapes. Next, we create a variable, groupCount, to store the total number of groups (which will be 3 in our case). On line 28, we begin looping through all of our groups. To make things easier to read, I've created a variable called group to store the current group layer to analyze, groupName to store the name of the group (e.g. "maps") and layerCount for the total number of layers within a group (e.g. there are 6 layers in the "maps" group).

While we are looping through all of the groups, on line 34 we loop through the layers within each group. Lines 36-38 are simply checking to see what type of layer we're currently parsing, and based on the type, we either go to line 41, 57, or 59.

If the current layer being parsed is a map layer, it's put into the mapLayers array (using table.insert), along with an object which contains all of the properties of that map (lines 44-55). If the current layer is a sprite or a shape instead, it's dealt with accordingly.

Now we can cover the only other large function to worry about, createTileMaps(). Although it spans over 30 lines of code (lines 66-103), it's all very simple.

function createTilemaps()

	-- First, format the string we want to create, using $variableName for interpolation
	local mapTextFormat = tab1 .. '<tiles layer="$layerName" x="$xOffset" y="$yOffset" width="$mapWidth" height="$mapHeight" tileWidth="$tileWidth" tileHeight="$tileHeight" widthInTiles="$mapWidthInTiles" heightInTiles="$mapHeightInTiles">\n'

	local mapText = ''

	for i in ipairs(mapLayers) do
		local map = mapLayers[i]

		-- Second, interpolate $variableName with actual values
		mapText =
			string_interpolate(
				mapTextFormat,
				{
					layerName=map.layerName,
					xOffset=map.xOffset,
					yOffset=map.yOffset,
					mapWidth=map.mapWidth,
					mapHeight=map.mapHeight,
					tileWidth=map.tileWidth,
					tileHeight=map.tileHeight,
					mapWidthInTiles=map.mapWidthInTiles,
					mapHeightInTiles=map.mapHeightInTiles
				}
			)
			..
			-- Parse through all of the subchildren
			as3.tolua(DAME.ConvertMapToText(map.layer,"","",tab2.."<tile","",""," tx=\"%tilex%\" ty=\"%tiley%\" x=\"%pxpos%\" y=\"%pypos%\"/>\n", true))
			..
			tab1
			-- Close the <tiles> tag
			.."</tiles>\n"

			fileText = fileText .. mapText
	end

end

Unfortunately, string interpolation within Lua isn't as nice as some other languages (like PHP or Perl). You can't just throw variables right into a string (e.g. "Hello $firstname $lastname!" outputs "Hello John Doe!") without writing a helper function. Or rather, without finding a decent string interpolation function online, like this one (visit the link for an example). After adding this function, we can put variables right into our string, which makes reading the much easier, as you can see on line 69.

After we've defined our string format (mapTextFormat), we just pass that string along with an object that contains all of the parameters to interpolate (lines 80-90). The dots you see on lines 92, 95, and 98 just concatenate all of this text together (into the mapText variable). It should all look fairly straightforward, with the exception of line 94, which calls DAME.ConvertMapToText().

			-- Parse through all of the subchildren
			as3.tolua(DAME.ConvertMapToText(map.layer,"","",tab2.."<tile","",""," tx=\"%tilex%\" ty=\"%tiley%\" x=\"%pxpos%\" y=\"%pypos%\"/>\n", true))

When looking at the DAME Functions list, you can see that the DAME.ConvertMapToText() function accepts parameters for mapLayer, rowPrefix, rowSuffix, columnPrefix, columnSeperator, columnSuffix, keywords, and whether or not to ignoreHiddenTiles. For this tutorial, we've omitted rowPrefix and rowSuffix, and set "<tile" to be the columnPrefix. The keywords are defined in the DAME Tilemap Keywords list.

After parsing all of the child ("<tile>") nodes, we simple close the parent ("<tiles>") tag, then continue on with the loop. Once all of the tilemaps have been looped through, we append the generated text (mapText) to our fileText variable.

It's all smooth sailing from here, seriously! Lets take a look at the createSprites() function on lines 105-112:

function createSprites()
	for i in ipairs(spriteLayers) do
		fileText = fileText..tab1.."<sprites layer=\""..as3.tolua(spriteLayers[i][2].name).."\">\n"
		local creationText = tab2.."<sprite className=\"%class%\" x=\"%xpos%\" y=\"%ypos%\" width=\"%width%\" height=\"%height%\""..propertiesString.." />\n"
		fileText = fileText..as3.tolua(DAME.CreateTextForSprites(spriteLayers[i][2],creationText,"sprite type=\"%spritename%\""))
		fileText = fileText..tab1.."</sprites>\n"
	end
end

For the createSprites() function, we just loop through all of the items in the spriteLayers array. Similar to the DAME.ConvertMapToText() function, we have a DAME.CreateTextForSprites() function which parses all of the sprites in a layer. Also, just as with DAME.ConvertMapToText(), this function allows us to pass in some DAME Keywords. The inclusion of propertiesString (line 108) makes sure that if any custom parameters had been set in DAME, we capture those too.

Almost identical to the createSprites() function, is the next function, createShapes() on lines 114-122. The only big difference between the functions for sprites and shapes, is that DAME.CreateTextForShapes() allows us to pass in a different string format for circles vs rectangles:

function createShapes()
	for i in ipairs(shapeLayers) do
		fileText = fileText..tab1.."<shapes layer=\""..as3.tolua(shapeLayers[i][2].name).."\">\n"
		local circleText = tab2.."<shape type=\"circle\" x=\"%xpos%\" y=\"%ypos%\" radius=\"%radius%\""..propertiesString.." guid=\"%guid%\" />\n"
		local rectangleText = tab2.."<shape type=\"rectangle\" x=\"%xpos%\" y=\"%ypos%\" width=\"%width%\" height=\"%height%\""..propertiesString.." guid=\"%guid%\" />\n"
		fileText = fileText..as3.tolua(DAME.CreateTextForShapes(shapeLayers[i][2],circleText,rectangleText,"shape"))
		fileText = fileText..tab1.."</shapes>\n"
	end
end

Almost done... let's just look at the finish() function on lines 124-131.

function finish()
	-- Get settings from the *_settings.lua file which accopanies this one.
	local levelName = as3.tolua(VALUE_LevelName)
	local relativeExportPath = as3.tolua(DAME.GetProjectFileLocation())..'\\'..as3.tolua(VALUE_RelativeExportPath)..'\\'

	fileText = fileText.."</level>"
	DAME.WriteFile(relativeExportPath..levelName..".xml", fileText )
end

The first two lines (126-127) to notice, are where we capture the data from the custom fields defined in the settings.lua file.  As you can see, we simply use the function as3.tolua() in order to get the data, while passing in "VALUE_" and then the defined name.  If you refer to the settings code above, you can see that since the TextInput name was "LevelName," we are retrieving that data by using as3.tolua(VALUE_LevelName).

At this point, we are pretty much done.  All of the tilemaps, sprites, and shapes have been exported to the XML string (still stored in fileText).  The very last step is to add our closing </level> tag, and then write the file using DAME.WriteFile().

Now that we've covered all of the functions, we simply need to run them all in order. There are better ways to do this, but to make the file easy to follow along with, I've just called them out one after the other (lines 137-142).

init()
parseLayers()
createTilemaps()
createSprites()
createShapes()
finish()

Notes

As mentioned at the beginning of this post, most of the magic being used is actually via a set of functions provided by DAME, which can be referenced here.  I want to emphasize that this tutorial is only using a very small number of features available with DAME.  For example, I'm not using any paths, linking, sprite rotation or a few others.  The author of DAME is very responsive, and was a huge help in the process of writing this tutorial.  I found a few bugs/missing features which were fixed/implemented very quickly (within days of notifying the author via email).  In addition to the help I got via email, this crash course on Lua for ActionScript developers article by Jesse Warden was extremely helpful.

Since originally writing this exporter script (a few months ago), Lua has actually popped up quite a few times in the context of games.  Specifically, the Corona SDK for making mobile games uses Lua, and is another great example of another reason to learn it!  If I decide to cover more Lua in future tutorials, it will probably be via Coronoa.

Anyways, in the next part of this tutorial, we will finally get into the ActionScript which ties all of this together.

Continue to Part 5


Leave a Reply