|
Overview of javascript game source code April 20, 2007 If you look at the source code of the javascript game page (yes, just View Source from your browser), you'll see the entire program. Here is a discussion of that source code. Cross-browser supportWriting javascript is primarily an exercise in finding ways to do things that will work across different browsers. A lot of this program is the result of experimenting, and I wish I had kept more complete notes about all the code that was attempted before settling on the final version. At the top of the program there are several functions that support generic capabilities across different browsers. This program was debugged in Internet Explorer 6, FireFox 1.5 and Opera 9. Capturing Mouse MovementsBoth var _nMouseX = 0;
var _nMouseY = 0;
function _getMousePosition(e)
{
if (e && e.pageX)
{
_nMouseX = e.pageX;
_nMouseY = e.pageY;
}
else
{
_nMouseX = event.clientX + document.body.scrollLeft;
_nMouseY = event.clientY + document.body.scrollTop;
}
}
if (document.layers)
document.captureEvents(Event.MOUSEMOVE);
document.onmousemove = _getMousePosition;
Object positionsThere are a lot of variations in how image object or layer positions are supported in different browsers, as well as whether properties are strings or integers. function _moveTo(o,x,y) { o.style.left = x+"px"; o.style.top = y+"px"; }
function _moveBy(o,dx,dy)
{
o.style.left = (parseInt(o.style.left)+dx)+"px";
o.style.top = (parseInt(o.style.top)+dy)+"px";
}
function _show(o) { o.style.visibility = "visible"; }
function _hide(o) { o.style.visibility = "hidden"; }
function _getH(o)
{
if (o.style.pixelHeight) return o.style.pixelHeight;
else if (o.clientHeight) return parseInt(o.clientHeight)
return parseInt(o.style.height)
}
function _getW(o)
{
if (o.style.pixelWidth) return o.style.pixelWidth;
else if (o.clientWidth) return parseInt(o.clientWidth)
return parseInt(o.style.width)
}
function _getL(o) { return parseInt(o.style.left); }
function _getT(o) { return parseInt(o.style.top); }
function _getR(o) { return(parseInt(o.style.left) + _getW(o)); }
function _getB(o) { return(parseInt(o.style.top) + _getH(o)); }
Animation SetupThe animation involves creating a bunch of image objects (or "layers" on some browsers), each with its own state so that it can be moved around accordingly. Object dynamicsThe next functions support the dynamics of object movement. The first 2 are regular bounds checking of function getInX(o,oLim,x)
{
if ( x > _getR(oLim)-_getW(o)) x = _getR(oLim)-_getW(o);
if ( x < _getL(oLim) ) x = _getL(oLim);
return x;
}
function getInY(o,oLim,y)
{
if ( y > _getB(oLim)-_getH(o)) y = _getB(oLim)-_getH(o);
if ( y < _getT(oLim) ) y = _getT(oLim);
return y;
}
function getNumBetween(n,a,b)
{
var r=parseInt(n);
if (r<a) r=a;
else if (r>b) r=b;
return r;
}
function isCollision(o1,o2,g)
{
return _getT(o1)+g<_getB(o2)&&_getT(o2)+g<_getB(o1)
&&_getL(o1)+g<_getR(o2)&&_getL(o2)+g<_getR(o1);
}
function getDirection8(dx,dy)
{
var n = (Math.atan2(-dy, dx) * 180 / Math.PI) + 22.5;
if (n < 0) n += 360;
return Math.floor( n / 45 );
}
Image listThis part demonstrates threaded loading of the object images. The var aImgList = new Array();
var _nTotImgs = 0;
var _nImg = 0;
function setProgressBar(n,t)
{
var p = Math.floor(n*100/t);
if (p<1) p=1;
if (p>100) p=100;
document.getElementById('ProgressBar').style.width = p+"%";
}
function setStatusTitle(m)
{
document.getElementById('StatusTitle').firstChild.data = m;
}
function loadObjImages()
{
if ( document.getElementById )
{
setStatusTitle( "Loading..." );
var aSrc = new Array( "main",
"bee0","bee1","bee2","bee3","bee4","bee5",
"bee6","bee7","bex0","bex1","bex2",
"fly0","fly1","fly2","fly3","fly4","fly5",
"fly6","fly7","fex0","fex1","fex2" );
var i;
_nTotImgs = aSrc.length;
for (i = 0; i < _nTotImgs; i++)
{
aImgList[i] = new Image();
aImgList[i].onabort = onObjImageEvent;
aImgList[i].onerror = onObjImageEvent;
aImgList[i].onload = onObjImageEvent;
}
for (i = 0; i < _nTotImgs; i++)
aImgList[i].src = aSrc[i] + ".gif";
}
else
{
var oText = document.createTextNode(
'Requires Internet Explorer 5.0 or Netscape 6.0 compatible');
var oGameObjs = document.getElementById('GameObjs');
oGameObjs.appendChild(oText);
oGameObjs.setAttribute('align','center');
}
}
function onObjImageEvent()
{
_nImg++;
setProgressBar(_nImg,_nTotImgs);
if (_nImg==_nTotImgs)
{
setStatusTitle("Ready");
updateObjs();
}
}
Object ManagementThe animation objects are different than the images. At the beginning of each game, enough objects are created to support the maximum number of animated objects during the game. Extras will be hidden while they are unused. All of the global object variables and game variables are declared here. var _nObjCount = 0;
var _aObjs = new Array;
var _oBee = new Object;
var _oFly = new Object;
var _oBorder;
var _nGameState = 0;
var _aLevels;
var _nLevel;
var _nTicksThisLevel;
var _nTicksPerLevel = 300;
var _nBeesAccidentsAllowed = 10;
var _oScore = new Object;
function addObjs(n,t)
{
var oGameObjs = document.getElementById('GameObjs');
var i;
for (i = 0; i < n; i++)
{
_aObjs[_nObjCount] = new Object();
_aObjs[_nObjCount].typ = t;
_aObjs[_nObjCount].sta = 0;
_aObjs[_nObjCount].dx = 0;
_aObjs[_nObjCount].dy = 0;
var oInsDiv = document.createElement('div');
oInsDiv.setAttribute('id','movobj'+_nObjCount);
oGameObjs.appendChild(oInsDiv);
var oNewDiv = document.getElementById('movobj'+_nObjCount);
oNewDiv.style.position = 'absolute';
_hide(oNewDiv);
oGameObjs.appendChild(oNewDiv);
var oInsImg = document.createElement('img');
oInsImg.setAttribute('id','movobjimg'+_nObjCount);
document.getElementById('movobj'+_nObjCount).appendChild(oInsImg);
++_nObjCount;
}
}
function clearObjs()
{
for (i = 0; i < _nObjCount; i++)
{
_aObjs[i].o.image.src = "clear.gif"
_aObjs[i].o.parentNode.removeChild(_aObjs[i].o);
_aObjs[i].o = null;
}
_nObjCount = 0;
}
Game CoreNow we get into the stuff that is more specific to this particular game. The game layout is specified in a comma delimited format in the large edit box on the screen. It can be changed, but the default has 10 levels. Each level has a name and 3 number pairs for
Displaying ScoreThe score consists of several metrics tracked in the function displayScore()
{
document.getElementById('FliesInARow').firstChild.data
= _oScore.nFliesInARow;
document.getElementById('PointFactor').firstChild.data
= _oScore.nPointFactor;
document.getElementById('TopFactor').firstChild.data
= _oScore.nMaxPointFactor;
document.getElementById('Score').firstChild.data
= _oScore.nScore;
document.getElementById('BeeAccidents').firstChild.data
= _oScore.nBeeAccidents;
}
Preparing LevelsThe function prepareLevel()
{
if ( _nLevel < _aLevels.length )
{
var aLevelSettings = _aLevels[_nLevel].split(',');
setStatusTitle( aLevelSettings[0] + ' '
+ (_nLevel+1) + '/' + _aLevels.length );
_oFly.nCount = getNumBetween(aLevelSettings[1],1,20);
_oBee.nCount = getNumBetween(aLevelSettings[2],1,20);
_oFly.nMaxSpeed = getNumBetween(aLevelSettings[3],1,15);
_oBee.nMaxSpeed = getNumBetween(aLevelSettings[4],1,15);
_oFly.nAttract = getNumBetween(aLevelSettings[5],-30,30);
_oBee.nAttract = getNumBetween(aLevelSettings[6],-30,30);
_oFly.nFactor = 1;
if ( _oFly.nAttract < 0 )
{
_oFly.nAttract = - _oFly.nAttract;
_oFly.nFactor = -1;
}
if ( _oFly.nAttract == 0 )
_oFly.nAttract = 1;
if ( _oFly.nMaxSpeed > 8 )
_oFly.nFactor *= 2;
_oBee.nFactor = 1;
if ( _oBee.nAttract < 0 )
{
_oBee.nAttract = - _oBee.nAttract;
_oBee.nFactor = -1;
}
if ( _oBee.nAttract == 0 )
_oBee.nAttract = 1;
if ( _oBee.nMaxSpeed > 8 )
_oBee.nFactor *= 2;
var i;
var iFirst = 1;
for (i = iFirst+_oFly.nPrevCount; i < iFirst+_oFly.nCount; i++)
{
_aObjs[i].sta = 1;
initObj(i);
_show(_aObjs[i].o);
}
for (i = iFirst+_oFly.nCount; i < iFirst+_oFly.nPrevCount; i++)
{
_aObjs[i].sta = 0;
_hide(_aObjs[i].o);
_moveTo(_aObjs[i].o,0,0);
}
iFirst += _oFly.nMaxCount;
for (i = iFirst+_oBee.nPrevCount; i < iFirst+_oBee.nCount; i++)
{
_aObjs[i].sta = 1;
initObj(i);
_show(_aObjs[i].o);
}
for (i = iFirst+_oBee.nCount; i < iFirst+_oBee.nPrevCount; i++)
{
_aObjs[i].sta = 0;
_hide(_aObjs[i].o);
_moveTo(_aObjs[i].o,0,0);
}
_oFly.nPrevCount = _oFly.nCount;
_oBee.nPrevCount = _oBee.nCount;
}
else
{
setGameOver();
setStatusTitle( 'Complete' );
}
}
function setGameOver()
{
_nGameState = 0;
var d = new Date();
var oScore = document.createElement('b');
oScore.appendChild(document.createTextNode(_oScore.nScore));
var oGameObjs = document.getElementById('GameScores');
oGameObjs.appendChild(document.createElement('br'));
oGameObjs.appendChild(oScore);
oGameObjs.appendChild(document.createTextNode( ' ' + d ));
document.getElementById('StartButton').value = 'Start';
}
Start GameWhen the user clicks the start button, function startObjs()
{
if ( document.getElementById )
{
if ( _nGameState == 1 )
{
_nGameState = 2;
document.getElementById('StartButton').value = 'Continue';
}
else if ( _nGameState == 2 )
{
_nGameState = 1;
document.getElementById('StartButton').value = 'Pause';
}
else
{
document.getElementById('StartButton').value = 'Pause';
clearObjs();
_oScore.nFliesInARow = 0;
_oScore.nPointFactor = 0.0;
_oScore.nMaxPointFactor = 0;
_oScore.nScore = 0;
_oScore.nBeeAccidents = 0;
displayScore();
if ( document.forms[0].LV.value == "" )
{
document.forms[0].LV.value = "Busy Swamp,10,10,5,5,4,4;\n"
+ "Arid Swoop,10,10,5,7,4,4;\nLate Flurry,15,15,7,7,4,4;\n"
+ "Easy Scoop,15,15,5,5,3,3;\nValley Swarm,20,20,4,4,4,4;\n"
+ "Busy Flyby,10,10,10,10,4,4;\nMarsh Frenzy,10,10,7,7,2,2;\n"
+ "Pungent Fog,10,10,7,7,4,3;\nForest Rain,12,20,8,5,1,1;\n"
+ "Pollen Pie,20,10,15,8,-5,4";
}
var sLevels = document.forms[0].LV.value;
sLevels = sLevels.replace(/^\n+/g, '');
_aLevels = sLevels.split(';');
_oFly.nMaxCount = 0;
_oBee.nMaxCount = 0;
_oFly.nPrevCount = 0;
_oBee.nPrevCount = 0;
for (_nLevel = 0; _nLevel < _aLevels.length; ++_nLevel )
{
var aLevelSettings = _aLevels[_nLevel].split(',');
var nFlyCount = getNumBetween(aLevelSettings[1],1,20);
var nBeeCount = getNumBetween(aLevelSettings[2],1,20);
if ( nFlyCount > _oFly.nMaxCount )
_oFly.nMaxCount = nFlyCount;
if ( nBeeCount > _oBee.nMaxCount )
_oBee.nMaxCount = nBeeCount;
}
addObjs(1,0);
addObjs(_oFly.nMaxCount,1);
addObjs(_oBee.nMaxCount,2);
var i;
for (i = 0; i < _nObjCount; i++)
{
_aObjs[i].o = document.getElementById("movobj" + i);
_aObjs[i].o.image = document.getElementById("movobjimg" + i);
}
_oBorder = document.getElementById("GameBorder");
_nMouseX = _getW(_oBorder) / 2 + _getL(_oBorder);
_nMouseY = _getH(_oBorder) / 2 + _getT(_oBorder);
_aObjs[0].o.image.src = "main.gif";
_moveTo(_aObjs[0].o,_nMouseX-(_getW(_aObjs[0].o)/2),
_nMouseY-(_getH(_aObjs[0].o)/2));
_show(_aObjs[0].o);
_nLevel = 0;
_nTicksThisLevel = 0;
prepareLevel();
_nGameState = 1;
}
}
}
Object stateThe function initObj(i)
{
var s, x, y;
x = Math.floor(Math.random() * _getW(_oBorder)) + _getL(_oBorder);
y = Math.floor(Math.random() * _getH(_oBorder)) + _getT(_oBorder);
s = Math.floor(Math.random() * 4);
if (s == 0) x = _getL(_oBorder);
else if (s == 1) x = _getR(_oBorder);
else if (s == 2) y = _getT(_oBorder);
else if (s == 3) y = _getB(_oBorder);
_aObjs[i].o.image.src = "clear.gif";
x = getInX(_aObjs[i].o,_oBorder,x);
y = getInY(_aObjs[i].o,_oBorder,y);
_moveTo(_aObjs[i].o, x, y);
_aObjs[i].dx = 0;
_aObjs[i].dy = 0;
}
function adjustObj(i,ddx,ddy)
{
var m = ( _aObjs[i].typ == 1 )? _oFly.nMaxSpeed : _oBee.nMaxSpeed;
if ( ddx > 0 )
{
_aObjs[i].dx += ddx;
if ( _aObjs[i].dx > m )
_aObjs[i].dx = m;
}
if ( ddx < 0 )
{
_aObjs[i].dx += ddx;
if ( _aObjs[i].dx < -m )
_aObjs[i].dx = -m;
}
if ( ddy > 0 )
{
_aObjs[i].dy += ddy;
if ( _aObjs[i].dy > m )
_aObjs[i].dy = m;
}
if ( ddy < 0 )
{
_aObjs[i].dy += ddy;
if ( _aObjs[i].dy < -m )
_aObjs[i].dy = -m;
}
_moveBy(_aObjs[i].o,_aObjs[i].dx,_aObjs[i].dy);
if ( _aObjs[i].typ == 1 )
_aObjs[i].o.image.src = "fly"
+ getDirection8(_aObjs[i].dx,_aObjs[i].dy) + ".gif";
else
_aObjs[i].o.image.src = "bee"
+ getDirection8(_aObjs[i].dx,_aObjs[i].dy) + ".gif";
}
MainDuring play, the updateObjs function calls function updateObjs()
{
if ( _nGameState == 1 )
{
var i, dx, dy, theta, d;
var x = _nMouseX - (_getW(_aObjs[0].o)/2);
var y = _nMouseY - (_getH(_aObjs[0].o)/2);
x = getInX(_aObjs[0].o,_oBorder,x);
y = getInY(_aObjs[0].o,_oBorder,y);
for (i = 0; i < _nObjCount; i++)
{
if ( _aObjs[i].typ == 0 )
{
_moveTo( _aObjs[i].o, x, y );
}
else
{
if ( _aObjs[i].sta == 1 )
{
if ( isCollision(_aObjs[0].o,_aObjs[i].o,2) )
{
_aObjs[i].sta = 2;
_aObjs[i].o.image.src = "clear.gif";
_moveBy(_aObjs[i].o,_aObjs[i].dx-40,_aObjs[i].dy-40);
if ( _aObjs[i].typ == 1 )
{
_aObjs[i].o.image.src = "fex0.gif";
_oScore.nFliesInARow += 1;
_oScore.nPointFactor += 1.0;
if ( _oScore.nPointFactor > _oScore.nMaxPointFactor )
_oScore.nMaxPointFactor = _oScore.nPointFactor;
_oScore.nScore += Math.round(_oScore.nPointFactor);
}
else
{
_aObjs[i].o.image.src = "bex0.gif";
if ( _oScore.nScore < 0 )
_oScore.nScore = 0;
_oScore.nBeeAccidents += 1;
if ( _oScore.nBeeAccidents < _nBeesAccidentsAllowed )
{
_oScore.nFliesInARow = 0;
_oScore.nPointFactor -= 10.0;
}
}
}
else
{
var nAttract = (_aObjs[i].typ == 1)? _oFly.nAttract : _oBee.nAttract;
var nFactor = (_aObjs[i].typ == 1)? _oFly.nFactor : _oBee.nFactor;
if ( nFactor < 0 && ! isCollision(_aObjs[i].o,_aObjs[0].o,-60) )
nFactor = -nFactor;
var r = Math.floor(Math.random() * (4+nAttract));
if ( r < nAttract )
{
var n = getDirection8( x - _getL(_aObjs[i].o), y - _getT(_aObjs[i].o) );
if ( n == 0 ) adjustObj(i, 1*nFactor, 0);
else if ( n == 1 ) adjustObj(i, 1*nFactor, -1*nFactor);
else if ( n == 2 ) adjustObj(i, 0, -1*nFactor);
else if ( n == 3 ) adjustObj(i, -1*nFactor, -1*nFactor);
else if ( n == 4 ) adjustObj(i, -1*nFactor, 0);
else if ( n == 5 ) adjustObj(i, -1*nFactor, 1*nFactor);
else if ( n == 6 ) adjustObj(i, 0, 1*nFactor);
else adjustObj(i, 1*nFactor, 1*nFactor);
}
else if ( r == nAttract ) adjustObj(i, 0, -1);
else if ( r == nAttract+1 ) adjustObj(i, 0, 1);
else if ( r == nAttract+2 ) adjustObj(i, 1, 0);
else adjustObj(i, -1, 0);
}
}
else if ( _aObjs[i].sta == 4 )
{
_aObjs[i].sta = 1;
initObj(i);
}
else if ( _aObjs[i].sta )
{
++ _aObjs[i].sta;
_moveBy(_aObjs[i].o,_aObjs[i].dx,_aObjs[i].dy);
if ( _aObjs[i].typ == 1 )
_aObjs[i].o.image.src = "fex" + (_aObjs[i].sta-2) + ".gif";
else
_aObjs[i].o.image.src = "bex" + (_aObjs[i].sta-2) + ".gif";
}
}
}
_oScore.nPointFactor = Math.round((_oScore.nPointFactor - 0.1) * 10) / 10;
if ( _oScore.nPointFactor < 0 )
_oScore.nPointFactor = 0;
++_nTicksThisLevel;
setProgressBar(_nTicksThisLevel,_nTicksPerLevel);
if ( _oScore.nBeeAccidents >= _nBeesAccidentsAllowed )
{
setGameOver();
}
else if ( _nTicksThisLevel == _nTicksPerLevel )
{
++_nLevel;
_nTicksThisLevel = 0;
if ( _nLevel < _aLevels.length )
_oScore.nPointFactor = 0;
prepareLevel();
}
displayScore();
}
setTimeout('updateObjs()', 50);
}
HTMLThe HTML is part of the game implementation too. Some of the key parts are outlined below. Initial Kick-offThat would be the <BODY onLoad="loadObjImages()">
ScoreHere is an example of an element which is set by the <P id="FliesInARow">0</P> <!-- etc --> Progress BarThe progress bar was a bit tricky to find a dynamix HTML property that would work nicely in the different browsers with their differences in bits and margins etc. <TABLE id="ProgressBar" cellpadding=0 cellspacing=0 width=1% height=20 bgcolor=#808080 style="border-width:1px;border-style:solid;border-color:#f0f0f0"> <TR><TD style="font-size:2pt;"> </TD></TR> </TABLE> Start Button and Game LayoutThis is an HTML form. The button uses the <form> <P align=center> <input id="StartButton" type='button' value='Start' onclick='startObjs()'> </P> <P align=center> <textarea name=LV COLS=24 ROWS=2 maxlength=200></textarea> </P> </form> Game Border and ObjectsSome of the experimentation was with whether to put the game objects inside the play area so that their coordinates would be relative to it, or after it so they would simply appear above it (rather than behind it, i.e. covered by it). In the end they had to be after it because the relative coordinates worked differently across the browsers. <TABLE id="GameBorder" style='position:absolute;left:28;top:105;width:470;height:360' cellpadding=0 bgcolor='#d0d0ff'> <TR><TD width=100% valign=top></TD></TR> </TABLE> <P id="GameObjs"> </P> </BODY> |