Terrain Modification in Grid Based Games – Part 1: Isometric Landscape

Terrain Modification Tutorial Thumbnail

Target of this tutorial is the development of a scrolling isometric tile map where the terrain can be manipulated. Inspiration for a test like this were Populous and SimCity with their respective methods of terrain manipulation. While Populous lets the player change the height of a single node (forming a pyramid), SimCity always pushes the whole tile. The challenge here is that node’s heights can only be altered in dependency of their adjacent nodes.
This part deals with the creation of a tile map displaying the isometric map with terrain.

Part 1.1: The basic node grid
Some basic variable defining the grid and tile size as well as the array holding the nodes and its position pendant.

var nodeArray:Array = new Array();
var posArray:Array = new Array();

var nodeRows:int = 9;
var nodeCols:int = 9;

var tileSizeH:int = 32; //horizontal size
var tileSizeV:int = 16; //vertical size
//the tile size represents a 45° rotated square with backwards tilting

This function will create nodes in a nested loop. u stands for columns, v for the respective rows. Nodes are created as Objects, holding information without graphical representation. When a node is created the object is pushed into an array. At the same time a string is created and pushed into a second array with the same index as the object in the first array. That way a node can be found by its coordinates

function makeGrid()
{
	for ( var u = 0; u <= nodeCols; u++ )
	{
		for ( var v = 0; v <= nodeRows; v++ )
		{
			var node:Object = new Object();
			node.u = u;
			node.v = v;
			node.nodePos = u + "." + v;
			
			nodeArray.push(node);
			posArray.push(node.nodePos);
		}
	}
}

Depending on row and column numbers the array holds the respective number of nodes. By using u and v the way shown below for placement the coordinate system is rotated by 45°.

function placeNodes()
{
	for each ( var node in nodeArray )
	{
		node.xPos = ( node.u - node.v ) * tileSizeH;
		node.yPos = ( node.v + node.u ) * tileSizeV;
	}
}

The next function places the nodes on a tilted plane called nodeMap.

function drawNodes()
{
	var map:Sprite = new Sprite();
  //the map size
	var mapSizeH = ( nodeCols - 1 + nodeRows - 1 ) * tileSizeH;
	var mapSizeV = ( nodeRows - 1 + nodeCols - 1 ) * tileSizeV;
  //centering the map on the stage
	map.x = stage.stageWidth * 0.5;
	map.y = stage.stageHeight * 0.5 - mapSizeV * 0.5;
  //graphic options, nodes will be represented by small black circles
	map.graphics.lineStyle(1, 0x000000);

	for each ( var node in nodeArray )
	{
		map.graphics.drawCircle(node.xPos, node.yPos, 3);
	}
	addChild(map);
}

Up to this moment nothing happened. We need to call all three functions to make it work.

makeGrid();
placeNodes();
drawNodes();

Here is what it should look like:


Part 1.2: The basic node grid with heights
Adding height to the nodes is rather simple. Altered code is marked.

var nodeArray:Array = new Array();
var posArray:Array = new Array();

var nodeRows:int = 9;
var nodeCols:int = 9;

var tileSizeH:int = 32; //horizontal
var tileSizeV:int = 16; //vertical
var tileSizeM:int = 8; //mountain

function makeGrid()
{
    for ( var u = 0; u <= nodeCols; u++ )
    {
        for ( var v = 0; v <= nodeRows; v++ )
        {
            var node:Object = new Object();
            node.u = u;
            node.v = v;
            node.nodePos = u + "." + v;
            
            nodeArray.push(node);
            posArray.push(node.nodePos);
        }
    }
}

function placeNodes()
{
    for each ( var node in nodeArray )
    {
        node.xPos = ( node.u - node.v ) * tileSizeH;
        node.yPos = ( node.v + node.u ) * tileSizeV;
        node.zPos = Math.round(Math.random()) * tileSizeM;
    }
}

function drawNodes()
{
    var nodeMap:Sprite = new Sprite();
    var nodeMapSizeH = ( nodeCols - 1 + nodeRows - 1 ) * tileSizeH;
    var nodeMapSizeV = ( nodeRows - 1 + nodeCols - 1 ) * tileSizeV;
    nodeMap.x = stage.stageWidth * 0.5;
    nodeMap.y = stage.stageHeight * 0.5 - nodeMapSizeV * 0.5;
    nodeMap.graphics.lineStyle(1, 0x000000);
    for each ( var node in nodeArray )
    {
        nodeMap.graphics.drawCircle(node.xPos, node.yPos - node.zPos, 3);
    }
    addChild(nodeMap);
}

makeGrid();
placeNodes();
drawNodes();

Right now, we can see that nodes have different heights though it is hardly visible.


Part 1.3: The basic map with drawn tiles
Now we will draw some edges between the nodes. The basic nodes are again drawn without height and act like a zero level now.

var nodeArray:Array = new Array();
var posArray:Array = new Array();

var nodeRows:int = 9;
var nodeCols:int = 9;

var tileSizeH:int = 32; //horizontal
var tileSizeV:int = 16; //vertical
var tileSizeM:int = 8; //mountain

function makeGrid()
{
	for ( var u = 0; u <= nodeCols; u++ )
	{
		for ( var v = 0; v <= nodeRows; v++ )
		{
			var node:Object = new Object();
			node.u = u;
			node.v = v;
			node.nodePos = u + "." + v;
			
			nodeArray.push(node);
			posArray.push(node.nodePos);
		}
	}
}

function placeNodes()
{
	for each ( var node in nodeArray )
	{
		node.xPos = ( node.u - node.v ) * tileSizeH;
		node.yPos = ( node.v + node.u ) * tileSizeV;
		node.zPos = Math.round(Math.random()) * tileSizeM;
	}
}

function drawNodes()
{
	var nodeMap:Sprite = new Sprite();
	var nodeMapSizeH = ( nodeCols - 1 + nodeRows - 1 ) * tileSizeH;
	var nodeMapSizeV = ( nodeRows - 1 + nodeCols - 1 ) * tileSizeV;
	nodeMap.x = stage.stageWidth * 0.5;
	nodeMap.y = stage.stageHeight * 0.5 - nodeMapSizeV * 0.5;
	nodeMap.graphics.lineStyle(1, 0xaaaaaa);
	for each ( var node in nodeArray )
	{
		nodeMap.graphics.drawCircle(node.xPos, node.yPos, 3);
	}
	addChild(nodeMap);
}

makeGrid();
placeNodes();
drawNodes();

With the next function nodes are found in the nodeArray via its position string in the posArray. Remember, node object and position string have the same index in the respective array. That way more difficult map types (hexagonal, triangles) can be used.

function getNodeByCoords( u, v )
{
	var posString:String = u + "." + v;
	var nodePos:int = posArray.indexOf(posString);
	if ( nodePos >= 0 )
	{
		return nodeArray[nodePos];
	}
	else
	{
		return null;
	}
}

Tiles are also created as objects at this point because manipulation is not yet implemented redrawing the whole map right now does't take much time. So, tiles are created with the north, east, south and west node.

var tileArray:Array = new Array();

function makeTiles()
{
	for ( var u = 0; u < nodeCols; u++ )
	{
		for ( var v = 0; v < nodeRows; v++ )
		{
			var tile:Object = new Object();
			tile.n = getNodeByCoords(u, v);
			tile.e = getNodeByCoords(u+1, v);
			tile.s = getNodeByCoords(u+1, v+1);
			tile.w = getNodeByCoords(u, v+1);
			tileArray.push(tile);
		}
	}
}

The northern node will act as reference point here. In many cases the reference point of isometric graphics will be the lower, southern point but those are easily interchangeable.

function drawTiles()
{
	var tileMap:Sprite = new Sprite();
	var tileMapSizeH = ( nodeCols - 1 + nodeRows - 1 ) * tileSizeH;
	var tileMapSizeV = ( nodeRows - 1 + nodeCols - 1 ) * tileSizeV;
	tileMap.x = stage.stageWidth * 0.5;
	tileMap.y = stage.stageHeight * 0.5 - tileMapSizeV * 0.5;
	tileMap.graphics.lineStyle(1, 0x000000);
	for each ( var tile in tileArray )
	{
		with( tileMap.graphics )
		{
			moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
			lineTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
			lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
			lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
			lineTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
		}
	}
	addChild(tileMap);
}

makeTiles();
drawTiles();

Tile edges are drawn from node to node:

Part 1.4: The map wire mesh
By changing the tile drawing function additional edges are shown.

function drawTiles()
{
	var tileMap:Sprite = new Sprite();
	var tileMapSizeH = ( nodeCols - 1 + nodeRows - 1 ) * tileSizeH;
	var tileMapSizeV = ( nodeRows - 1 + nodeCols - 1 ) * tileSizeV;
	tileMap.x = stage.stageWidth * 0.5;
	tileMap.y = stage.stageHeight * 0.5 - tileMapSizeV * 0.5;
	tileMap.graphics.lineStyle(1, 0x000000);
	for each ( var tile in tileArray )
	{
            if ( tile.n.zPos == tile.s.zPos )
            {
                tile.ver = true; //vertical heights are the same
            }
            else
            {
                tile.ver = false;
            }
            if ( tile.e.zPos == tile.w.zPos )
            {
                tile.hor = true; //horizontal heights are the same
            }
            else
            {
                tile.hor = false;
            }
            with(tileMap.graphics)
            {
                if ( tile.ver && tile.hor )
                {
                    if ( tile.n.zPos > tile.e.zPos )
                    {
                        moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
                        lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
                    }
                    else if ( tile.n.zPos < tile.e.zPos )
                    {
                        moveTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
                        lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
                    }
                    //if both are the same, there is no line
                }
                else if ( tile.ver && !tile.hor )
                {
                    moveTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
                    lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
                }
                else if ( !tile.ver && tile.hor )
                {
                moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
                lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
                }
	    moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
	    lineTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
	    lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
	    lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
	    lineTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
        }
    }
    addChild(tileMap);
}

Alright, all terrain changes are included.

Part 1.5: The map wire mesh with refined edges
Though that map does not need any backface culling right now (you can easily look over the mountains, so there are no hidden edges) let's give it some sides to emphasize the 3D look.

function drawTiles()
{
	var tileMap:Sprite = new Sprite();
	var tileMapSizeH = ( nodeCols - 1 + nodeRows - 1 ) * tileSizeH;
	var tileMapSizeV = ( nodeRows - 1 + nodeCols - 1 ) * tileSizeV;
	tileMap.x = stage.stageWidth * 0.5;
	tileMap.y = stage.stageHeight * 0.5 - tileMapSizeV * 0.5;
	//tileMap.graphics.lineStyle(1, 0x000000);
	
	for each ( var tile in tileArray )
	{
		tile.n.zPos == tile.s.zPos ? tile.ver = true : tile.ver = false;
		tile.e.zPos == tile.w.zPos ? tile.hor = true : tile.hor = false;
		
		with(tileMap.graphics)
		{
			lineStyle(1, 0xaaaaaa);
			
			if ( tile.s.u == nodeCols )
			{
				moveTo(tile.e.xPos, tile.e.yPos);
				lineTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
				moveTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
				lineTo(tile.s.xPos, tile.s.yPos);
				lineTo(tile.e.xPos, tile.e.yPos);	
			}
			if ( tile.s.v == nodeRows )
			{
				moveTo(tile.w.xPos, tile.w.yPos);
				lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
				moveTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
				lineTo(tile.s.xPos, tile.s.yPos);
				lineTo(tile.w.xPos, tile.w.yPos);
			}
			
			lineStyle(1, 0x000000);
			
			if ( tile.ver && tile.hor )
			{
				if ( tile.n.zPos > tile.e.zPos )
				{
					moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
					lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
				}
				else if ( tile.n.zPos < tile.e.zPos )
				{
					moveTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
					lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
				}
				//if both are the same, there is no line
			}
			else if ( tile.ver && !tile.hor )
			{
				moveTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
				lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
			}
			else if ( !tile.ver && tile.hor )
			{
				moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
				lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
			}
			moveTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
			lineTo(tile.e.xPos, tile.e.yPos - tile.e.zPos);
			lineTo(tile.s.xPos, tile.s.yPos - tile.s.zPos);
			lineTo(tile.w.xPos, tile.w.yPos - tile.w.zPos);
			lineTo(tile.n.xPos, tile.n.yPos - tile.n.zPos);
		}
	}
	addChild(tileMap);
}

I also did change the horizontal and vertical check to inline conditionals because the script is shorter that way. Two different colors are used to easily differentiate sides and surface. The function now checks if the southern node of a tile is in the last node row or column and if that is the case it draws the remaining visible side of the tile (two in case of the middle one).

Reload the page for different random terrains.

Yoho!

This entry was posted in as3, flash, grids, mochiads, Terrain Modification, Tutorial and tagged , , , , . Bookmark the permalink.

9 Responses to Terrain Modification in Grid Based Games – Part 1: Isometric Landscape