Terrain Modification in Grid Based Games – Part 3: Active Landscaping

Terrain Modification Tutorial Thumbnail

This part deals with the implementation of the moving nodes method (Part 2) from one dimension to the pseudo 3D landscape and begins with the movie itself for you to test what it does.
Just hover through the landscape and lift single nodes with a mouse click. Neither am I gone further by implementing lowering nodes nor did I hide the mouse cursor and the green pointer. The first one would have (additionally) increased the number of code lines in this post without adding relevant information. Hiding the cursor would have brought the movie closer to a final state but at the moment it is the method that counts.

I will once again show the whole code (timeline for your convenience, just copy and paste it) because of several small alterations. Here is comes:

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.w = Math.round(Math.random());
			node.nodePos = u + "." + v;
			
			nodeArray.push(node);
			posArray.push(node.nodePos);
		}
	}
}

This creates a 10 by 10 nodes grid (it counts up to nine in the loop, BUT begins with zero), so what we get out of this is capable of being a 9 by 9 tiles grid. The node gets its w value on a random basis. It is either zero or one what also is our maximum difference in height between adjacent nodes. One could go further and enter a higher max value but then the map has to be tested and modified accordingly after creation of all nodes. Else the creation loop itself could bring the existing heights into account. It is absolutely possible but not included in this part.

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

Going through all created nodes and assigning them their three dimensional pixel position and all the nodes that are directly adjacent to them (maxmum four).

var nodeMap:Sprite = new Sprite();
addChild(nodeMap);

function drawNodes()
{
	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.drawEllipse(node.xPos - 4, node.yPos - 2, 8, 4);
	}
}

Creating a new sprite, adding it to the screen and placing it. Flash basics there. Then again looping through all nodes drawing them to the sprite that will serve as the visualization of the "landscapes" "zero level" (would be water in Populous or SimCity).

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

Yes, those three function need to be called for the code to work.

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

This function returns a node by given grid coordinates.

function validateNode ( u, v )
{
	if ( u >= 0 && u <= nodeCols && v >= 0 && v <= nodeRows )
	{
		return true;
	}
	else
	{
		return false;
	}
}

function getNeighbors( cNode )
{
	var nArray:Array = new Array();
	validateNode ( cNode.u, cNode.v - 1 ) ? nArray.push( getNodeByCoords ( cNode.u, cNode.v - 1 ) ) : void;
	validateNode ( cNode.u + 1, cNode.v ) ? nArray.push( getNodeByCoords ( cNode.u + 1, cNode.v ) ) : void;
	validateNode ( cNode.u, cNode.v + 1 ) ? nArray.push( getNodeByCoords ( cNode.u, cNode.v + 1 ) ) : void;
	validateNode ( cNode.u - 1, cNode.v ) ? nArray.push( getNodeByCoords ( cNode.u - 1, cNode.v ) ) : void;

        validateNode ( cNode.u - 1, cNode.v - 1 ) ? nArray.push( getNodeByCoords ( cNode.u - 1, cNode.v - 1 ) ) : void;
	validateNode ( cNode.u - 1, cNode.v + 1 ) ? nArray.push( getNodeByCoords ( cNode.u - 1, cNode.v + 1 ) ) : void;
	validateNode ( cNode.u + 1, cNode.v - 1 ) ? nArray.push( getNodeByCoords ( cNode.u + 1, cNode.v - 1 ) ) : void;
	validateNode ( cNode.u + 1, cNode.v + 1 ) ? nArray.push( getNodeByCoords ( cNode.u + 1, cNode.v + 1 ) ) : void;

	return ( nArray );
}

Those two functions are basically checking which of the eight adjacent nodes exist (there are less than four for margin nodes) and return an array that is then saved in the single node's properties.

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);
		}
	}
}

Tiles are created between four nodes. So there is one tile less than nodes in a column and the same goes for the rows.

var tileMap:Sprite = new Sprite();
addChild(tileMap);
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;

var terraform:Boolean = false;

function drawTiles()
{
	tileMap.graphics.clear();
	
	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);
		}
	}
	terraform = false;
}

Drawing the tiles was covered in Part 1 and Appendix A.
There is a new Boolean that will later on tell if the map is already updated or still drawing. Once the new map is on it is set to false.

makeTiles();
drawTiles();

Again, calling functions.

var tFormCursor:MovieClip = new MovieClip();
with( tFormCursor.graphics )
{
	lineStyle(1, 0x990000);
	beginFill(0x990000, 0.5);
	drawEllipse(-6, -4, 12, 8);
	endFill();
}
tFormCursor.visible = false;

var nFormCursor:MovieClip = new MovieClip();
with( nFormCursor.graphics )
{
	lineStyle(1, 0x009900);
	beginFill(0x009900, 0.5);
	drawEllipse(-5, -3, 10, 6);
	endFill();
}
nFormCursor.visible = false;
addChild(nFormCursor);
addChild(tFormCursor);

The cursors that were introduced in Part 2. For a more appropriate visulization in the isometric map ellipses instead of circles are used.

stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);

var actualNode:Object = new Object();

function mouseMoveHandler(event:MouseEvent)
{	
	updatePosition();
}
function updatePosition()
{
	var nodeU = Math.round (( nodeMap.mouseX / tileSizeH ) * 0.5 + ( nodeMap.mouseY / tileSizeV ) * 0.5 );
	var nodeV = Math.round ( - ( nodeMap.mouseX / tileSizeH ) * 0.5 + ( nodeMap.mouseY / tileSizeV ) * 0.5 );
	
	if ( validateNode(nodeU, nodeV) )
	{
		actualNode = getNodeByCoords ( nodeU, nodeV );
		
		tFormCursor.visible = true;
		tFormCursor.x = actualNode.xPos + nodeMap.x;
		tFormCursor.y = actualNode.yPos - actualNode.zPos + nodeMap.y;
		
		nFormCursor.visible = true;
		nFormCursor.x = actualNode.xPos + nodeMap.x;
		nFormCursor.y = actualNode.yPos + nodeMap.y;
	}
	else
	{
		actualNode = null;
		tFormCursor.visible = false;
		nFormCursor.visible = false;
	}
}

The function showing the actual position in the grid. By hiding the the mouse cursor and the nFormCursor one would only see the actual position that will be altered by click. updatePosition calculates the position in grid coordinates (or column and row). Node distances that are set at the beginning are taken into account. Instead of nodeMap.mouseX one could take just mouseX but then the map's registration point must also be considered in that calculation.

stage.addEventListener(MouseEvent.CLICK, mouseClickHandler);

function mouseClickHandler(event:MouseEvent)
{
	if ( actualNode && !terraform )
	{
		terraform = true;
		var openList:Array = new Array();
		var closedList:Array = new Array();
		openList.push(actualNode);
		while ( openList.length > 0 )
		{
			var oNode:Object = openList.shift();
			closedList.push(oNode);
			oNode.w++;
			for each ( var nNode in oNode.nArray )
			{
				if( closedList.indexOf(nNode) < 0 && openList.indexOf(nNode) < 0 )
				{
					if ( oNode.w - nNode.w > 1 )
					{
						openList.push(nNode);
					}
				}
			}
		}
		placeNodes();
		drawTiles();
		updatePosition();
	}
}

The heart of this tutorial part. When the click listener fires it checks wether there is actually a grid node under the cursor by looking for actualNode not being null. Also, if there is actually a calculation going on, means terraform would be true nothing happens. Let's assume there is an actualNode and no calculation is going on.
terraform is set true (a click won't have any effect in that moment), two arrays are created and the actualNode is pushed into an openList. That array is crucial for the calculation loop. So, while there are more than zero objects in there it loops through the following lines.
We declare a new Object named oNode and create a reference to the first element of openList. oNode is then pushed into the closedList because we pretty sure will take care about it now.

shift():* Removes the first element from an array and returns that element.

This node's (that's what this element/object is) height step is increased by one. Now, this node's adjacent neighbors are checked wether they are already in the open or closed list. If so, skip further steps. If not the height differnce is calculated and if it differs more than on step, guess what, the relevant node is pushed into the openList.
All that open and closed list checking is done to avoid an infinite loop via checking nodes that already where moved/visited. leaving that out can create interesting results but those may be not the ones we were looking for.

Now, with that done, you should be ready to implement the same for lowering nodes. What bothers me so far and needs to be clarified prior to action is the best possible control scheme. Without the right mouse button, not because I do not have one but we're in Flash and there is a pre-defined use for it(context menu), there are several possibilities. One could be having a button for 'LAND UP' and a button for 'LAND DOWN'. Hmm, what's a common techniqe in Populous? Twice up, one down for four flat tiles. Okay, I could switch here. Three up, three down, one up for 16 flat tiles. And that is no seldom action. In the name of user friendliness: no buttons to switch here please! That would be more button mashing than actual landscaping. Up next: the control scheme!

Yoho!

This entry was posted in as3, flash, game development, grids, mochiads, Terrain Modification, Tutorial and tagged . Bookmark the permalink.
Be Sociable, Share!

Leave a Reply

Your email address will not be published.