Last Good Quote: Son's are the seasoning on our lives. - Someone on Facebook

Friday, April 23

Javascript - 2D Map Library

This article is a wrap up of a series of articles in which I walk through building a simple 2d top down map library. Below are the articles that build this library:
For those of you that want the code, scroll past the explanations, it's at the bottom.

Style.css.
While testing and playing I usually will include the style in one page, for the ease of editing, but it's always a good idea to separate the style sheet from the main body of code. This allows an artist to come along and play with the look and feel of the site without screwing with your brilliantly created code.

Map.js.
All the general re-usable functions for mapping. If you ever finish coding a function and think, man I'll never have to touch this again, its so perfect and I'll use it all the time! First, slap yourself, cause we all know it's going to have a bug in it at some time. Second realize that the function in question, is prime material to go into a library of sorts. The functions in map.js are generic and can be kept from game to game as you build different things. At least that is my hope.

TileClicked()
I left tile clicked in the main body of the page because the actions you might want to take when a tile is clicked will change from game to game. Thus it is not library material.

DestReached()
This is a new function that was not discussed in other articles. Once a unit is done moving to it's destination of location, this function is called. I figured game builders might want to change this function depending on the game.

Play()
This is the heart of your game, if you want enemies to run around randomly, here is where you put that stuff. This function will call itself every few milliseconds, the more code in this function the slower your game is going to run.

ObjectsCollided()
This function is called when ever two units combine. The unit that moved last and did the collision is passed along with the id of each unit that is involved in the collision. You'll want to customize this function.


The Body Tag
The body tag calls the buildMap and addUnit function. I'd expect that you will want to change that.

Div Elements
There are two divs in the html one to hold the map and another to hold the score. You can move those around, but they must be somewhere in the page for the library to work.

Map.js Function List

BuildMap(width, height)
Builds a map of width and height. It will also add walls to all edges of the map, so units don't wonder off it. Comment out the "addBorders()" line if you don't want this.

AddBorders()
Ummm, it adds borders...nuff said

addWall(x,y)
Turns tile x,y into a wall. Sets tile's flag = -1, sets the background color

addUnit(unitID, x, y)
Adds a unit with that ID to the map at tile location x, y. Multiple units can be added. If unitID is 1000 it is given the css class "unit". Otherwise the class is "enemy"

setWalkableFlag(x, y, flag)
Sets tile x, y "flag" variable to the flag passed.

jumpUnit(unitID, tileDestX, tileDestY)
Moves a unit directly to that tile, no animation at all...unit just jumps to location.

move(unitID, dir)
Move a unit one tile in the direction specified. Valid directions are N, E, S, W. This is an animated move (one pixel at a time). This uses the moveUnit() function.

moveUnit(unitID, tileDestX, tileDestY)
Moves a unit to the tile passed in. If at anytime the unit hits a none walkable tile, the unit will move back to last position. (no route finding). Calls DestReached() when completed.

isWalkable(x,y)
Returns "Y" or "N" if pixel x, y is walkable

isWalkableTile(x,y)
Returns "Y" or "N" if tile x, y is walkable

getTop(obj)
Return the top pixel (y) of the object. Could use some optimization

getLeft(obj)
Return the left pixel (x) of the object. Could use some optimization

translateTileX(x), translateTileY(y)
return the tileX that pixel x is located within

getTileX(unitID), getTileY(unitID)
return the tile x and tile y that the unit is located on

Here be code!

Style Sheet (style.css)


.tile {
background:#BBB;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.unit {
background:green;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.enemy {
background:red;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.food {
background:black;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}



Map.js (the library)



var originX = 0;
var originY = 0;
var tileSize = 16;
var mapHeight = 0;
var mapWidth =0;
var score=0;
var playSpeed = 1000;
var numUnits = 0;
var maxNumUnits = 40;


function buildMap(width, height)
{
mapHeight = height;
mapWidth = width;

for(x=0;x<width;x++)
for(y=0;y<height;y++)
{
var d = document.createElement("DIV");
d.className="tile";
d.setAttribute("ID","tile_" + x + "_" + y);
d.style.top = originY + (y*tileSize);
d.style.left = originX + (x*tileSize);
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("flag","0");
d.setAttribute("onclick","tileClicked(" + x + "," + y + ")")
document.getElementById("divMap").appendChild(d);
}

addBorders();



}

function addBorders()
{
for(x=0;x<mapWidth;x++)
{
addWall(x, 0);
addWall(x, mapHeight-1);
}


for(y=0;y<mapHeight;y++)
{
addWall(0, y);
addWall(mapWidth-1,y);
}
}

function addWall(x,y)
{
var tile = document.getElementById("tile_" + x + "_" + y)
tile.setAttribute("flag","-1");
tile.style.background = "#777"
}


function addUnit(unitID, x,y)
{
var d = document.createElement("DIV");
var tile = document.getElementById("tile_" + x + "_" + y)
if(unitID == 1000)
{
d.className="unit";
}
else
{
d.className="enemy";
}
d.setAttribute("xVel", 0);
d.setAttribute("yVel", 0);
d.setAttribute("ID","unit_" + unitID);
d.style.top = tile.style.top;
d.style.left = tile.style.left;
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("currTileX",x);
d.setAttribute("currTileY",y);
d.setAttribute("priorTileX",x);
d.setAttribute("priorTileY",y);

setWalkableFlag(x, y, unitID);

document.getElementById("divMap").appendChild(d);
}

function setWalkableFlag(x, y, flag)
{
var tmpTile = document.getElementById("tile_" + x + "_" + y)
tmpTile.setAttribute("flag", flag);

}

function jumpUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
unit.style.left = getLeft(tile);
unit.style.top = getTop(tile);
//
//mark the current tile as UNwalkable
//
setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), "0");

//
//mark the old tile as WALKABLE
//
setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0");

unit.setAttribute("currTileX",x);
unit.setAttribute("currTileY",y);
unit.setAttribute("priorTileX",x);
unit.setAttribute("priorTileY",y);

}



function move(unitID, dir)
{
x = getTileX(unitID)
y = getTileY(unitID)

if(dir == "N")
{
y--;
}

if(dir == "E")
{
x++;
}

if(dir == "S")
{
y++;
}

if(dir == "W")
{
x--;
}


if(isWalkableTile(x, y) == "Y")
{
moveUnit(unitID, x, y);
}


}


function moveUnit(unitID, tileDestX, tileDestY)
{

var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
unit.setAttribute("priorTileX",getTileX(unitID));
unit.setAttribute("priorTileY",getTileY(unitID));

if(!tile)
{
alert("no tile found");
return;
}
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;

if(x > destX) newX--;
if(x < destX) newX++;
if(y > destY) newY--;
if(y < destY) newY++;

if(isWalkable(newX, newY) == "Y" && isWalkable(newX+tileSize-2, newY + tileSize-2) == "Y")
{
unit.style.left = newX;
unit.style.top = newY;

if(newX != destX || newY != destY)
{
setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5);
}
else
{
destReached(unitID, destX, destY);
}
}

}

function isWalkable(x,y)
{
var tile = document.getElementById("tile_" + translateTileX(x) + "_" + translateTileY(y))
if(tile.getAttribute("flag") >= 0) return "Y";


return "N";
}


function isWalkableTile(x,y)
{
var tile = document.getElementById("tile_" + x + "_" + y)
if(tile.getAttribute("flag") >= 0) return "Y";


return "N";
}

function getTop(obj)
{
if(obj)
{
return parseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
return parseInt(obj.style.left.replace("px",""));
}

return 0;
}


function translateTileX(x)
{
return Math.floor(x/tileSize - originX);
}

function translateTileY(y)
{
return Math.floor(y/tileSize - originY);
}


function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getTop(unit));
}

function getTileX(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getLeft(unit));
}

function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = Math.floor(getLeft(unit)/tileSize - originX);
y = Math.floor(getTop(unit)/tileSize - originY);

return y;
}




Basic HTML page (index.html)


<html>
<link type="text/css" href="style.css"rel="stylesheet" />
<script language=javascript src="map.js"></script>

<script language=javascript>
function tileClicked(x,y)
{
//called whenever a tile is clicked
moveUnit(1000, x, y);
}

function destReached(unitID, x, y)
{
//called when ever the unit is done moving to a spot

}


function play()
{
//once called, this will loop forever, checking the game state and other things
setTimeout("play()",playSpeed);
}


function objectsCollided(unit, unitID, enemyID)
{
//called whenever two objects collide

}

</script>

<body onload="buildMap(10,10); addUnit(1000,4,4);">

<div id="divMap"></div>

<div id="score" style="font-family:verdana;size:14px; background:green;position:absolute;top:5px;left:350px; width:100px height:15px;border:5px solid green;">
Score: 0
</div>


</body>
</html>

1 comments:

Bloogle said...

thx much for this lib, i think use it for a little game i made. when i test, i fund a little bug, if you click on a cell when the unit have no finish his traject, it cause truble to the coordonate of the unit, and if you repeat that a lot, the unit is completly lost.. i have fix this little bug by add a flag "on_move" wich block another move of the unit until the move have not yet finish. sorry for my english.. i'm french, and thank you for your time !

Post a Comment

Followers