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.
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.



