Flash RPG – Tile Engine (3/3)

March 21, 2009

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.

  1. Absolute Ground (always under the Hero)
  2. Sorted Tiles (sorted based on y values)
  3. 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 }

Juan Sierra May 10, 2009 at 12:04 am

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

Reply

Chris August 18, 2009 at 9:46 pm

Thank you very much for this. Would you mind zipping the source and making it available for download?

Reply

N'DOYE September 28, 2009 at 2:43 pm

Leave a Comment

Previous post:

Next post: