Javascript - 2D Map Library

August 30, 2022

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 wantthe code, scroll past the explanations, it's at the bottom.

Style.css.
While testing and playing I usually will includethe style in one page, for the ease of editing, but it's always a good idea toseparate the style sheet from the main body of code. This allows an artist tocome along and play with the look and feel of the site without screwing withyour 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 touchthis 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 realizethat 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 youbuild different things. At least that is my hope.

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

DestReached()
This is a new function that was not discussed inother 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 thisfunction depending on the game.

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

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


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

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

Map.jsFunction List

BuildMap(width, height)
Builds a map of width and height. It will alsoadd walls to all edges of the map, so units don't wonder off it. Comment outthe "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 locationx, 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 theflag passed.

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

move(unitID, dir)
Move a unit one tile in thedirection 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 atanytime the unit hits a none walkable tile, the unit will move back to lastposition. (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. Coulduse some optimization

getLeft(obj)
Return the left pixel (x) of the object. Coulduse 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 islocated on

Herebe 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)
{
 returnparseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
 returnparseInt(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 donemoving 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:100pxheight:15px;border:5px solid green;">
Score: 0
</div>


</body>
</html>

 

Grow your business.
I am focused on helping start-ups in stabilizing and growing through strategy and analysis. Reach out to me today to start growing your business.
Start Now