Updated: The tile engine discussed in this post has since been significantly cleaned-up and improved. This tile engine is packaged up with example applications and is available for download here.
My last two prototypes are based around the final two steps in this milestone. Namely, making tiles that act like walls, and making tiles that will draw above our hero.
Prototype 5 – isWall
The premise here is that if a tile is a wall, I need to create an object to do some hitTest operations. The problem of course is that our tiles aren’t objects. They are just pixel data that gets copied to a buffer and viewport. So this is what I did.
- A “Wall” class was created that extends Rectangle. This Wall class would make itself a rectangle the size of a tile, and allow me to store worldX and worldY coordinates (it’s coordinates relative to the whole world).
- As I parse the XML file, if “isWall = 1″ then I call the Wall class passing it worldX and worldY coordinates.
- I then add the Wall tile to an array within our Engine.
- On the Hero hitTest function, I just add the Wall array to the list of objects to test.
My Wall class:
package com.refrag.tileTest06{ import flash.display.Shape; //Wall is a very simple class that allows us to store a worldX and worldY value within a shape //The Wall objects will be our hitTest objects for isWall tiles public class Wall extends Shape{ var worldX=0; var worldY=0; //Our constructor asks for world X,Y coordinates and a tile size. public function Wall(wx,wy,sx,sy):void{ graphics.beginFill(0xFFCC00,0); //Changing the alpha from 0 to 0.5 allows for great debugging. graphics.lineStyle(0, 0x666666,0); //Changing the alpha from 0 to 0.5 allows for great debugging. graphics.drawRect(0, 0, sx, sy); graphics.endFill(); worldX=wx; worldY=wy; } } }
To properly show what’s happening here I coloured the Wall object with 50% transparency. You can see the Wall hittest objects existing above the actual tiles.
I did one shortcut that I may regret later. I’m testing all Wall objects for collision when in reality I only need the ones that exist in the viewPort. This may lead to performance issues down the road. I have a few ideas on how to resolve this, but wont bother unless it actually becomes an issue. The one benefit of the way I’m doing it is any Enemy that is moving off screen can also use the Walls for path logic. Anyway.. something to keep track of.
Prototype 6 – isSorted
The problem with Prototype 5 is that although the wall is 2 tiles tall, my Hero can’t walk behind it. Really he should be able to walk behind the top tiles of the wall. Only the bottom ones need collision detection, but the top ones need to draw overtop of the hero.
My solution involved creating another small class called “SortedTile” that extends Bitmap. SortedTile would store the bitmap data of the tile needing to be drawn above the player, as well as some worldX, and worldY coordinates.
My SortedTile class:
package com.refrag.tileTest06{ import flash.display.Bitmap; import flash.display.BitmapData; //A SortedTile extends a bitmap object by adding World X,Y coordinates public class SortedTile extends Bitmap{ public var worldX; public var worldY; //Our constructor asks for the bitmap data, as well as World X,Y coodinates. public function SortedTile(data:BitmapData,wx:int,wy:int){ bitmapData=data; worldX=wx; worldY=wy; } } }
So to break down what happens:
- I step through all of the tiles that were created, looking for the “isSorted” flag.
- I then create a SortedTile for each of these, copying the appropriate bitmap data from the tileSet.
- WorldX and WorldY coordinates are setup for the SortedTile
- I add the SortedTile to the sortedItems array in our Engine
- And of course finally, add the SortedTile to the stage
There are some limitations with what I did here. Every SortedTile is drawn every frame, even if it exists outside of the viewPort. This may cause performance issues down the road. Also, this will limit some effects and will certainly break with objects that are taller than 1 tile (e.g. Trees).
A better solution for the future is to add another layer of tiles that will act the same way as the ground tiles, but always display above the Hero.
My end all solution will be to have the world created with 3 layers.
- Absolute Ground (always under the Hero)
- Sorted Tiles (sorted based on y values)
- Absolute Above (always above the Hero)
But… sorted tiles are still a good start and meet the requirements of this Milestone.
World.as:
package com.refrag.tileTest06{ import flash.display.Loader; import flash.display.Stage; import flash.display.MovieClip; import flash.events.*; import flash.net.*; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Shape; import flash.geom.Point; import flash.geom.Rectangle; public class World extends MovieClip { //Create an array to hold the tiles var theWorld:Array = new Array(); //Setup our view offset var viewXOffset = 0; var viewYOffset = 0; //Create variables that will be filled from the XML file var tilesX:int; var tilesY:int; var mapName = ""; public var worldPixSizeX = ""; public var worldPixSizeY = ""; var tiles_perRow = 0; var tileSize:int; //We'll use this variable to check if bitmap stuff is still loading var loading = false; //Setup our bitmap objects //Our Tileset var worldTileSet:Bitmap = new Bitmap(); //Our Buffer var buffer:Bitmap = new Bitmap(); var bufferWindow:Rectangle; //Our Canvas/View Port var canvas:Bitmap = new Bitmap(); var viewPort:Rectangle = new Rectangle(0,0,320,480); var viewPointer:Point = new Point(0,0); var viewCols:int; var viewRows:int; public var viewWidth:int=viewPort.width; public var viewHeight:int=viewPort.height; //And finally a reference of the stage so we can add children var stageRef:Stage; //Constructor public function World(s:Stage) { //Reference our stage stageRef=s; //Add our canvas to the stage, at Index 0 stageRef.addChild(canvas); stageRef.setChildIndex(canvas,0); //Initiate world setup setupWorld(); } //World Setup. private function setupWorld():void { loading=true; loadXML("corneria.xml"); } //XML Loader private function loadXML(url:String):void { var loader:URLLoader = new URLLoader(); configureListeners(loader); var request:URLRequest = new URLRequest(url); try { loader.load(request); } catch (error:Error) { trace("Unable to load requested document."); } } private function completeXMLSetup(e:Event):void { //now we can finish setting up our variables with the xml data var xml:XML = new XML(e.target.data); tileSize = xml.tileSizeX; bufferWindow = new Rectangle(viewPort.x-tileSize,viewPort.y-tileSize, viewPort.width+(tileSize*2), viewPort.height+(tileSize*2)); viewCols = viewPort.width/tileSize; viewRows = viewPort.height/tileSize; mapName = xml.title; tileSize=xml.tileSizeX; worldPixSizeX = xml.tilesX*tileSize; worldPixSizeY = xml.tilesY*tileSize; tilesX=xml.tilesX; tilesY=xml.tilesY; //We store tile info (id,isWall,isSorted) in a small object var tile:Tile = new Tile(); //parse xmlMapData and create aWorld array for (var i=0; i<xml.tilesX; i++) { var tempArray:Array=new Array(); for (var j=0; j<xml.tilesY; j++) { //Create the tiles and add to the array tile = new Tile(); tile.id=xml.tileRow[i].tileCol[j].tileLayer[0].tile.id; tile.isWall=xml.tileRow[i].tileCol[j].tileLayer[0].tile.isWall; tile.isSorted=xml.tileRow[i].tileCol[j].tileLayer[0].tile.isSorted; tempArray.push(tile); //Check to see if this tile is a wall if (tile.isWall==1) { //If so.. set it up! setWall(j*xml.tileSizeX,i*xml.tileSizeY); } } //We store each colum as a an array theWorld.push(tempArray); } //Once we have our array of tiles, we can load our bitmap loadBitmap(xml.tileSetBitmap); } //Bitmap Loader private function loadBitmap(url:String):void { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeBitmapSetup); loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); var request:URLRequest = new URLRequest(url); loader.load(request); } //Complete Bitmap load private function completeBitmapSetup(event:Event):void { //Our tileset bitmap is loaded! Loading is now false worldTileSet = event.target.content; loading=false; //Determine how many tiles we have in each row with some simple math tiles_perRow=worldTileSet.bitmapData.width/32; //Calculate how many tiles we need to fill the buffer var xtiles=bufferWindow.width/32; var ytiles=bufferWindow.height/32; //Setup our pointer and tile rectangle var tilePointer:Point = new Point(bufferWindow.x, bufferWindow.y); var tile:Rectangle = new Rectangle (0,0,tileSize,tileSize); //Setup the bimaps with some filler data buffer.bitmapData = new BitmapData(bufferWindow.width,bufferWindow.height,true,0x00000000); canvas.bitmapData = new BitmapData(viewPort.width,viewPort.height,true,0x00000000); //And finally, setup those tiles that need depth sorting setupSortedTiles(); } private function setupSortedTiles():void { //All we're doing here is stepping through the whole tile array looking for //tiles that need depth sorting (isSorted=1). //We create a new SortedTile object and pass it to the stage var tile:Tile = new Tile(); var tileRect:Rectangle = new Rectangle(); tileRect.width=tileSize; tileRect.height=tileSize; var tilePoint:Point = new Point(0,0); for (var i=0; i<tilesX; i++) { for (var j=0; j<tilesY; j++) { //Grab the tile tile=theWorld[i][j]; //Check to see if it needs sorting if (tile.isSorted==1) { tileRect.x = int((tile.id % tiles_perRow))*tileSize; tileRect.y = int((tile.id / tiles_perRow))*tileSize; //Create a temporary buffer tile var bufferTile:Bitmap = new Bitmap(); bufferTile.bitmapData = new BitmapData(tileSize,tileSize,true,0x00000000); //Copy pixels from the tileSet to the bufferTile bufferTile.bitmapData.copyPixels(worldTileSet.bitmapData,tileRect,tilePoint); //Create a SortedTile using the bufferTile bitmapData var sTile = new SortedTile(bufferTile.bitmapData,j*tileSize,i*tileSize); //Add the Sorted tile to our Engine's sorting array Engine.sortedItems.push(sTile); //And finally to the stage stageRef.addChild(sTile); } } } } //Small function to create Wall objects and add them to the Engine private function setWall(x:int,y:int):void { var tile:Wall = new Wall(x,y,tileSize,tileSize); Engine.walls.push(tile); stageRef.addChild(tile); } //This will be called from our engine every frame public function updateViewPort():void { //Let's make sure we aren't in a loading pattern first! if (loading==true) { return; } //What is the first tile we need to worry about it based on our viewport? //Remember: we only need to worry about visible tiles here. var tilex:int=int(viewXOffset/tileSize); var tiley:int=int(viewYOffset/tileSize); //Loop Counters var rowCtr:int=0; var colCtr:int=0; //tile is used to hold the object of the current tile on the tile sheet var tile:Tile; //The tile Rectange gives us the copy rectangle var tileRect:Rectangle = new Rectangle(); tileRect.width=tileSize; tileRect.height=tileSize; //The tilePoint tells us where to paint the rectangle copy var tilePoint:Point = new Point(); //Now all we need to do is step through all of the visible tiles for (rowCtr=0; rowCtr<=viewRows; rowCtr++) { for (colCtr=0; colCtr<=viewCols; colCtr++) { tile=theWorld[rowCtr+tiley][colCtr+tilex]; tilePoint.x=colCtr*tileSize; tilePoint.y=rowCtr*tileSize; tileRect.x = int((tile.id % tiles_perRow))*tileSize; tileRect.y = int((tile.id / tiles_perRow))*tileSize; //And copy the appropriate tile from our tileset to the buffer buffer.bitmapData.copyPixels(worldTileSet.bitmapData,tileRect,tilePoint); } } //Since our buffer has whole tiles, we need to offset our viewport copy by doing some math //If we divide the view Offset by the tilesize, the remainder will tell us what we need viewPort.x=viewXOffset % tileSize; viewPort.y=viewYOffset % tileSize; //Using the 0-31 pixel shift, copy the buffer to the canvas. canvas.bitmapData.copyPixels(buffer.bitmapData,viewPort,viewPointer); } //This function will be called from our engine every frame public function centerViewPort(x:int, y:int):void { //It centers the viewPort on whatever X,Y is passed viewXOffset=x-(viewPort.width/2); viewYOffset=y-(viewPort.height/2); if (viewYOffset<0) { viewYOffset=0; } if (viewYOffset>(worldPixSizeY-viewPort.height-1)) { viewYOffset = worldPixSizeY-viewPort.height-1; } if (viewXOffset<0) { viewXOffset=0; } if (viewXOffset>worldPixSizeX-viewPort.width-1) { viewXOffset = worldPixSizeX-viewPort.width-1; } } //A public function that will translate world coordinates to absolute screen coordinates public function translateCoordinate(p:Point):Point { p.x-=viewXOffset; p.y-=viewYOffset; return p; } //IO handler for the bitmap load private function ioErrorHandler(event:IOErrorEvent):void { trace("Load Error"); } //IO handler for the XML load private function configureListeners(dispatcher:IEventDispatcher):void { dispatcher.addEventListener(Event.COMPLETE, completeXMLSetup); //dispatcher.addEventListener(Event.OPEN, openHandler); //dispatcher.addEventListener(ProgressEvent.PROGRESS, progressHandler); //dispatcher.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); //dispatcher.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler); //dispatcher.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); } } }
Engine.as
package com.refrag.tileTest06{ import flash.display.Loader; import flash.display.MovieClip; import flash.events.*; import flash.net.*; import flash.geom.Point; import com.senocular.utils.KeyObject; import flash.ui.Keyboard; import flash.utils.getTimer; //Our main engine. This class is loaded when we start our Flash game and handles our game loops public class Engine extends MovieClip { //An array for the badguys public static var enemyList:Array = new Array(); //An array for objects that need index (z) sorting public static var sortedItems:Array = new Array(); //An array for wall tiles public static var walls:Array = new Array(); //Our key listener var key = new KeyObject(stage); //Variables used by our loop timer and fps meter private var time:int; private var prevTime:int = 0; private var fps:int; var frameTime:int =0; //Our Hero!! var ourHero:Hero = new Hero(stage); //Our World!! var ourWorld:World = new World(stage); public function Engine() { addEventListener(Event.ENTER_FRAME, loop, false, 0, true); sortedItems.push(ourHero); stage.addChild(ourHero); } //Main Loop (every frame) private function loop(e:Event):void { frameTime=getTimer() //Center the world view port on our Hero ourWorld.centerViewPort(ourHero.worldX,ourHero.worldY); //Update the viewport (draw tiles) ourWorld.updateViewPort(); //Resolve absolte x,y coordinates for all special (wall, sorted) tiles positionTiles(); //Resolve absolute x,y coordinates for all sprites positionSprites(); //Depth sort all of our sprites arrange(); //Get our FPS & calculate how many milleseconds our game loop takes getFps(); Hud.msText.text="Loop ms: " + (getTimer()-frameTime); } //We sort our sortedItems by their y coordinate private function arrange():void { sortedItems.sortOn("y", Array.NUMERIC); var i:int = sortedItems.length; while (i--) { if (stage.getChildIndex(sortedItems[i]) != i) { stage.setChildIndex(sortedItems[i], i+1); } } } //Right now all this does is figure out where the Hero should be on the stage private function positionSprites():void{ //The code is complex because we need the hero to be the center of the viewport //unless the viewport is stuck at the end of the drawable world. //In that case, the Hero should be allowed to move to the end pixels. if (ourHero.worldX>(ourWorld.worldPixSizeX)-(ourWorld.viewWidth/2)) ourHero.x=ourHero.worldX - (ourWorld.worldPixSizeX-ourWorld.viewWidth); if (ourHero.worldX<(ourWorld.viewWidth/2)) ourHero.x=ourHero.worldX; if (ourHero.worldY>(ourWorld.worldPixSizeY)-(ourWorld.viewHeight/2)) ourHero.y=ourHero.worldY - (ourWorld.worldPixSizeY-ourWorld.viewHeight); if (ourHero.worldY<(ourWorld.viewHeight/2)) ourHero.y=ourHero.worldY; } //Here we determine what the absolute x,y values for all special tiles should be private function positionTiles():void{ var p:Point=new Point(); var newP:Point=new Point(); var i:int = walls.length; //First we step through the walls while (i--){ p = new Point(walls[i].worldX,walls[i].worldY); newP = ourWorld.translateCoordinate(p); walls[i].x=newP.x; walls[i].y=newP.y; } i = sortedItems.length; //Then the sorted tiles while (i--){ p = new Point(sortedItems[i].worldX,sortedItems[i].worldY); newP = ourWorld.translateCoordinate(p); sortedItems[i].x=newP.x; sortedItems[i].y=newP.y; } } //Just a small function to figure out our fps private function getFps():void { time = getTimer(); fps = 1000 / (time - prevTime); Hud.fpsText.text = "fps: " + fps; prevTime = getTimer(); } } }







{ 3 comments… read them below or add one }
Hi there!
I really apreciate you are doing this because I always have been interested in making a top down game in AS3, so I probably will find your code useful.
thx & enjoy coding
Thank you very much for this. Would you mind zipping the source and making it available for download?
Hi,
I m doing something similar have look:
- http://angelstreetv2.free.fr/as3/IsoEngine_AS3.swf
- http://angelstreetv2.free.fr/as3/Editeur_AS3/Editeur.swf
- http://code.google.com/p/2d-isometric-engine/