Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Creating Isometric Worlds: A Primer for Game Developers, Continued

$
0
0

In this tutorial, we’ll build on the original Creating Isometric Worlds primer, and learn about implementing pickups, trigger tiles, level swapping, path finding and following, level scrolling, isometric height and isometric projectiles.


1. Pickups

Pickups are items that can be collected within the level, normally by simply walking over them – for example, coins, gems, cash, and ammo.

Pickup data can also be accommodated right into our level data as below:

[[1,1,1,1,1,1],
 [1,0,0,0,0,1],
 [1,0,8,0,0,1],
 [1,0,0,8,0,1],
 [1,0,0,0,0,1],
 [1,1,1,1,1,1]]

In this level data, we use 8 to denote a pickup (1 and 0 represent walls and walkable tiles respectively, as before).

It’s important to understand that 8 actually denotes two tiles, not just one: it means we need to first place a walkable grass tile and then place a pickup on top. This means that every pickup will always be on a grass tile. If we want it to be on a walkable brick tile, then we’ll need another tile denoted by another number, say 9, that represents “pickup on brick tile”.

Typical isometric art will have multiple walkable tiles – suppose we have 30. The above approach means that if we have N pickups we will need (N * 30) tiles in addition to the 30 original tiles, as each tile will need to have one version with pickups and one without. This is not very efficient; instead, we should try to dynamically create these combinations.

To do this, we can use another array with the pickup data alone, and use this to place pickup tiles atop the level layout data:

// Level layout data
[[1,1,1,1,1,1],
 [1,0,0,0,0,1],
 [1,0,0,0,0,1],
 [1,0,0,0,0,1],
 [1,0,0,0,0,1],
 [1,1,1,1,1,1]]

…plus:

// Pickup layout data
[[0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,8,0,0,0],
 [0,0,0,8,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0]]

…results in:

pickups in level

This approach ensures that we need only the 30 original tiles in addition to N pickup tiles, as we can create any combination by blending both pieces of art when rendering the level.

Picking Up Pickups

Detecting pickups is done in the same way as detecting collision tiles, but after moving the character.

tile coordinate= getTileCoordinates (isoTo2D (Iso point) , tile height);
if(isPickup(tile coordinate)) {
  pickupItem(tile coordinate);
}

In the function isPickup(tile coordinate), we check whether the pickup data array value at the given coordinate is a pickup tile or not. The number in the pickup array at that tile coordinate denotes the type of pickup.

We check for collisions before moving the character but check for pickups afterwards, because in the case of collisions the character should not occupy the spot if it is already occupied by the collision tile, but in case of pickups the character is free to move over it.

Another thing to note is that the collision data usually never changes, but the pickup data changes whenever we pick up an item. (This usually just involves changing the value in the pickup array from, say, 8 to 0.)

This leads to a problem: what happens when we need to restart the level, and thus reset all pickups back to their original positions? We do not have the information to do this, as the pickup array has been changed as the player picked up items. The solution is to use a duplicate array for pickups while in play and to keep the original pickup array intact – for instance, we use pickupsArray[] and pickupsLive[], clone the latter from the former at the start of the level, and only change pickupsLive[] during play.


Click to give the SWF focus, then use the arrow keys. Click here for the full-sized version.

You should notice that we check for pickups whenever the character is on that tile. This can happen multiple times within a second (we check only when the user moves, but we may go round and round within a tile) but the above logic won’t fail; since we set the pickup array data to 0 the first time we detect a pickup, all subsequent isPickup(tile) checks will returns false for that tile.


2. Trigger Tiles

As the name suggests, trigger tiles cause something to happen when the player steps on them or presses a key while stepping on them. They might teleport the player to a different location, open a gate, or spawn an enemy, to give a few examples. In a sense, pickups are just a special form of trigger tiles: when the player steps on a tile containing a coin, the coin disappears and their coin counter increases.

Let’s look at how we could implement a door that takes the player to a different level. The tile next to the door will be a trigger tile; when the player presses the Space bar, they’ll proceed to the next level.

hero at door

To change levels, all we need to do is swap the current level data array with that of the new level, and set the new tile position and direction for the hero character.

Suppose there are two levels with doors to allow passing between them. Since the ground tile next to the door will be the trigger tile in both levels, we can use this as the new position for the character when they appear in the level.

The implementation logic here is the same as for pickups, and again we use an array to store trigger values. This is inefficient and you should consider other data structures for this purpose, but let’s keep this simple for the sake of the tutorial. Let the new level arrays be as below (7 denotes a door):

// Level 1
[[1,1,1,1,1,1],
 [1,0,0,0,0,1],
 [1,0,0,0,0,7],
 [1,0,0,0,0,1],
 [1,0,0,0,1,1],
 [1,1,1,1,1,1]];

// Level 2
[[1,1,1,1,1,1],
 [1,0,0,0,1,1],
 [7,0,0,0,0,1],
 [1,0,0,0,0,1],
 [1,1,0,0,0,1],
 [1,1,1,1,1,1]];

Let levels have a few pickups, as detailed by the below pickup arrays:

[[0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,8,0,0,0],
 [0,0,0,8,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0]];

[[0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,8,0],
 [0,0,8,0,0,0],
 [0,0,0,0,0,0]];

Let the corresponding trigger tile arrays for each level be as below:

[[0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,2,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0]];

[[0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,1,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0],
 [0,0,0,0,0,0]];

The values (1 and 2) denote the level which will be loaded when the player presses Space.

Here’s the code that runs when the player hits that key:

// Pseudocode
tile coordinate = getTileCoordinates(isoTo2D(Iso point), tile height);
if (isTrigger(tile coordinate)) {
   doRelevantAction(tile coordinate);
}

The function isTrigger() checks whether the trigger data array value at the given coordinate is greater than zero. If so, our code passes that value to doRelevantAction(), which decides which function to call next. For our purposes, we’ll use the simple rule that if the value lies between 1 and 10, it’s a door, and so this function will be called:

function swapLevel(level) {
    // swap level arrays, pickup arrays, and trigger arrays with new level's data
    // move player's starting position to tile next to door that leads to previous level
    // set character's direction
}

Since the value of the tile in the trigger array also denotes the level which needs to be loaded, we can simply pass it to swapLevel(). This implies, in turn, that our game has ten levels.

Here is a working demo. Try picking up items by walking over them and swapping levels by standing next to doors and hitting Space.


Click to give the SWF focus, then use the arrow keys. Click here for the full-sized version.

I have made the trigger be activated when Space is released; if we just listen for the key being pressed then we end up in a loop where we swap between levels as long as the key is held down, since the character always spawns in the new level on top of a trigger tile.

Here is the full code (in AS3):

import flash.display.Sprite;
import com.csharks.juwalbose.IsoHelper;
import flash.display.MovieClip;
import flash.geom.Point;
import flash.filters.GlowFilter;
import flash.events.Event;
import com.senocular.utils.KeyObject;
import flash.ui.Keyboard;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.geom.Rectangle;
import flash.events.KeyboardEvent;

var level1Data=[[1,1,1,1,1,1],
[1,0,0,0,0,1],
[1,0,0,0,0,7],
[1,0,0,0,0,1],
[1,0,0,0,1,1],
[1,1,1,1,1,1]];

var pickup1Array=[[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,8,0,0,0],
[0,0,0,8,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0]];

var level2Data=[[1,1,1,1,1,1],
[1,0,0,0,1,1],
[7,0,0,0,0,1],
[1,0,0,0,0,1],
[1,1,0,0,0,1],
[1,1,1,1,1,1]];

var pickup2Array=[[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,8,0],
[0,0,8,0,0,0],
[0,0,0,0,0,0]];

var trigger1Array=[[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,2,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0]];

var trigger2Array=[[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,1,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0]];
var levelData:Array=new Array();
var pickupArray:Array=new Array();
var triggerArray:Array=new Array();
var currentLevel:uint=1;
var newLevel:uint=1;

var tileWidth:uint = 50;
var borderOffsetY:uint = 70;
var borderOffsetX:uint = 275;

var facing:String = "south";
var currentFacing:String = "south";
var hero:MovieClip=new herotile();
hero.clip.gotoAndStop(facing);
var heroPointer:Sprite;
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class
var heroHalfSize:uint=20;
var pickupCount:uint=0;

//the tiles
var grassTile:MovieClip=new TileMc();
grassTile.gotoAndStop(1);
var wallTile:MovieClip=new TileMc();
wallTile.gotoAndStop(2);
var pickupTile:MovieClip=new TileMc();
pickupTile.gotoAndStop(3);
var doorTile:MovieClip=new TileMc();
doorTile.gotoAndStop(4);

//the canvas
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
addChild(bg);
var rect:Rectangle=bg.bitmapData.rect;

//to handle depth
var overlayContainer:Sprite=new Sprite();
addChild(overlayContainer);

//to handle direction movement
var dX:Number = 0;
var dY:Number = 0;
var idle:Boolean = true;
var speed:uint = 5;
var heroCartPos:Point=new Point();
var heroTile:Point=new Point();

var spaceKeyUp:Boolean=false;
//initial hero position
var spawnPt:Point=new Point(1,3);

//add items to start level, add game loop
function createLevel()
{
	if(currentLevel==1){
		levelData=level1Data;
		pickupArray=pickup1Array;
		triggerArray=trigger1Array;
	}else{
		levelData=level2Data;
		pickupArray=pickup2Array;
		triggerArray=trigger2Array;
	}
	levelData[spawnPt.x][spawnPt.y]=2;
	
	var tileType:uint;
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			placeTile(tileType,i,j);
			if (tileType == 2)
			{
				//trace(i,j);
				levelData[i][j] = 0;
			}
		}
	}
	
	depthSort();
	addEventListener(Event.ENTER_FRAME,loop);
}

//place the tile based on coordinates
function placeTile(id:uint,i:uint,j:uint)
{
var pos:Point=new Point();
	if (id == 2)
	{
		
		id = 0;
		pos.x = j * tileWidth;
		pos.y = i * tileWidth;
		pos = IsoHelper.twoDToIso(pos);
		hero.x = borderOffsetX + pos.x;
		hero.y = borderOffsetY + pos.y;
		//overlayContainer.addChild(hero);
		heroCartPos.x = j * tileWidth;
		heroCartPos.y = i * tileWidth;
		heroTile.x=j;
		heroTile.y=i;
		heroPointer=new herodot();
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;
		
	}
	
}

//the game loop
function loop(e:Event)
{
	
	if (key.isDown(Keyboard.UP))
	{
		dY = -1;
	}
	else if (key.isDown(Keyboard.DOWN))
	{
		dY = 1;
	}
	else
	{
		dY = 0;
	}
	if (key.isDown(Keyboard.RIGHT))
	{
		dX = 1;
		if (dY == 0)
		{
			facing = "east";
		}
		else if (dY==1)
		{
			facing = "southeast";
			dX = dY=0.5;
		}
		else
		{
			facing = "northeast";
			dX=0.5;
			dY=-0.5;
		}
	}
	else if (key.isDown(Keyboard.LEFT))
	{
		dX = -1;
		if (dY == 0)
		{
			facing = "west";
		}
		else if (dY==1)
		{
			facing = "southwest";
			dY=0.5;
			dX=-0.5;
		}
		else
		{
			facing = "northwest";
			dX = dY=-0.5;
		}
	}
	else
	{
		dX = 0;
		if (dY == 0)
		{
			//facing="west";
		}
		else if (dY==1)
		{
			facing = "south";
		}
		else
		{
			facing = "north";
		}
	}
	if (dY == 0 && dX == 0)
	{
		hero.clip.gotoAndStop(facing);
		idle = true;
	}
	else if (idle||currentFacing!=facing)
	{
		idle = false;
		currentFacing = facing;
		hero.clip.gotoAndPlay(facing);
	}
	if (! idle && isWalkable())
	{
		heroCartPos.x +=  speed * dX;
		heroCartPos.y +=  speed * dY;
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;

		var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
		//collision check
		hero.x = borderOffsetX + newPos.x;
		hero.y = borderOffsetY + newPos.y;
		heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
		
		if(isPickup(heroTile)){
			pickupItem(heroTile);
		}
		depthSort();
		//trace(heroTile);
	}
	tileTxt.text="Level "+ currentLevel.toString()+" - x: "+heroTile.x +" & y: "+heroTile.y;
	pickupTxt.text="Pickups Collected: "+pickupCount.toString();
	
	if (spaceKeyUp)
	{
		spaceKeyUp=false;
		if(isTrigger(heroTile)){
  			doRelevantAction(heroTile);
		}
	}
}
function doRelevantAction(tilePt:Point):void{
	newLevel=triggerArray[tilePt.y][tilePt.x];
	
	if(newLevel==1){
		spawnPt=getSpawn(trigger1Array,currentLevel);
	}else{
		spawnPt=getSpawn(trigger2Array,currentLevel);
	}
	
	swapLevel(newLevel);
}
function getSpawn(ar:Array,index:uint):Point{
	var tilePt:Point=new Point();
	for (var i:uint=0; i<ar.length; i++)
	{
		for (var j:uint=0; j<ar[0].length; j++)
		{
			if(ar[i][j]==index){
				tilePt.x=i;
				tilePt.y=j;
				break;
			}
		}
	}
	return tilePt;
}
function swapLevel(level:uint):void{
	removeEventListener(Event.ENTER_FRAME,loop);
	currentLevel=level;
	//trace("load",level);
	createLevel();
}
function isPickup(tilePt:Point):Boolean{
	return(pickupArray[tilePt.y][tilePt.x]==8);
}
function isTrigger(tilePt:Point):Boolean{
	return(triggerArray[tilePt.y][tilePt.x]>0);
}
function pickupItem(tilePt:Point):void{
	pickupCount++;
	pickupArray[tilePt.y][tilePt.x]=0;
}
//check for collision tile
function isWalkable():Boolean{
	var able:Boolean=true;
	var newPos:Point =new Point();
	newPos.x=heroCartPos.x +  (speed * dX);
	newPos.y=heroCartPos.y +  (speed * dY);
	switch (facing){
		case "north":
			newPos.y-=heroHalfSize;
		break;
		case "south":
			newPos.y+=heroHalfSize;
		break;
		case "east":
			newPos.x+=heroHalfSize;
		break;
		case "west":
			newPos.x-=heroHalfSize;
		break;
		case "northeast":
			newPos.y-=heroHalfSize;
			newPos.x+=heroHalfSize;
		break;
		case "southeast":
			newPos.y+=heroHalfSize;
			newPos.x+=heroHalfSize;
		break;
		case "northwest":
			newPos.y-=heroHalfSize;
			newPos.x-=heroHalfSize;
		break;
		case "southwest":
			newPos.y+=heroHalfSize;
			newPos.x-=heroHalfSize;
		break;
	}
	newPos=IsoHelper.getTileCoordinates(newPos,tileWidth);
	if(levelData[newPos.y][newPos.x]>=1){
		able=false;
	}else{
		//trace("new",newPos);
	}
	return able;
}

//sort depth & draw to canvas
function depthSort()
{
	bg.bitmapData.lock();
	bg.bitmapData.fillRect(rect,0xffffff);
	var tileType:uint;
	var mat:Matrix=new Matrix();
	var pos:Point=new Point();
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			//placeTile(tileType,i,j);

			pos.x = j * tileWidth;
			pos.y = i * tileWidth;
			pos = IsoHelper.twoDToIso(pos);
			mat.tx = borderOffsetX + pos.x;
			mat.ty = borderOffsetY + pos.y;
			if(tileType==0){
				bg.bitmapData.draw(grassTile,mat);
			}else if(tileType==1){
				bg.bitmapData.draw(wallTile,mat);
			}else if(tileType==7){
				bg.bitmapData.draw(doorTile,mat);
			}
			if(pickupArray[i][j]==8){
				bg.bitmapData.draw(pickupTile,mat);
			}
			if(heroTile.x==j&&heroTile.y==i){
				mat.tx=hero.x;
				mat.ty=hero.y;
				bg.bitmapData.draw(hero,mat);
			}

		}
	}
	bg.bitmapData.unlock();
//add character rectangle
}
function handleKeyUp(e:KeyboardEvent):void{//listen for space key release
	if(e.charCode==Keyboard.SPACE){
		spaceKeyUp=true;
	}
}
stage.addEventListener(KeyboardEvent.KEY_UP,handleKeyUp);
createLevel();

3. Path Finding

Path finding and path following is a fairly complicated process. There are various approaches using different algorithms for finding the path between two points, but our level data is a 2D array things are easier than they might otherwise be – we have well defined and unique nodes which the player can occupy and we can easily check whether they are walkable.

A detailed overview of pathfinding algorithms is outside of the scope of this article but I will try to explain the most common way it works: the shortest path algorithm, of which A* and Dijkstra’s algorithms are famous implementations.

We aim to find nodes connecting a starting node and an ending node. From the starting node we visit all eight neighboring nodes and mark them all as visited; this core process is repeated for each newly visited node, recursively. Each thread tracks the nodes visited. When jumping to neighboring nodes, nodes that have already been visited nodes are skipped (the recursion stops); otherwise, the process continues until we reach the ending node, where the recursion ends and the full path followed is returned as a node array. Sometimes the end node is never reached, in which case the path finding fails. We usually end up finding multiple paths between the two nodes, in which case we take the one with the least number of nodes.

There are many standard solutions available for path finding based on 2D arrays, so we’ll skip reinventing that wheel. Let’s use this AS3 solution (I recommend you check out this great explanatory demo)).

The solution returns an array with points forming the path from the starting point to the end point:

path = PathFinder.go(start x, start y, end x, end y, levelData);

Path Following

Once we have the path as a node array, we need to make the character follow it.

Say we want to make the character walk to a tile that we click on. We first need to look for a path between the node that the character currently occupies and the node where we clicked. If a successful path is found, then we need to move the character to the first node in the node array by setting is as the destination. Once we get to the destination node, we check where there are any more nodes in the node array and, if so, set the next node as destination – and so on until we reach the final node.

We will also change the direction of the player based on the current node and new destination node each time we reach a node. Between nodes, we just walk in the required direction until we reach the destination node. This is a very simple AI.

Check out this working example:


Click to move the character. Click here for the full-sized version.

Here’s the full source:

import flash.display.Sprite;
import com.csharks.juwalbose.IsoHelper;
import flash.display.MovieClip;
import flash.geom.Point;
import flash.filters.GlowFilter;
import flash.events.Event;
import com.senocular.utils.KeyObject;
import flash.ui.Keyboard;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.geom.Rectangle;
import flash.events.MouseEvent;

import libs.PathFinder.*;

var levelData=[[1,1,1,1,1,1,1,1],
[1,0,0,0,0,2,0,1],
[1,0,0,1,0,0,0,1],
[1,1,0,0,0,0,0,1],
[1,0,0,0,0,1,1,1],
[1,0,0,1,0,0,0,1],
[1,0,0,1,0,0,0,1],
[1,1,1,1,1,1,1,1]];

var tileWidth:uint = 50;
var borderOffsetY:uint = 70;
var borderOffsetX:uint = 275;

var facing:String = "south";
var currentFacing:String = "south";
var hero:MovieClip=new herotile();
hero.clip.gotoAndStop(facing);
var heroPointer:Sprite;
var heroHalfSize:uint=20;

//the tiles
var grassTile:MovieClip=new TileMc();
grassTile.gotoAndStop(1);
var wallTile:MovieClip=new TileMc();
wallTile.gotoAndStop(2);

//the canvas
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
addChild(bg);
var rect:Rectangle=bg.bitmapData.rect;

//to handle depth
var overlayContainer:Sprite=new Sprite();
addChild(overlayContainer);

//to handle direction movement
var dX:Number = 0;
var dY:Number = 0;
var idle:Boolean = true;
var speed:uint = 5;
var heroCartPos:Point=new Point();
var heroTile:Point=new Point();

var path:Array=new Array();
var destination:Point=new Point();
var stepsTillTurn:uint=5;
var stepsTaken:uint;

var glowFilter:GlowFilter=new GlowFilter(0x00ffff);

//add items to start level, add game loop
function createLevel()
{
	var tileType:uint;
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			placeTile(tileType,i,j);
			if (tileType == 2)
			{
				levelData[i][j] = 0;
			}
		}
	}
	overlayContainer.addChild(heroPointer);
	overlayContainer.alpha=0.9;
	overlayContainer.scaleX=overlayContainer.scaleY=0.3;
	overlayContainer.y=320;
	overlayContainer.x=10;
	depthSort();
	
	addEventListener(Event.ENTER_FRAME,loop);
}

//place the tile based on coordinates
function placeTile(id:uint,i:uint,j:uint)
{
var pos:Point=new Point();
	if (id == 2)
	{
		
		id = 0;
		pos.x = j * tileWidth +tileWidth/2;
		pos.y = i * tileWidth +tileWidth/2;
		pos = IsoHelper.twoDToIso(pos);
		hero.x = borderOffsetX + pos.x;
		hero.y = borderOffsetY + pos.y;
		//overlayContainer.addChild(hero);
		heroCartPos.x = j * tileWidth +tileWidth/2;
		heroCartPos.y = i * tileWidth +tileWidth/2;
		
		heroTile.x=j;
		heroTile.y=i;
		heroPointer=new herodot();
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;
		
	}
	var tile:MovieClip=new cartTile();
	tile.gotoAndStop(id+1);
	tile.x = j * tileWidth;
	tile.y = i * tileWidth;
	tile.name="tile_"+j.toString()+"_"+i.toString();
	overlayContainer.addChild(tile);
}

//the game loop
function loop(e:Event)
{
	aiWalk();
	
	if (dY == 0 && dX == 0)
	{
		hero.clip.gotoAndStop(facing);
		idle = true;
	}
	else if (idle||currentFacing!=facing)
	{
		//trace(idle,"facing ",currentFacing,facing);
		idle = false;
		
		currentFacing = facing;
		hero.clip.gotoAndPlay(facing);
	}
	if (! idle )
	{
		heroCartPos.x +=  speed * dX;
		heroCartPos.y +=  speed * dY;
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;

		var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
		//collision check
		hero.x = borderOffsetX + newPos.x;
		hero.y = borderOffsetY + newPos.y;
		heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
		depthSort();
		//trace(heroTile);
	}
	tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y;
}
function aiWalk():void{
	if(path.length==0){//path has ended
		dX=dY=0;
		return;
	}
	if(heroTile.equals(destination)){//reached current destination, set new, change direction
		//wait till we are few steps into the tile before we turn
		stepsTaken++;
		if(stepsTaken<stepsTillTurn){
			return;
		}
		//place the hero at tile middle before turn
		var pos:Point=new Point();
		pos.x = heroTile.x * tileWidth +tileWidth/2;
		pos.y = heroTile.y * tileWidth +tileWidth/2;
		pos = IsoHelper.twoDToIso(pos);
		hero.x = borderOffsetX + pos.x;
		hero.y = borderOffsetY + pos.y;
		heroCartPos.x = heroTile.x * tileWidth +tileWidth/2;
		heroCartPos.y = heroTile.y * tileWidth +tileWidth/2;
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;
		depthSort();
		
		//new point, turn, find dX,dY
		stepsTaken=0;
		destination=path.pop();
		if(heroTile.x<destination.x){
			dX = 1;
		}else if(heroTile.x>destination.x){
			dX = -1;
		}else {
			dX=0;
		}
		if(heroTile.y<destination.y){
			dY = 1;
		}else if(heroTile.y>destination.y){
			dY = -1;
		}else {
			dY=0;
		}
		if(heroTile.x==destination.x){//top or bottom
			dX=0;
		}else if(heroTile.y==destination.y){//left or right
			dY=0;
		}
		
		if (dX == 1)
	{
		
		if (dY == 0)
		{
			facing = "east";
		}
		else if (dY==1)
		{
			facing = "southeast";
			dX = dY=0.5;
		}
		else
		{
			facing = "northeast";
			dX=0.5;
			dY=-0.5;
		}
	}
	else if (dX==-1)
	{
		
		if (dY == 0)
		{
			facing = "west";
		}
		else if (dY==1)
		{
			facing = "southwest";
			dY=0.5;
			dX=-0.5;
		}
		else
		{
			facing = "northwest";
			dX = dY=-0.5;
		}
	}
	else
	{
		
		if (dY == 0)
		{
			facing=currentFacing;
		}
		else if (dY==1)
		{
			facing = "south";
		}
		else
		{
			facing = "north";
		}
	}
	}
	
	
	
}
//sort depth & draw to canvas
function depthSort()
{
	bg.bitmapData.lock();
	bg.bitmapData.fillRect(rect,0xffffff);
	var tileType:uint;
	var mat:Matrix=new Matrix();
	var pos:Point=new Point();
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			//placeTile(tileType,i,j);

			pos.x = j * tileWidth;
			pos.y = i * tileWidth;
			pos = IsoHelper.twoDToIso(pos);
			mat.tx = borderOffsetX + pos.x;
			mat.ty = borderOffsetY + pos.y;
			if(tileType==0){
				bg.bitmapData.draw(grassTile,mat);
			}else{
				bg.bitmapData.draw(wallTile,mat);
			}
			if(heroTile.x==j&&heroTile.y==i){
				mat.tx=hero.x;
				mat.ty=hero.y;
				bg.bitmapData.draw(hero,mat);
			}

		}
	}
	bg.bitmapData.unlock();
//add character rectangle
}
function handleMouseClick(e:MouseEvent):void{
	path.splice(0,path.length);
	var clickPt:Point=new Point();
	clickPt.x=e.stageX-borderOffsetX;
	clickPt.y=e.stageY-borderOffsetY;
	clickPt=IsoHelper.isoTo2D(clickPt);
	clickPt.x-=tileWidth/2;
	clickPt.y+=tileWidth/2;
	clickPt=IsoHelper.getTileCoordinates(clickPt,tileWidth);
	if(clickPt.x<0||clickPt.y<0||clickPt.x>levelData.length-1||clickPt.x>levelData[0].length-1){
		//trace("invalid");
		//we have clicked outside
		return;
	}
	if(levelData[clickPt.y][clickPt.x]==1){
		//trace("wall");
		//we clicked on a wall
		return;
	}
	trace("find ",heroTile, clickPt);
	destination=heroTile;
	path= PathFinder.go(heroTile.x, heroTile.y, clickPt.x, clickPt.y, levelData);
	path.reverse();
	path.push(clickPt);
	path.reverse();
	paintPath();
}
function paintPath():void{//show hihglighted path in minimap
	var sp:Sprite;
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			sp=overlayContainer.getChildByName("tile_"+j.toString()+"_"+i.toString()) as Sprite;
			sp.filters=[];
		}
	}
	for(i=0;i<path.length;i++){
		sp=overlayContainer.getChildByName("tile_"+(path[i].x).toString()+"_"+(path[i].y).toString()) as Sprite;
		sp.filters=[glowFilter];
		overlayContainer.setChildIndex(sp,overlayContainer.numChildren-1);
	}
	overlayContainer.setChildIndex(heroPointer,overlayContainer.numChildren-1);
}
stage.addEventListener(MouseEvent.CLICK, handleMouseClick);
createLevel();

You may have noticed that I removed the collision check logic; it’s no longer needed as we cannot manually move our character using the keyboard. However, we do need to filter out valid click points by determining whether we’ve clicked within the walkable area, rather than a wall tile or other non-walkable tile.

Another interesting point for coding the AI: we do not want the character to turn to face the next tile in the node array as soon as he has arrived in the current one, as such an immediate turn results in our character walking on the borders of tiles. Instead, we should wait until the character is a few steps inside the tile before we look for the next destination. It is also better to manually place the hero in the middle of the current tile just before we turn, to make it all feel perfect.

Also, if you explore the above demo, you may notice that our draw logic gets disrupted when the hero is moving diagonally close to a wall tile. This is an extreme case where, for one frame, our hero seems to be inside the wall tile. This happens because we have disabled the collision check. One workaround is to use a pathfinding algorithm that ignores the diagonal solutions. (Almost all path finding algorithms have options to enable or disable diagonal walk solutions.)


4. Projectiles

A projectile is something that moves in a particular direction with a particular speed, like a bullet, a magic spell, a ball, and so on.

Everything about the projectile is same as the hero character, apart from the height: rather than rolling along the ground, projectiles often float above it at a certain height. A bullet will travel above the waist level of the character, and even a ball may need to bounce around.

One interesting thing to note is that isometric height is the same as height in a 2D side view. There are no complicated conversions involved. If a ball is 10 pixels above ground in Cartesian coordinates, it is 10 pixels above the ground in isometric coordinates. (In our case, the relevant axis is the y-axis.)

Let’s try to implement a ball bouncing around in our walled grassland. We’ll ignore damping effects (and so make the bouncing continue endlessly), and for a touch of realism we’ll add a shadow to the ball. We move the shadow just like we move the hero character (i.e. without using a height value), but for the ball we must add the height value to the isometric Y value. The height value will change from frame to frame depending on the gravity, and once the ball hits the ground we’ll flip the current velocity along the y-axis.

Before we tackle bouncing in an isometric system, we’ll see how we can implement it in a 2D Cartesian system. Let us represent the height of the ball by a variable zValue. Imagine that, to begin with, the ball is ten pixels high, so zValue = 10. We’ll use two more variables: incrementValue, which starts at 0, and gravity, which has a value of 1.

Each frame, we add incrementValue to zValue, and subtract gravity from incrementValue. When zValue reaches 0, it means the ball has reached the ground; at this point, we flip the sign of incrementValue by multiplying it by -1 turning it into a positive number. This means that the ball will move upwards from the next frame, thus bouncing.

Here’s how that looks in code:

zValue=10;
gravity=1;
incrementValue=0;

gameLoop(){
    incrementValue-=gravity;
    zValue-=incrementValue;
    if(zValue<=0){
        zValue=0;
        incrementValue*=-1;
    }
}

We’re actually going to use a slightly modified version of that:

zValue=10;
gravity=1;
incrementValue=0;
incrementReset=-12

gameLoop(){
    incrementValue-=gravity;
    zValue-=incrementValue;
    if(zValue<=0){
        zValue=0;
        incrementValue=incrementReset;
    }
}

This removes the damping effect, and lets the ball bounce forever.

Applying this to our ball, we get the below demo:


Click to give the SWF focus, then use the arrow keys. Click here for the full-sized version.

Here is the full AS3 code:

import flash.display.Sprite;
import com.csharks.juwalbose.IsoHelper;
import flash.display.MovieClip;
import flash.geom.Point;
import flash.events.Event;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.geom.Rectangle;
import com.senocular.utils.KeyObject;

var levelData=[[1,1,1,1,1,1],
[1,0,0,0,0,1],
[1,0,0,0,0,1],
[1,0,0,2,0,1],
[1,0,0,0,0,1],
[1,1,1,1,1,1]];

var tileWidth:uint = 50;
var borderOffsetY:uint = 70;
var borderOffsetX:uint = 275;

var key:KeyObject = new KeyObject(stage);

var facing:String = "south";
var currentFacing:String = "south";
var heroHalfSize:uint = 20;

//the tiles
var grassTile:MovieClip=new TileMc();
grassTile.gotoAndStop(1);
var wallTile:MovieClip=new TileMc();
wallTile.gotoAndStop(2);
var pickupTile:MovieClip=new TileMc();
pickupTile.gotoAndStop(3);
var ball:Sprite=new Ball();
var shadow_S:Sprite=new Shadow();

//the canvas
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
addChild(bg);
var rect:Rectangle = bg.bitmapData.rect;

//to handle depth
var heroPointer:Sprite;
var overlayContainer:Sprite=new Sprite();
addChild(overlayContainer);

//to handle direction movement
var dX:Number = 0;
var dY:Number = 0;
var speed:uint = 5;
var ballCartPos:Point=new Point();
var ballTile:Point=new Point();

var zValue:int = 50;
var gravity:int = -1;
var incrementValue:Number = 0;

//add items to start level, add game loop
function createLevel()
{
	var tileType:uint;
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			placeTile(tileType,i,j);
			if (tileType == 2)
			{
				levelData[i][j] = 0;
			}
		}
	}
	overlayContainer.addChild(heroPointer);
	overlayContainer.alpha=0.5;
	overlayContainer.scaleX=overlayContainer.scaleY=0.5;
	overlayContainer.y=290;
	overlayContainer.x=10;

	depthSort();
	addEventListener(Event.ENTER_FRAME,loop);
}

//place the tile based on coordinates
function placeTile(id:uint,i:uint,j:uint)
{
	var pos:Point=new Point();
	if (id == 2)
	{

		id = 0;
		pos.x = j * tileWidth;
		pos.y = i * tileWidth;
		pos = IsoHelper.twoDToIso(pos);
		
		ball.x = borderOffsetX + pos.x;
		ball.y = borderOffsetY + pos.y;
		
		ballCartPos.x = j * tileWidth;
		ballCartPos.y = i * tileWidth;
		ballTile.x = j;
		ballTile.y = i;
		
		heroPointer=new herodot();
		heroPointer.x=ballCartPos.x;
		heroPointer.y=ballCartPos.y;
		
	}
	var tile:MovieClip=new cartTile();
	tile.gotoAndStop(id+1);
	tile.x = j * tileWidth;
	tile.y = i * tileWidth;
	overlayContainer.addChild(tile);

}

//the game loop
function loop(e:Event)
{
	incrementValue -=  gravity;
	zValue -=  incrementValue;
	if (zValue <= 0)
	{
		zValue = 0;
		incrementValue = -12;
	}
	if (key.isDown(Keyboard.UP))
	{
		dY = -1;
	}
	else if (key.isDown(Keyboard.DOWN))
	{
		dY = 1;
	}
	else
	{
		dY = 0;
	}
	if (key.isDown(Keyboard.RIGHT))
	{
		dX = 1;
		if (dY == 0)
		{
			facing = "east";
		}
		else if (dY==1)
		{
			facing = "southeast";
			dX = dY = 0.5;
		}
		else
		{
			facing = "northeast";
			dX = 0.5;
			dY = -0.5;
		}
	}
	else if (key.isDown(Keyboard.LEFT))
	{
		dX = -1;
		if (dY == 0)
		{
			facing = "west";
		}
		else if (dY==1)
		{
			facing = "southwest";
			dY = 0.5;
			dX = -0.5;
		}
		else
		{
			facing = "northwest";
			dX = dY = -0.5;
		}
	}
	else
	{
		dX = 0;
		if (dY == 0)
		{
			//facing="west";
		}
		else if (dY==1)
		{
			facing = "south";
		}
		else
		{
			facing = "north";
		}
	}
	if (isWalkable())
	{
		ballCartPos.x +=  speed * dX;
		ballCartPos.y +=  speed * dY;
		
		heroPointer.x=ballCartPos.x;
		heroPointer.y=ballCartPos.y;
		var newPos:Point = IsoHelper.twoDToIso(ballCartPos);
		//collision check
		ball.x = borderOffsetX + newPos.x;
		ball.y = borderOffsetY + newPos.y;
		ballTile = IsoHelper.getTileCoordinates(ballCartPos,tileWidth);

	}
	depthSort();

}

//check for collision tile
function isWalkable():Boolean
{
	var able:Boolean = true;
	var newPos:Point =new Point();
	newPos.x=ballCartPos.x +  (speed * dX);
	newPos.y=ballCartPos.y +  (speed * dY);
	switch (facing)
	{
		case "north" :
			newPos.y -=  heroHalfSize;
			break;
		case "south" :
			newPos.y +=  heroHalfSize;
			break;
		case "east" :
			newPos.x +=  heroHalfSize;
			break;
		case "west" :
			newPos.x -=  heroHalfSize;
			break;
		case "northeast" :
			newPos.y -=  heroHalfSize;
			newPos.x +=  heroHalfSize;
			break;
		case "southeast" :
			newPos.y +=  heroHalfSize;
			newPos.x +=  heroHalfSize;
			break;
		case "northwest" :
			newPos.y -=  heroHalfSize;
			newPos.x -=  heroHalfSize;
			break;
		case "southwest" :
			newPos.y +=  heroHalfSize;
			newPos.x -=  heroHalfSize;
			break;
	}
	newPos = IsoHelper.getTileCoordinates(newPos,tileWidth);
	if (levelData[newPos.y][newPos.x] == 1)
	{
		able = false;
	}
	else
	{
		//trace("new",newPos);
	}
	return able;
}

//sort depth & draw to canvas
function depthSort()
{
	bg.bitmapData.lock();
	bg.bitmapData.fillRect(rect,0xffffff);
	var tileType:uint;
	var mat:Matrix=new Matrix();
	var pos:Point=new Point();
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			//placeTile(tileType,i,j);

			pos.x = j * tileWidth;
			pos.y = i * tileWidth;
			pos = IsoHelper.twoDToIso(pos);
			mat.tx = borderOffsetX + pos.x;
			mat.ty = borderOffsetY + pos.y;
			if (tileType == 0)
			{
				bg.bitmapData.draw(grassTile,mat);
			}
			else
			{
				bg.bitmapData.draw(wallTile,mat);
			}

			if (ballTile.x == j && ballTile.y == i)
			{
				mat.tx = ball.x;//+ tileWidth;
				mat.ty = ball.y;// +tileWidth/2;
				bg.bitmapData.draw(shadow_S,mat);
				mat.ty = mat.ty - zValue - heroHalfSize;
				bg.bitmapData.draw(ball,mat);
			}

		}
	}
	bg.bitmapData.unlock();

}
createLevel();

Do understand that the role played by the shadow is a very important one which adds to the realism of this illusion. In the above example, I have added half the ball’s height to the ball’s y-position, so that it bounces at the right position with respect to the shadow.

Also, note that we’re now using the two screen coordinates (x and y) to represent three dimensions in isometric coordinates – the y-axis in screen coordinates is also the z-axis in isometric coordinates. This can be confusing!


5. Isometric Scrolling

When the level area is much larger than the visible screen area, we will need to make it scroll.

The visible screen area can be considered as a smaller rectangle within the larger rectangle of the complete level area. Scrolling is, essentially, just moving the inner rectangle inside the larger one:

Scrolling Start

Usually, when such scrolling happens, the position of the player remains the same with respect to the screen rectangle, commonly at the screen center. All we need, to implement scrolling, is to track the corner point of the inner rectangle:

Scrolling Next

This corner point, which is in Cartesian coordinates (in the image we can only show the isometric values), will fall within a tile in the level data. For scrolling, we increment the x- and y-position of the corner point in Cartesian coordinates. Now we can convert this point to isometric coordinates and use it to draw the screen.

The newly converted values, in isometric space, need to be the corner of our screen too, which means they are the new (0, 0). So, while parsing and drawing the level data, we subtract this value from the isometric position of each tile, and only draw it if the tile’s new position falls within the screen. We can express this in steps as so:

  • Update Cartesian corner point’s x- and y-coordinates.
  • Convert this to isometric space.
  • Subtract this value from the isometric draw position of each tile.
  • Draw the tile only if the new isometric draw position falls within the screen.

Check out this example (use arrows to scroll):


Click to give the SWF focus, then use the arrow keys. Click here for the full-sized version.

Here’s the full AS3 source code:

import flash.display.Sprite;
import com.csharks.juwalbose.IsoHelper;
import flash.display.MovieClip;
import flash.geom.Point;
import flash.filters.GlowFilter;
import flash.events.Event;
import com.senocular.utils.KeyObject;
import flash.ui.Keyboard;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.geom.Matrix;
import flash.geom.Rectangle;

var levelData=[[1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,1,0,0,0,0,0,0,0,1],
[1,0,0,1,2,0,0,0,0,0,0,1],
[1,0,0,1,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,1,1,1,0,0,0,0,1],
[1,0,0,0,0,0,0,0,0,0,0,1],
[1,0,0,0,0,0,0,0,1,1,0,1],
[1,1,0,0,0,0,0,0,0,0,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1]];

var tileWidth:uint = 50;
var borderOffsetY:uint = 70;
var borderOffsetX:uint = 275;

var facing:String = "south";
var currentFacing:String = "south";
var hero:MovieClip=new herotile();
hero.clip.gotoAndStop(facing);
var heroPointer:Sprite;
var key:KeyObject = new KeyObject(stage);//Senocular KeyObject Class
var heroHalfSize:uint=20;

//the tiles
var grassTile:MovieClip=new TileMc();
grassTile.gotoAndStop(1);
var wallTile:MovieClip=new TileMc();
wallTile.gotoAndStop(2);

//the canvas
var bg:Bitmap = new Bitmap(new BitmapData(650,450));
addChild(bg);
var rect:Rectangle=bg.bitmapData.rect;

//to handle depth
var overlayContainer:Sprite=new Sprite();
addChild(overlayContainer);

//to handle direction movement
var dX:Number = 0;
var dY:Number = 0;
var idle:Boolean = true;
var speed:uint = 6;
var heroCartPos:Point=new Point();
var heroTile:Point=new Point();

var cornerPoint:Point=new Point();

//add items to start level, add game loop
function createLevel()
{
	var tileType:uint;
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			placeTile(tileType,i,j);
			if (tileType == 2)
			{
				levelData[i][j] = 0;
			}
		}
	}
	overlayContainer.addChild(heroPointer);
	overlayContainer.alpha=0.5;
	overlayContainer.scaleX=overlayContainer.scaleY=0.2;
	overlayContainer.y=310;
	overlayContainer.x=10;
	depthSort();
	addEventListener(Event.ENTER_FRAME,loop);
}

//place the tile based on coordinates
function placeTile(id:uint,i:uint,j:uint)
{
var pos:Point=new Point();
	if (id == 2)
	{
		
		id = 0;
		pos.x = j * tileWidth;
		pos.y = i * tileWidth;
		pos = IsoHelper.twoDToIso(pos);
		hero.x = borderOffsetX + pos.x;
		hero.y = borderOffsetY + pos.y;
		//overlayContainer.addChild(hero);
		heroCartPos.x = j * tileWidth;
		heroCartPos.y = i * tileWidth;
		heroTile.x=j;
		heroTile.y=i;
		heroPointer=new herodot();
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;
		
	}
	var tile:MovieClip=new cartTile();
	tile.gotoAndStop(id+1);
	tile.x = j * tileWidth;
	tile.y = i * tileWidth;
	overlayContainer.addChild(tile);
}

//the game loop
function loop(e:Event)
{
	if (key.isDown(Keyboard.UP))
	{
		dY = -1;
	}
	else if (key.isDown(Keyboard.DOWN))
	{
		dY = 1;
	}
	else
	{
		dY = 0;
	}
	if (key.isDown(Keyboard.RIGHT))
	{
		dX = 1;
		if (dY == 0)
		{
			facing = "east";
		}
		else if (dY==1)
		{
			facing = "southeast";
			dX = dY=0.5;
		}
		else
		{
			facing = "northeast";
			dX=0.5;
			dY=-0.5;
		}
	}
	else if (key.isDown(Keyboard.LEFT))
	{
		dX = -1;
		if (dY == 0)
		{
			facing = "west";
		}
		else if (dY==1)
		{
			facing = "southwest";
			dY=0.5;
			dX=-0.5;
		}
		else
		{
			facing = "northwest";
			dX = dY=-0.5;
		}
	}
	else
	{
		dX = 0;
		if (dY == 0)
		{
			//facing="west";
		}
		else if (dY==1)
		{
			facing = "south";
		}
		else
		{
			facing = "north";
		}
	}
	if (dY == 0 && dX == 0)
	{
		hero.clip.gotoAndStop(facing);
		idle = true;
	}
	else if (idle||currentFacing!=facing)
	{
		idle = false;
		currentFacing = facing;
		hero.clip.gotoAndPlay(facing);
	}
	if (! idle && isWalkable())
	{
		heroCartPos.x +=  speed * dX;
		heroCartPos.y +=  speed * dY;
		
		cornerPoint.x -=  speed * dX;
		cornerPoint.y -=  speed * dY;
		
		heroPointer.x=heroCartPos.x;
		heroPointer.y=heroCartPos.y;

		var newPos:Point = IsoHelper.twoDToIso(heroCartPos);
		heroTile=IsoHelper.getTileCoordinates(heroCartPos,tileWidth);
		depthSort();
		//trace(heroTile);
	}
	tileTxt.text="Hero is on x: "+heroTile.x +" & y: "+heroTile.y;
}

//check for collision tile
function isWalkable():Boolean{
	var able:Boolean=true;
	var newPos:Point =new Point();
	newPos.x=heroCartPos.x +  (speed * dX);
	newPos.y=heroCartPos.y +  (speed * dY);
	switch (facing){
		case "north":
			newPos.y-=heroHalfSize;
		break;
		case "south":
			newPos.y+=heroHalfSize;
		break;
		case "east":
			newPos.x+=heroHalfSize;
		break;
		case "west":
			newPos.x-=heroHalfSize;
		break;
		case "northeast":
			newPos.y-=heroHalfSize;
			newPos.x+=heroHalfSize;
		break;
		case "southeast":
			newPos.y+=heroHalfSize;
			newPos.x+=heroHalfSize;
		break;
		case "northwest":
			newPos.y-=heroHalfSize;
			newPos.x-=heroHalfSize;
		break;
		case "southwest":
			newPos.y+=heroHalfSize;
			newPos.x-=heroHalfSize;
		break;
	}
	newPos=IsoHelper.getTileCoordinates(newPos,tileWidth);
	if(levelData[newPos.y][newPos.x]==1){
		able=false;
	}else{
		//trace("new",newPos);
	}
	return able;
}

//sort depth & draw to canvas
function depthSort()
{
	bg.bitmapData.lock();
	bg.bitmapData.fillRect(rect,0xffffff);
	var tileType:uint;
	var mat:Matrix=new Matrix();
	var pos:Point=new Point();
	for (var i:uint=0; i<levelData.length; i++)
	{
		for (var j:uint=0; j<levelData[0].length; j++)
		{
			tileType = levelData[i][j];
			
			//pos.x = j * tileWidth;
			//pos.y = i * tileWidth;
			
			pos.x = j * tileWidth+cornerPoint.x;
			pos.y = i * tileWidth+cornerPoint.y;
			
			pos = IsoHelper.twoDToIso(pos);
			mat.tx = borderOffsetX + pos.x;
			mat.ty = borderOffsetY + pos.y;
			if(tileType==0){
				bg.bitmapData.draw(grassTile,mat);
			}else{
				bg.bitmapData.draw(wallTile,mat);
			}
			if(heroTile.x==j&&heroTile.y==i){
				mat.tx=hero.x;
				mat.ty=hero.y;
				bg.bitmapData.draw(hero,mat);
			}

		}
	}
	bg.bitmapData.unlock();
//add character rectangle
}
createLevel();

Please note that the corner point is incremented in the opposite direction to the hero’s position update as he moves. This makes sure that hero stays where he is with respect to the screen:

heroCartPos.x += speed * dX;
heroCartPos.y += speed * dY;
cornerPoint.x -= speed * dX;
cornerPoint.y -= speed * dY;

The draw logic only changes in two lines, where we determine the Cartesian coordinates of each tile. We just pass the corner point to the original point which actually combines points 1, 2 and 3 above:

pos.x = j * tileWidth+cornerPoint.x;
pos.y = i * tileWidth+cornerPoint.y;

A couple of notes:

  • While scrolling, we may need to draw additional tiles at the screen borders, or else we may see tiles disappearing and appearing at the screen extremes.
  • If you have tiles that take up more than one space, then you will need to draw more tiles at the borders. For example, if the largest tile in the whole set measures X by Y, then you will need to draw X more tiles to the left and right and Y more tiles to the top and bottom. This makes sure that the corners of the bigger tile will still be visible when scrolling in or out of the screen.
  • We still need to make sure that we don’t have blank areas in the screen while we are drawing near the borders of the level.
  • The level should only scroll until the most extreme tile gets drawn at the corresponding screen extreme – after this, the character should continue moving in screen space without the level scrolling. For this, we will need to track all four corners of the inner screen rectangle, and throttle the scrolling and player movement logic accordingly. Are you up for the challenge to try implementing that for yourself?

Conclusion

This series is particularly aimed at beginners trying to explore isometric game worlds. Many of the concepts explained have alternate approaches which are a bit more complicated and I have purposefully chosen the easiest ones. They may not fulfill all scenarios which you may encounter, but the knowledge gained can be used to build upon these concepts to create much complicated solutions.


Viewing all articles
Browse latest Browse all 728

Trending Articles