An update to this post has been published.

The problem

A few weeks ago I showed you the (very) beginnings of a tile based flash game using a physics engine called Box2d. The advantage of a tile based game is how easy it can be, once the game engine is complete, to edit and create levels. I would even like people who are not familiar with AS3 and Flash to be able to make levels. With this in mind, it is obviously no good having the level data all tied up in the Flash files.

Level overlaid with XML data

Level overlaid with XML data

The solution?

Store the level data in an external XML file!

For those of you not too interested in the AS3 code and just want to play around with making your own levels, download this zip file and open the XML file in any text editor program to make your own level layout.

A look at the code

The first important thing to note is that we now have another AS3 file called Levels.as. We are going to create an instance of this object which we can then use to access any data it collects from the XML file. All we need to know for now is how to use this Level object. Lets take a look at the start of the Levels.as file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package
{
	import flash.events.*;
	import flash.net.URLRequest;
	import flash.net.URLLoader;
 
	public class Levels extends EventDispatcher 
	// Extends Event Dispatcher so it can fire an event after loading
	{
 
		/**** vars ****/
		private var url:String;
		public var levels:Array;
 
		public function Levels(u:String) {
			url = u;
		}
 
		public function loadLevels():void
		{
			var urlReq:URLRequest = new URLRequest(url);
			var loader:URLLoader = new URLLoader();
			loader.addEventListener(Event.COMPLETE, getLevels);
			loader.addEventListener(IOErrorEvent.IO_ERROR, IOError);
			loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, SError);
			loader.load(urlReq);
		}

This class extends EventDispatcher, this allows the object to fire an Event when it is finished loading the XML file into an array. We can also see that we need to send through a URL variable to direct the object to the right XML file. Next is the getLevels() function

28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private function getLevels(e:Event):void
{
	if ( e.target.data )
	{
		levels = new Array();
		var levelsXML:XML = new XML(e.target.data);
		var levelsList:XMLList = levelsXML.children();
 
		for each (var levelInfo:XML in levelsList) 
		{
			if (levelInfo.name() == "level") 
			{
				var levelContent:XMLList = levelInfo.children();
				var obj:Object = new Object();
				for each (var levelPart:XML in levelContent)
				{
					if(levelPart.name() == "title")
					{
                                               //This is for later when we want to name levels
						obj.title = levelPart.text();
					}
					if(levelPart.name() == "rows")
					{
                                               //This is for retrieving the number of rows
						obj.rows = levelPart.text();
					}
					if(levelPart.name() == "data")
					{
						//Regular expression to decode the level data
						var myPattern:RegExp = /[A-Z]|[a-z]|[0-9]/g;  
						var str:String = levelPart.text();
						var sArray:Array = str.match(myPattern);
						obj.data = new Array();
						obj.data = sArray;
					}
				}
				levels.push(obj);
			}
		}
		//Event to tell the game that the levels have loaded
		this.dispatchEvent(new Event("xmlloaded"));
	}
}

This function retrieves the XML and parses it into an array of objects. Each object in the array has a title, a number of rows and then an array containing the level data. Once the function finishes parsing the XML, it dispatches an event so that the main class knows it can continue.

Meanwhile, back in the main class

First we need to create an instance of the Levels object.

46
47
// The level object which will hold all our level data
private var L:Levels = new Levels("levels.xml");

Next we need to listen for the “xmlloaded” event that gets thrown by the Levels object so we can trigger the createLvl() function. This gets added to the Levels object we just created. The listener is placed in with the other event listeners for the game. With the listener ready, we can call the function that loads and parses the XML.

49
50
51
52
53
54
public function TileGame()
{
	stage.addEventListener(KeyboardEvent.KEY_DOWN, key_pressed);
	stage.addEventListener(KeyboardEvent.KEY_UP, key_released);
	L.addEventListener("xmlloaded", createLvl);
        L.loadLevels();

Now that we have a Levels object and it contains all of the levels data, we can make use of that data to build the levels. When we get the level data from the object, note that there is a variable called lvlCurrent. This will be used later on when we want to be able to progress from level to level.

71
72
73
74
75
76
77
private function createLvl(e:Event):void
{
	//Retrieve the level data and put it into a more manageable array.
	//Notice the "lvlCurrent" variable which will be used later for changing levels.
	var lvlArray:Array = L.levels[lvlCurrent-1].data; 
	var lvlRows:int = L.levels[lvlCurrent-1].rows;
	var lvlColumns:int = Math.ceil((lvlArray.length)/lvlRows);

With the level data successfully retrieved and stored in an easy to use array, we can now build the level based on that data. Using a switch statement, each value in the array will trigger a different object to be built with the physics engine.

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
for(var i:int = 0;i<lvlArray.length;i++)
{
        /*If i is a multiple of lvlColumns, then the 
        row number needs to be increased.*/
	if((i/lvlColumns) is int)
	{
		row++;
	}
	column = lvlColumns + (i - row*lvlColumns)+1;
 
	switch (lvlArray[i]){
		case "1":						
			//Create Solid Block
			var wallFd:b2FixtureDef = new b2FixtureDef();
			var wallSd:b2PolygonShape= new b2PolygonShape();						
			var wallBd:b2BodyDef = new b2BodyDef();
			wallBd.position.Set(column/ m_physScale*30, row/ m_physScale*30);
			wallBd.type = b2Body.b2_staticBody;
			wallFd.friction = .9;
			wallFd.restitution = .05;
			wallFd.shape = wallSd;
			wallSd.SetAsBox(15 / m_physScale, 15 / m_physScale);						
			levelB = m_world.CreateBody(wallBd);
			levelB.CreateFixture(wallFd);						
		break;
		case "2":
			//Create Ramp A
			var rampAFd:b2FixtureDef = new b2FixtureDef();
			var rampASd:b2PolygonShape = new b2PolygonShape();
			var rampABd:b2BodyDef = new b2BodyDef();
			rampAFd.friction = 1.0;
			rampAFd.restitution = .05;
			rampAFd.shape = rampASd;
			var vxsA:Array = [new b2Vec2(15 / m_physScale, 15 / m_physScale), 
							 new b2Vec2(-(15 / m_physScale), 15 / m_physScale), 
							 new b2Vec2(15 / m_physScale, -(15 / m_physScale))];
			rampASd.SetAsArray(vxsA, vxsA.length);
			rampABd.type = b2Body.b2_staticBody;
			rampABd.userData = "ramp";
			rampABd.position.Set(column/ m_physScale*30, row/ m_physScale*30);
			levelB = m_world.CreateBody(rampABd);
			levelB.CreateFixture(rampAFd);				
		break;
		case "3":
			//Create Ramp B
			var rampBFd:b2FixtureDef = new b2FixtureDef();
			var rampBSd:b2PolygonShape = new b2PolygonShape();
			var rampBBd:b2BodyDef = new b2BodyDef();
			rampBFd.friction = 1.0;
			rampBFd.restitution = .05;
			rampBFd.shape = rampBSd;
			var vxsB:Array = [new b2Vec2(15 / m_physScale, 15 / m_physScale), 
							 new b2Vec2(-(15 / m_physScale), 15 / m_physScale), 
							 new b2Vec2(-(15 / m_physScale), -(15 / m_physScale))];
			rampBSd.SetAsArray(vxsB, vxsB.length);
			rampBBd.type = b2Body.b2_staticBody;
			rampBBd.userData = "ramp";
			rampBBd.position.Set(column/ m_physScale*30, row/ m_physScale*30);
			levelB = m_world.CreateBody(rampBBd);
			levelB.CreateFixture(rampBFd);				
		break;
		case "4":
			//Create Loose block
			var bodyDef:b2BodyDef = new b2BodyDef();
			bodyDef.type = b2Body.b2_dynamicBody;
			var boxShape:b2PolygonShape = new b2PolygonShape();
			fixtureDef.shape = boxShape
			fixtureDef.density = .2;
			fixtureDef.friction = 0.9;
			fixtureDef.restitution = 0.1;
			boxShape.SetAsBox(15 / m_physScale, 15 / m_physScale);
			bodyDef.position.Set(column/ m_physScale*30, row/ m_physScale*30);
			bodyDef.angle = 0;
			levelB = m_world.CreateBody(bodyDef);
			levelB.CreateFixture(fixtureDef);
		break;
		case "5":
			//Create Player
			var bodyDefC:b2BodyDef = new b2BodyDef();
			bodyDefC.type = b2Body.b2_dynamicBody;
			var playerShape:b2PolygonShape = new b2PolygonShape();
			playerShape.SetAsBox(10 / m_physScale, 10 / m_physScale);
			fixtureDef.shape = playerShape;
			fixtureDef.density = 5;
			fixtureDef.friction = .3;
			fixtureDef.restitution = 0.3;
			bodyDefC.position.Set(column/ m_physScale*30, row/ m_physScale*30);
			bodyDefC.angle = 0;
			player = m_world.CreateBody(bodyDefC);
			player.CreateFixture(fixtureDef);
		break;
	}
}
//Once the level is built, begin running the physics engine.
addEventListener(Event.ENTER_FRAME, run);

As you can see from the code, 1 is a solid block, 2 and 3 are ramp blocks created with the box2d polygon function, 4 is a dynamic block and 5 is our player block. All objects in box2d have a friction value and a restitution value, which is fancy talk for bounciness. Only dynamic or movable objects have density, which is weight. Once the loop is finished and all the physics objects are built, we can add an ENTER_FRAME event listener which runs a step of the physics world for every frame that passes.

If you would like to mess around with the engine, download the full source here.

The result

Get Adobe Flash player