When I began experimenting with the HTML5 canvas I decided that I might learn more if I actually wrote a program that took advantage of the functionality. This led me to create a clone of the classic Space Invaders game. Since I was primarily concerned with learning the canvas element, the Space Invaders clone was written as a monolithic application. Upon completing Space Invaders I wanted to create another game and further refine my abilities. However, in order to take advantage of the code that I wrote for the Space Invaders clone I would have first had to perform significant refactoring, so I decided to just start from scratch and write generalized classes that I could easily reuse.

The first class that I wrote, the one shown below, was designed to simply handle the game sprites. This class was designed to draw, undraw, perform basic moves, and display the sprite death sequence. Since I was attempting to emulate the look and feel of the Atari 2600 system I used very simple pixmaps for the sprites rather than loading images.

In this class the pixmaps are stored in three dimensional arrays. The first dimension represents time (the stage of animation), the second represents the y coordinates, and the third represents the x coordinates. Since a string is basically an array of characters the third dimension is compressed into a string. This works as long as the color palette is limited to 16 colors where each color can be represented by a single character (0-f). To use a palette with more colors an actual array would be required for the third dimension (no changes to the parser are required). Also, since the pixmap parser is only looking for numeric color codes, non-numeric characters can be used to represent transparency.

To use the class a canvas object and a context must first be created in addition to the pixmap. These must be passed to the constructor along with the sprite’s coordinates.

The code for the class is below. I’m sure that there is room for optimization, but I have moved on to creating sprites using images. I am working up, from the Atari 2600 level to the NES level. I will post the code for those classes once they are complete.

/*!
 * Sprites
 * For creating Atari 2600 style block
 * sprites using data from a matrix.
 *
 * For use with the canvas element
 * 
 * @author Christopher Stoll
 * @copyright Christopher Stoll 2011
 * @version 1.1.0
 * 2011-07-10
 */

/**
 * This is a sprite data type
 *
 * @constructor
 * @param {object} pC The context for the canvas
 * @param {number} [pX=0] The sprite's x position
 * @param {number} [pY=0] The sprite's y position
 * @param {number} [pP[][][]=[]] The sprite's pixmap
 * @param {string} [pB='black'] The sprite's background color
 */
function Sprite(pC, pX, pY, pP, pB){
 if(pC){
  this.canvasContext = pC;
  this.x = pX || 0;
  this.y = pY || 0;
  this.pixmap = pP || [];
  this.color = pB || 'black';
  
  this.height = 4; // set on first draw
  this.width = 4; // set on first draw
  this.pixelHeight = 4;
  this.pixelWidth = 4;
  
  this.stepVertical = 4;
  this.stepHorizontal = 4;
  
  this._pixmap_save = [[]];
  
  this.reset();
 }
}

 /**
  * Sprite color map (10 color)
  *
  * @private
  * @this {Sprite}
  */
 Sprite.prototype._colors_10 = {
  0: '#000000', // Black
  1: '#0000ff', // Blue
  2: '#008000', // Green
  3: '#00ff00', // Lime
  4: '#800080', // Purple
  5: '#808080', // Gray
  6: '#c0c0c0', // Silver
  7: '#ff0000', // Red
  8: '#ffff00', // Yellow
  9: '#ffffff'  // White
 };

 /**
  * Sprite death pixmap 0
  *
  * @private
  * @this {Sprite}
  */
 Sprite.prototype._pixmap_death = [
  [
   'xxxxxxxx',
   'xxxxxxxx',
   'xx8778xx',
   'xx7xx7xx',
   'xx7xx7xx',
   'xx8778xx',
   'xxxxxxxx',
   'xxxxxxxx'
  ],[
   'xxxxxxxx',
   'x88xx88x',
   'x87xx78x',
   'xxxxxxxx',
   'xxxxxxxx',
   'x87xx78x',
   'x88xx88x',
   'xxxxxxxx'
  ],[
   'x6x55x6x',
   '69xxxx96',
   'xxxxxxxx',
   '5xxxxxx5',
   '5xxxxxx5',
   'xxxxxxxx',
   '69xxxx96',
   'x6x55x6x'
  ],[
   '65xxxx56',
   '5xxxxxx5',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   '5xxxxxx5',
   '65xxxx56'
  ],[
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx',
   'xxxxxxxx'
  ]

 ];

 /**
  * Reset dynamic properties
  *
  * @this {Sprite}
  */
 Sprite.prototype.reset = function(){
  for(var h=0; h<this.pixmap.length; h++){
   for(var i=0; i<this.pixmap[h].length; i++){
    for(var j=0; j<this.pixmap[h][i].length; j++){
     //
    }
   }
  }
  this.alength = h;
  this.height = i * this.pixelHeight;
  this.width = j * this.pixelWidth;
  
  this.isDrawn = false;
  this.aindex = 0;
  
  this.animateDeath = true;
  this.leaveBody = false;
  this.deathCounter = 0;
  this.isDying = false;
  
  this.active = true;
  this.draw();
 }

 /**
  * Undraw sprite
  *
  * @private
  * @this {Sprite}
  */
 Sprite.prototype._undraw = function(){
  this.canvasContext.fillStyle = this.color;
  this.canvasContext.strokeStyle = this.color;
  this.canvasContext.fillRect(
   this.x, 
   this.y, 
   this.width, 
   this.height
  );
  
  this.isDrawn = false;
 }

 /**
  * Draw sprite
  *
  * @this {Sprite}
  * @param {number} [pX] The sprite's x position
  * @param {number} [pY] The sprite's y position
  * @param {number} [pS] The sprite's scale
  */
 Sprite.prototype.draw = function(pX, pY, pS){
  // clear
  if(this.isDrawn){
   this._undraw();
  }
  
  // move
  if(pX && pY){
   this.x = pX;
   this.y = pY;
  }
  
  // scale
  if(pS){
   this.pixelHeight = this.pixelHeight * pS;
   this.pixelWidth = this.pixelWidth * pS;
  }
  
  // draw
  var th = this.height / this.pixelHeight,
   tw = this.width / this.pixelWidth,
   tx, ty, tc;
  for(var i=0; i<th; i++){
   for(var j=0; j<tw; j++){
    // skip anything that is not a number
    if(!isNaN(+this.pixmap[this.aindex][i][j])){
     tx = this.x + (this.pixelHeight * j);
     ty = this.y + (this.pixelWidth * i);
     tc = this.pixmap[this.aindex][i][j];
     
     this.canvasContext.fillStyle = 
      this._colors_10[tc];
     this.canvasContext.strokeStyle = 
      this._colors_10[tc];
      
     this.canvasContext.fillRect(
      tx, ty, this.pixelHeight, this.pixelWidth);
    }
   }
  }
  this.isDrawn = true;
  
  // increment animation
  if(this.alength > 1){
   if((this.aindex + 1) < this.alength){
    this.aindex++;
   }else{
    this.aindex = 0;
   }
  }
 }

 /**
  * Move sprite up
  *
  * @this {Sprite}
  * @param {number} [pS] The movement step
  */
 Sprite.prototype.moveUp = function(pS){
  var tS = pS || this.stepVertical;
  this.draw(this.x, this.y - tS);
 }

 /**
  * Move sprite down
  *
  * @this {Sprite}
  * @param {number} [pS] The movement step
  */
 Sprite.prototype.moveDown = function(pS){
  var tS = pS || this.stepVertical;
  this.draw(this.x, this.y + tS);
 }

 /**
  * Move sprite left
  *
  * @this {Sprite}
  * @param {number} [pS] The movement step
  */
 Sprite.prototype.moveLeft = function(pS){
  var tS = pS || this.stepHorizontal;
  this.draw(this.x - tS, this.y);
 }

 /**
  * Move sprite right
  *
  * @this {Sprite}
  * @param {number} [pS] The movement step
  */
 Sprite.prototype.moveRight = function(pS){
  var tS = pS || this.stepHorizontal;
  this.draw(this.x + tS, this.y);
 }

 /**
  * Kill a sprite
  *
  * @this {Sprite}
  */
 Sprite.prototype.die = function(){
  // can't die twice
  if(this.active){
   // are we dramatizing the death
   if(this.animateDeath){
    // first stage of death
    if(this.deathCounter == 0){
     this.isDying = true;
     this._undraw();
     this._pixmap_save = this.pixmap;
     this.pixmap = this._pixmap_death;
     for(var h=0; h<this.pixmap.length; h++){
      for(var i=0; i<this.pixmap[h].length; i++){
       for(var j=0; j<this.pixmap[h][i].length; j++){
        //
       }
      }
     }
     this.alength = h;
     this.height = i * this.pixelHeight;
     this.width = j * this.pixelWidth;
     this.aindex = 0;
     this.draw();
    
    }else if(this.deathCounter == 1){
     this.draw();
    
    }else if(this.deathCounter == 2){
     this.draw();
    
    }else if(this.deathCounter == 3){
     this.draw();
     
    }else if(this.deathCounter == 4){
     this.draw();
     
    // last stage of death
    }else if(this.deathCounter == 5){
     this.deathCounter = 0;
     this.isDying = false;
     this.active = false;
     
     this.pixmap = this._pixmap_save;
     for(var h=0; h<this.pixmap.length; h++){
      for(var i=0; i<this.pixmap[h].length; i++){
       for(var j=0; j<this.pixmap[h][i].length; j++){
        //
       }
      }
     }
     this.alength = h;
     this.height = i * this.pixelHeight;
     this.width = j * this.pixelWidth;
     this._undraw();
     if(this.leaveBody){
      this.draw();
     }
    }
    
    this.deathCounter++;
   
   }else{
    this.active = false;
    if(!this.leaveBody){
     this._undraw();
    }
   }
  }
 }

Tags: Blogger JavaScript Programming HTML5 Code

Published: 2011-07-14