Web xmljungle.com
 

Overview of javascript game source code

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 support

Writing 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 Movements

Both if statements below involve identifying the browser that supports or requires the logic that follows. This code makes sure that the latest mouse position is available in the global _nMouseX and _nMouseY variables.

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 positions

There 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 Setup

The 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 dynamics

The next functions support the dynamics of object movement. The first 2 are regular bounds checking of x and y positions within the game area limits. The GetNumBetween function is just a safe method for returning a number between a maximum and minimum when parsing the game layout. IsCollision tests whether 2 object rectanges intersect. GetDirection8 determines one of eight directions from a two dimensional trajectory, where 0 is right, 1 is right-up, 2 is up, etc.

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 list

This part demonstrates threaded loading of the object images. The aImgList variable holds the array of images, and the progress bar is updated as they are loaded. We loop through the images listed in aSrc, creating a new Image for each with an onload handler called onObjImageEvent. Once all the images have been loaded, the handler calls updateObjs to kick off the game.

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 Management

The 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 Core

Now 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 nCount, nMaxSpeed, and nAttract settings, the first value in each pair is for _oFly and the second is for _oBee. The busiest level has 20 of each. Usually they are all attracted to the swiping object, but in the last level the flies are actually repelled when within a certain range.

NamenCountnMaxSpeednAttract
Busy Swamp,10,10,5,5,4,4;
Arid Swoop,10,10,5,7,4,4;
Late Flurry,15,15,7,7,4,4;
Easy Scoop,15,15,5,5,3,3;
Valley Swarm,20,20,4,4,4,4;
Busy Flyby,10,10,10,10,4,4;
Marsh Frenzy,10,10,7,7,2,2;
Pungent Fog,10,10,7,7,4,3;
Forest Rain,12,20,8,5,1,1;
Pollen Pie,20,10,15,8,-5,4;

Displaying Score

The score consists of several metrics tracked in the _oScore object. The displayScore function updates the values in the HTML using document.getElementById to access an HTML element by its id.

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 Levels

The prepareLevel routine updates all of the objects to the next level or if there are no more levels it calls setGameOver which appends the score to the page. The global _oFly and _oBee variables contain settings for the object counts and behaviors.

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 Game

When the user clicks the start button, startObjs is called. Depending on the _nGameState, this will start, pause or resume the game. To start the game, it clears the objects, the score, and parses the levels in the game layout. It determines the maximum number of objects needed and creates them.

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 state

The initObj function hides the object and moves it to a random position on the border of the play area. The adjustObj function changes the motion by the delta and sets the image based on the direction of movement.

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

Main

During play, the updateObjs function calls setTimeout('updateObjs()', 50) to call itself again after 50 milliseconds. This is where all the action is. It loops through all the objects. The swiping object it moves to the latest mouse position. For all the other objects in a regular visible state it checks for a collision with the swiping object in which case it starts an explosion sequence by setting the object state _aObjs[i].sta = 2. If there is no collision it adjusts their motion according to the level settings and moves them.

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

HTML

The HTML is part of the game implementation too. Some of the key parts are outlined below.

Initial Kick-off

That would be the onLoad attribute in the BODY element

<BODY onLoad="loadObjImages()">

Score

Here is an example of an element which is set by the displayScore function.

<P id="FliesInARow">0</P> <!-- etc -->

Progress Bar

The 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;">&nbsp;</TD></TR>
</TABLE>

Start Button and Game Layout

This is an HTML form. The button uses the onclick attribute.

<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 Objects

Some 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>

 
 
Copyright 2007 Ben Bryant, First Objective Software, Inc.