2010-11-20

Project "HoT MetaL JaZz" - Part 2

In the previous installment of this series, we added code to our project that showed us how to draw text to our canvas and manipulate it a bit. Now, we will learn more about the drawing functions within the HTML5 canvas element, including image-drawing capabilities.

Let us begin this exercise by modifying our base template for HTML5 games. Everything will be similar to the skeleton presented in Part 0 of our series, except that we will have a separate file for our game scripts.

The new skeleton code is as follows:


<head>
  <title>HoT MetaL JaZz - Rev.2</title>

  <script src="gamecore.js"></script>

  <script>
    var game;
    function startGame() {
      game = new GameCore(document.getElementById("gamecanvas"),
                          document.getElementById("backbuffer"),
                          30);
    }
  </script>

  <style type="text/css">
    body { margin: 0 auto; text-align: center; }
  </style>
</head>

<body onload="startGame();">
  <canvas id="gamecanvas" width="300" height="300"></canvas>
  <canvas id="backbuffer" width="300" height="300" style="display:none;"></canvas>
</body>


Save the code into a plain text file, with a file name ending with ".html". This simple HTML file will allow us to write as much game code as we want without touching the "front-end" HTML file, with the exception of adding new script file definition in the <head> area.

This skeleton code works with the script file "gamecore.js". To add this file, create a new, empty text file to the same folder as your HTML file, and name it "gamecore.js". We will use this new file to store the "core" scripts for our game.

We will define a GameCore class in gamecore.js. Its constructor will simply take a primary canvas, a buffer canvas, and a "desired" framerate as arguments. It will then store references to the canvas elements and do a few other basic setup tasks. Then, it will automatically call a function to initialize the game. Take the following code:


function GameCore(primaryCanvas, bufferCanvas, desiredFramerate) {
  this.primaryCanvas = primaryCanvas;
  this.primaryContext;
  this.bufferCanvas = bufferCanvas;
  this.bufferContext;

  //store the width and height of each canvas to reduce
  //the overhead involved in individual queries to canvas elements
  this.primaryCanvasWidth = primaryCanvas.width;
  this.primaryCanvasHeight = primaryCanvas.height;
  this.bufferCanvasWidth = bufferCanvas.width;
  this.bufferCanvasHeight = bufferCanvas.height;

  //To get a framerate of N frames per second, divide 1000 by N
  this.frameSpeed = 1000 / desiredFramerate;

  this.images = new Array();

  this.init();
}


As you can see, there isn't much going on here that we haven't covered. After we reference the Canvas objects and make variables to store the context for each canvas, we store the width and height of each. We have a "frameSpeed" variable, which sets a timeout between refreshes - timeouts are measured in milliseconds (1/1000th of a second), so we get our actual frames-per-second delay by dividing 1000 by our desiredFramerate. The desiredFramerate should be a reasonable value - no more than 60, but in practice, about 30. Then we create a new array to store the images we will use for this example.

At the end of the GameCore constructor, we make a call to the init() function of our GameCore object. For now, our GameCore.init() function looks like this:


GameCore.prototype.init = function() {
  if (this.primaryCanvas.getContext) {
    this.primaryContext = this.primaryCanvas.getContext("2d");
    this.primaryContext.clearRect(0,
                                  0,
                                  this.primaryCanvasWidth,
                                  this.primaryCanvasHeight);
  }

  if (this.bufferCanvas.getContext) {
    this.bufferContext = this.bufferCanvas.getContext("2d");
    this.bufferContext.clearRect(0,
                                 0,
                                 this.bufferCanvasWidth,
                                 this.bufferCanvasHeight);
  }

  this.loadData();
}


We simply set up the references to the context of our primary and buffer canvases here. We use the variables in which we stored each canvas's width and height, to avoid any unnecessary overhead from calling the canvas properties directly. Once this has been done, the GameCore.loadData() function is called. This function is defined as such:


GameCore.prototype.loadData = function() {
  var testImage = new Image();
  testImage.src = "sprite.png";
  this.images.push(testImage);

  this.draw();
}


For the purpose of our demonstration, we define the loadData() function to create a new Image object, set it to point to an image named "sprite.png", and then add the image to our GameCore.images array. Once that is done, we are ready to start the game. Our GameCore.draw() function looks like this:


GameCore.prototype.draw = function() {
  this.bufferContext.fillRect(0,
                              0,
                              this.bufferCanvasWidth,
                              this.bufferCanvasHeight);


  for (currImage = 0; currImage < this.images.length; currImage++) {
    if (this.images[currImage].complete) {
      this.bufferContext.drawImage(this.images[currImage],
                                   this.bufferCanvasWidth / 2,
                                   this.bufferCanvasHeight / 2);
    }
  }


  this.primaryContext.clearRect(0,
                                0,
                                this.primaryCanvasWidth,
                                this.primaryCanvasHeight);

  this.primaryContext.drawImage(this.bufferCanvas,
                                0,
                                0,
                                this.primaryCanvasWidth,
                                this.primaryCanvasHeight);

  setTimeout("game.draw()", this.frameSpeed);
}


This function begins by filling the backbuffer with a solid black rectangle (remember, the default fillStyle is black). We then iterate through all the images in our demo (currently, just one) to make sure they have completely loaded. If the image is finished loading, we draw the image to the center of our backbuffer using the context.drawImage(...) function. Here, we put our image as the first argument, and the X/Y coordinate at which to place the image.

Once we have iterated through all our images, we clear the primary context, and then "flip" our backbuffer onto it using the drawImage() function. This time, we call the drawImage() function with five arguments: The image, the X/Y coordinates, and the width/height of the image to draw. We then set a timeout for redrawing the canvas.

To test this out, we will need our "sprite.png" image file in our game directory. If you don't feel like making your own, here is the one I used in writing this article:



When you open the page, you should see your sprite image appear near the center of the canvas. It is off-center a bit, because the image is drawn starting from the top-left. In other words, the top-left of the image is in the center of the canvas!

Later, we will give suggestions for easily drawing images from their center, rather than their top-left corners. For now, though, let's go into detail on the drawImage() function.

The drawImage() function template looks like this:

drawImage(Object image, float dx, float dy, float dw, float dh)

Where image can be an HTML image, HTML canvas, or HTML video element. dx/dy indicate the origin of drawing (i.e. the top-left corner of where to draw). dw/dh are optional arguments, and indicate how much of the image to draw. In other words, if our image is 300x300, and only want to draw half of that image, we would call drawImage() as such:

[context].drawImage(myImage, 0, 0, 150, 150);

If dw/dh are not defined, the entire image will be drawn.

There is also an alternate way to use drawImage() that allows you to draw a specific portion of an image to a context. We will go over this method later in this series.

The next time we dive into Project "HoT MetaL JaZz", we will go over some of the advanced path functions built into the HTML5 canvas element, and also learn how to dynamically scale our game to fit the player's browser window. It will be an extremely exciting ride, so don't miss it!

No comments:

Post a Comment