Wednesday, June 6, 2012

Part I Drawing Sprites to an HTML5 Canvas



In my days of old I started off as an assembly language and machine code programmer. I spent my days looking at hardware register values and pointers to jump tables in memory. I had worked at Activision video games, which back then sported a benign meaningless name, Mediagenics Activision. I worked there with another engineer on reverse engineering a SEGA Genesis. Why you ask? We were looking for a cheaper means for developers to hack into the SEGA. The development systems were about $50K a pop back then and we were on a quest to hardware hack a cheaper development system. As we messed with SEGA Genesis hardware and machine code we would sometimes joke that one day in the future we would be scripting games and not machine coding them. Nonsense! We would chuckle at the mere possibility of such a crazy notion. That was an adorable fairy tale. Well, I guess the future is now. Here we are -scripting video games.

The SNES, SEGA Genesis, and the Nintendo N64 are in my blood as a game hacking hobbyist. I love back engineering games. Sure we all have a copy of MAME and cough as we lie about owning the actual roms. I own MAME for educational and hobby purposes. I am the guy that can't leave the toaster alone and has to disassemble it to see how it works. And then rebuild it with improvements. Video games to me are no different. In writing this I could careless if you wrote a complete game engine. I have a life and I am not going to sit down and look through your 10,000 lines of spaghetti code to realize I should have wrote mine from scratch in the first place. What a headache! This blog is all about sitting in front of your fireplace, with your favorite hacking platform of choice, throwing on your favorite smoking jacket and banging out some HTML 5 javascript code.

Today I am going to focus my attention on that mid-90's master piece, the coin op video game, Aliens vs. Predator! This ultimate arcade aliens versus Colonial Marines smack down game was released by Capcom. Sure you could play as a predator too, but the character I loved to throw a beating to the aliens with was Lieutenant Linn Kurosawa. Back when this game was out as a coin op I would shovel a few quarters into the machine at the Milpitas Pizza Depot as I waited for my pizza to cook. Lt. Linn was my favorite character because she was the one player wielding a katana and wielding it to take out alien hives. I remember making an early attempt at writing this game in C on my first gen Intel Pentium powered machine. But I wasn't much of a computer artist so my initial game had frustrating results in the sprite department. Now with MAME, screen grab tools, online sprite sheets, and that wonderful expensive application -Photoshop. It's back to hacking Capcom's coin op Alien vs. Predator.

The code demonstration here is all going to be straight forward. I get sick at looking at tutorials and you have to unravel the clever code the grad student used. This code is all for edification to help you learn HTML 5's Canvas and the javascript methods used to access it.

In this tutorial we're going to load an offscreen bitmap, or background onto a static screen. Don't worry, in the next tutorial we're going to scroll the background bitmap. Just like they do in the arcade games. For this example, I created a background from the second round of level 1 in the Aliens vs. Predator game. I did multiple screen grabs and pasted together a full background for us to use. You must download the art on this site to a local directory on your development machine. For right now all the work we're doing is off your local development machine. For this tutorial all the assets for the game will be in a local directory: img.

Download the background here:  Level_1_warehouse.png
Our background image is 977 pixels wide x 478 pixels high.











A Quick Blurb on Game Loops

Whenever we write games we need a main loop. That is a function that loops forever until the game is requested to stop by the user. Game loops have come a long way since the 90's. The days of old we simply wrote some code which looked like,

while(!done) {
   updateGame();
}

That was the beauty of machine code and raw C on DOS based PCs and game consoles of days of old. Not anymore! Now most applications are all window driven to some extent. A browser is a windowed application running on a windowed  operating system such as, OS X or Windows or whatever. These environments listen for events and we need an event listener. After many wrong ways of coding my game loop I decided to just go shopping with the Google search engine and find an event handler that would work with HTML 5. I felt I would mention this fact and not just dump code on you that any grad student would surmise to be intuitively obvious to the casual observer. I am using an animate frame listener. I found one such gem on Mr. Doob's website, which also functions when the window is not active and stops the graphics animation. A plus for mobile devices applications. Here is the basic code and HTML syntax for the start of this project.

Source Listing 1. Basic HTML with requestAnimationFrame callback
<html>
<head>
</head>
<body>

<script>
// requestAnim shim layer by Paul Irish
    window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       || 
              window.webkitRequestAnimationFrame || 
              window.mozRequestAnimationFrame    || 
              window.oRequestAnimationFrame      || 
              window.msRequestAnimationFrame     || 
              function(/* function */ callback, /* DOMElement */ element){
                window.setTimeout(callback, 1000 / 60);
              };
    })();
  

// example code from mr doob : http://mrdoob.com/lab/javascript/requestanimationframe/

</script>
</body>
</html>

That's it for our basic requirements to implement our game loop. Let's move on and setup the HTML 5 canvas.

HTML 5 Canvas and Context

The first thing we need to do is setup a simple HTML file for our javascript to reside in. You'll notice off the bat I don't have CSS in the source. It's not because I can't do CSS in my sleep it's because it's a distraction to those blokes trying to learn HTML 5 and javascript. When the time is right we'll add the CSS. Right now, just get comfortable with what is going on and what we're doing.

In side our HTML script element tag we're going to add the following code.

Source Listing 2. Global variables and initializing the game console viewport
// global variables
var gc_canvas;                  // 'gc' 'game console'
var gc_context;                 // context
var width = 592, height = 448;  // game console viewport width and height
var background;                 // image asset background
var bgwidth, bgheight;          // background width and height
var bgx, bgy;                   // background source x and y used for blitting

function initGameConsoleCanvas() {
    
    gc_canvas = document.createElement( 'canvas' );
    gc_context = gc_canvas.getContext( '2d' );
    gc_canvas.width = width;
    gc_canvas.height = height;
    document.body.appendChild( gc_canvas );
}
// END initGameConsole

 We have global variables for the browser <canvas> element, labelled gc_canvas, for game console canvas. And we have canvas drawing context variable, gc_context. The canvas element is the surface we draw to. The context is the javascript API we which we draw to the canvas with. These are two distinct components here. The canvas is part of the HTML 5 component, and the context is the mechanism (API) in javascript which we draw to the canvas element with.
In the code, we are creating a game console that is 592 pixels wide by 448 pixels wide. Why these dimensions? They were determined by analyzing the size of the screen grabs from the MAME application. The arcade resolution is 592 x 448 pixels. If you would like to create a SNES console, change the dimensions to 512 x 478, and so forth.  The remaining global variables are for loading and blitting the background bitmap.

Clear the Game Console Viewport

We're going to need a simple screen refresh that clears the screen. Basically, we're going to do a simple color fill. I use a sky blue, just so I know something is happening when I am debugging the code.

Source Listing 3. Clearing the Game Console Screen
function clearGameConsole(){
  gc_context.fillStyle = '#d0e7f9';
  gc_context.clearRect(0, 0, width, height);
  gc_context.beginPath();
  gc_context.rect(0, 0, width, height);
  gc_context.closePath();
  gc_context.fill();
}

During each iteration of our animationFrame callback we will call function clearGameConsole. This will essentially flood fill the viewport that is 592 x 448 pixels with the color sky blue or #d0e7f9. This is our basic clear screen before we render the background with each animation iteration.

Load the Asset

Now we need a function to load our bitmap from the local system to the application on the browser. At the moment we are only loading from local disk. In a future tutorial we will work with loading assets off the Internet which will require some more coding and a fancy load status bar. For now, we just want to throw out our bitmap onto the canvas of the browser. Remember, we have our tutorial_01.html file stored in a directory that has a subdirectory named img. In directory, img, we have the asset or bitmap file, Level_1_warehouse.png. With this in mind. let's look at the code to load the asset.

Source Listing 4. Load the local file asset.
function loadImageAssets(){

  background = new Image()
  background.src = "file:img/Level_1_warehouse.png"
  
  // background pixel dimensions
  bgwidth = 977;
  bgheight = 478;
  
  // initialize background source blitting x and y
  bgx = 0;
  bgy = 0;
}
// END loadImageAssets 


Rendering to the Game Console canvas

Now we need code to blit our bitmap, which is not stored in a javascript Image object. This is a simple render function that only draws a static image to the canvas element. As we expand our code in future tutorials we will discuss levels of blitting. This is important for parallax scrolling and scrolling bitmaps with various depths of sprite activity going on in the game scene. For now we have a simple blit operation to just through a portion of the bitmap out to the game console viewport (the canvas). Remember, our offscreen bitmap is 977 x 478 pixels. It is much larger than our game console viewport, which is 592 x 448 pixels. We will only blit a portion of bitmap to the viewport.

Source Listing 5. Blitting the bitmap to the game console viewport.
function renderGameConsole() {

  
  // gc_context.drawImage(img_elem, srcx, srcy, srcwidth, srcheight, dx, dy, dw, dh);
  // draw the background first
  try {
    gc_context.drawImage(background, bgx, bgy, gc_canvas.width, gc_canvas.height, 0, 0, gc_canvas.width, gc_canvas.height);
  } 
    catch (e) {
  };
    
}
// END renderGameConsole

We use the context API drawImage to blit to the game console viewport.

Source Listing 6. The animation loop
function animate() {
    requestAnimFrame( animate );

    clearGameConsole();
    renderGameConsole();
}
// END animate

We recursively call requestAnimFrame with our animate function as a parameter. This is the magic woobly, goobly, goo that makes time and relative dimension in space travel and coding possible. That in a nutshell is our game animation loop.
That's the whole lesson for right now. Next time we see you we'll be scrolling that background!
Remember, download the art to an img subdirectory,  should look like this.

tutorial_01.html
\img
    Level_01_warehouse.png

Or from a window environment, like this.


Just copy and paste the code below into your editor and save it as tutorial_01.html.

Source Listing 7. The whole tamale of code for tutorial_01.html !
 
<html>
<head>
</head>
<body>

<script>
// requestAnim shim layer by Paul Irish
    window.requestAnimFrame = (function(){
      return  window.requestAnimationFrame       || 
              window.webkitRequestAnimationFrame || 
              window.mozRequestAnimationFrame    || 
              window.oRequestAnimationFrame      || 
              window.msRequestAnimationFrame     || 
              function(/* function */ callback, /* DOMElement */ element){
                window.setTimeout(callback, 1000 / 60);
              };
    })();
  

// example code from mr doob : http://mrdoob.com/lab/javascript/requestanimationframe/

// global variables
var gc_canvas;                  // 'gc' 'game console'
var gc_context;                 // context
var width = 592, height = 448;  // game console viewport width and height
var background;                 // image asset background
var bgwidth, bgheight;          // background width and height
var bgx, bgy;                   // background source x and y used for blitting

// main code section
initGameConsoleCanvas();
loadImageAssets();
animate();

// function library
function initGameConsoleCanvas() {
    
    gc_canvas = document.createElement( 'canvas' );
    gc_context = gc_canvas.getContext( '2d' );
    gc_canvas.width = width;
    gc_canvas.height = height;
    document.body.appendChild( gc_canvas );
}
// END initGameConsole

function clearGameConsole(){
  gc_context.fillStyle = '#f0e7f9';
  gc_context.clearRect(0, 0, width, height);
  gc_context.beginPath();
  gc_context.rect(0, 0, width, height);
  gc_context.closePath();
  gc_context.fill();
}

function loadImageAssets(){

  background = new Image()
  background.src = "file:img/Level_1_warehouse.png"
  
  // background pixel dimensions
  bgwidth = 977;
  bgheight = 478;
  
  // initialize background source blitting x and y
  bgx = 0;
  bgy = 0;
}
// END loadImageAssets 


function animate() {
    requestAnimFrame( animate );

    clearGameConsole();
    renderGameConsole();
}
// END animate

function renderGameConsole() {

  
  // gc_context.drawImage(img_elem, srcx, srcy, srcwidth, srcheight, dx, dy, dw, dh);
  // draw the background first
  try {
    gc_context.drawImage(background, bgx, bgy, gc_canvas.width, gc_canvas.height, 0, 0, gc_canvas.width, gc_canvas.height);
  } 
    catch (e) {
  };
    
}
// END renderGameConsole 
</script>
</body>
</html>





No comments:

Post a Comment