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

Monday, April 5

Code: Simple 2D Javascript Map - Part 6

This code will create a map grid, when you click the grid the green box will move to the tile you clicked. Red boxes will "fall" from the top down at random intervals. If the green box hits a red box, the red box disappears and starts to fall again. Think of it as a catching game.

This was built to demonstrate one form of collision detection.In my mind there are two ways you can do collision detection. The first method is what most systems and algorithms perform:
  • Calculate the new position of the unit
  • Cycle through all other units to see if one of them occupies the new position
  • If no one is occupying then move the unit

The second method is to make tiles as walkable as a unit steps on the tile. Then rather then cycling through all the units, you just check the tile the unit is moving to.

  • Calculate the new position of the unit
  • Check to see if that tile is occupied
  • If no one is occupying then move the unit

The second method means that it "cost" more to move a unit. However it cost less than cycling through all units, every time there is any movement on the screen.

How did I implement? First, I had to edit the buildMap() function so that each tile has a flag for who is standing on it. So I added the following line inside my buildMap() loops:

d.setAttribute("flag","0");


I then needed a function to set and unset this flag. If you un comment out a few lines, it will change the background of the tile, so that you can see which tiles get marked as unwalkable.

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

/*
if(flag > 0)
{
tmpTile.style.background = "yellow";
}
else
{
tmpTile.style.background = "#BBB";
}
*/
}


I also need the unit to "remember" what tile they are on (currTile) and what tile they were on (priorTile). To do this, I add the following code to the addUnit() function:


d.setAttribute("currTileX",x);
d.setAttribute("currTileY",y);
d.setAttribute("priorTileX",x);
d.setAttribute("priorTileY",y);
setWalkableFlag(x, y, unitID);


Again, I needed a few "wrapper" functions. One translates a pixel point X to a tile X. To keep things clean I changed the getTile function to use the new translate function. Again, there is plenty of room to optimize.


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


Now we get to the meat and potatoes of coding. The move unit function is where all the collision detection takes place. Reading through the logic of this function reads as follows:
  1. Grab the unit object and the destination tile object
  2. If the destination tile does not exist, stop
  3. Move 1 pixel closer to the destination
  4. Get the tile located at the center of the new position. (Center of the unit)
  5. If that tile is not flagged or if it is flagged as this unit
  6. Move the unit to the new location
  7. Set the priorTile and currentTile values
  8. Flag the new current tile as walkable
  9. Flag the old prior tile as unwalkable
  10. If we still have not found our destination, repeat in 5 milliseconds
  11. If there is a collision (back at #5), call the collide function



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)
{
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++; checkX = newX + tileSize/2; checkY = newY + tileSize/2; var newTile = document.getElementById("tile_" + translateTileX(checkX) + "_" + translateTileY(checkY)) if(newTile.getAttribute("flag") == "0" || newTile.getAttribute("flag") == unitID) { unit.style.left = newX; unit.style.top = newY; unit.setAttribute("currTileX",getTileX(unitID)); unit.setAttribute("currTileY",getTileY(unitID)); if(unit.getAttribute("priorTileY") != unit.getAttribute("currTileY") || unit.getAttribute("priorTileX") != unit.getAttribute("currTileX") ) { // //mark the current tile as UNwalkable // setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), unitID); // //mark the old tile as WALKABLE // setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0"); } if(newX != destX || newY != destY) { setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5); } } else { // //we collided //snap back to last tile // objectsCollided(unit, unitID, newTile.getAttribute("flag")); } }


In our game, if the user/player manages to collide with an enemy we will pop the enemy back up to the top for them to start over.



function objectsCollided(unit, unitID, enemyID)
{
var tmpTile = document.getElementById("tile_" + unit.getAttribute("priorTileX") + "_" + unit.getAttribute("priorTileY"))
unit.style.top = getTop(tmpTile);
unit.style.left = getLeft(tmpTile);

if(unitID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(enemyID, x, y);
}

if(enemyID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(unitID, x, y);
}

}



One last change, I had to make was in the jumpUnit() function. I found that I needed to flag and unflag the tiles as the unit jumped.

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

}


And that's the entire change I made...the following is the entire code set:

<html>
<style>
.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
}
</style>
<script language=javascript>

var originX = 0;
var originY = 0;
var tileSize = 32;
var mapHeight = 0;
var mapWidth =0;
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);
}
}

function tileClicked(x,y)
{
//var tile = document.getElementById("tile_" + x + "_" + y)
//tile.style.background = "yellow"

moveUnit(1000, x, y);
}

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

/*
if(flag > 0)
{
tmpTile.style.background = "yellow";
}
else
{
tmpTile.style.background = "#BBB";
}
*/
}

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 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)
{
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(newX < 0) newX = 0;
//if(newX > mapWidth-1) newX = mapWidth-1;
//if(newY < 0) newY = 0;
//if(newY > mapHeight-1) newY = mapHeight-1;

checkX = newX + tileSize/2;
checkY = newY + tileSize/2;

var newTile = document.getElementById("tile_" + translateTileX(checkX) + "_" + translateTileY(checkY))
if(newTile.getAttribute("flag") == "0" || newTile.getAttribute("flag") == unitID)
{
unit.style.left = newX;
unit.style.top = newY;

unit.setAttribute("currTileX",getTileX(unitID));
unit.setAttribute("currTileY",getTileY(unitID));

if(unit.getAttribute("priorTileY") != unit.getAttribute("currTileY") || unit.getAttribute("priorTileX") != unit.getAttribute("currTileX") )
{
//
//mark the current tile as UNwalkable
//
setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), unitID);

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

if(newX != destX || newY != destY)
{
setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5);
}
}
else
{
//
//we collided
//snap back to last tile
//
objectsCollided(unit, unitID, newTile.getAttribute("flag"));


}



}


function objectsCollided(unit, unitID, enemyID)
{
var tmpTile = document.getElementById("tile_" + unit.getAttribute("priorTileX") + "_" + unit.getAttribute("priorTileY"))
unit.style.top = getTop(tmpTile);
unit.style.left = getLeft(tmpTile);

if(unitID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(enemyID, x, y);
}

if(enemyID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(unitID, x, y);
}

}


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


var playSpeed = 1000;
var numUnits = 0;
var maxNumUnits = 5;
function play()
{
if(numUnits < maxNumUnits)
{
unitID = 1001+numUnits
x = Math.floor(Math.random()*9);
speed = Math.floor(Math.random()*3);
addUnit(unitID,x,0);
var unit = document.getElementById("unit_" + unitID);
unit.setAttribute("xVel", 0);
unit.setAttribute("yVel", speed+1);
numUnits++;
}

for(i=0;i<numUnits;i++)
{
unitID = 1001+i;
var unit = document.getElementById("unit_" + unitID);
x = getTileX(unitID) + parseInt(unit.getAttribute("xVel"));
y = getTileY(unitID) + parseInt(unit.getAttribute("yVel"));

if(y >= mapHeight)
{
x = Math.floor(Math.random()*9);
y=0;
jumpUnit(unitID, x, y);
}
moveUnit(1001+i, x,y);
}

setTimeout("play()",playSpeed);
}


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

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

</body>
</html>


Question: How would you go about adding a score indicator?

0 comments:

Post a Comment

Followers