Showing posts with label context. Show all posts
Showing posts with label context. Show all posts

Monday, June 24, 2013

HTML5 Blitting Using an Offscreen Buffer



One of the great benefits HTML5 has brought us javascript game hackers is the canvas and context. Or more important, the ability to create an off screen canvas and context. This brings us one step closer to game console animation. Video games use off screen buffers for rendering. This technique provides smoother animation to the video game. The process involves rendering all the video game updates to an off screen buffer. An off screen buffer means that all the animation is being written to a buffer that is not visible.  When the game code has completed the off screen rendering, the buffer is FLIPPED to the visible video memory buffer.  Your game console and pc has hardware that handles this in video hardware. We're going to emulate this functionality using software. The reason buffer flipping is used is because it produces smoother looking animation in your video game.
This javascript tutorial is going to draw a simple background and a sprite using off screen buffer rendering and flipping. I am going to keep this simple. In the next demo we'll make the sprite walk and use multiple frames of animation. I just wanted to "keep it simple stupid" so you could get the idea of what is going on in the code. 

The Game Console Class

Over time I have been revamping the original game console code so I can use it in a library to write games. The first objective in this endeavor is to create a Game Console class, js/GameConsoleClass.js.

Source Listing 1. Defining a Game Console class in Javascript

function GameConsole(width, height) {
    this.width = width;
    this.height = height;  
    
    // create the main canvas
    this.canvas = document.createElement( 'canvas' );  
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    this.context = this.canvas.getContext( '2d' );
    
    // add the canvas to the DOM
    document.body.appendChild(this.canvas);
    
    // create the offscreen buffer (canvas)
    this.offscreenCanvas = document.createElement('canvas');
    this.offscreenCanvas.width = this.width;
    this.offscreenCanvas.height = this.height;
    this.offscreenCanvas.context = this.offscreenCanvas.getContext('2d');

} // END GameConsole class


Of course we're going to need some member functions to perform the initialization and rendering.

Source Listing 2. Game Console member functions in Javascript

GameConsole.prototype = {

    initGC: function() {
        this.context.fillStyle = '#000000'; // Black
        //this.context.fillStyle = '#f0eef9'; // Lavender
        this.context.clearRect(0, 0, this.width, this.height);
        this.context.beginPath();
        this.context.rect(0, 0, this.width, this.height);
        this.context.closePath();
        this.context.fill();
    },   // END initGC
    
    // render to the offscreen canvas
    renderOffscreenGC: function(image) {
     // render the background
        this.offscreenCanvas.context.drawImage(image, 0, 0);
    
    }, // END render 
    
    bitBlitOffscreenGC: function(sprite, srcx, srcy, srcwidth, srcheight, dstx, dsty, dstwidth, dstheight) {
        
        this.offscreenCanvas.context.drawImage(sprite, srcx, srcy, srcwidth, srcheight, dstx, dsty, dstwidth, dstheight);
        
    },
    
    // flip the offscreen canvas to the visible GC canvas
    flipGC: function () {
        this.context.drawImage(this.offscreenCanvas, 0, 0);
    }, // END flipGC
    
}; // END GameConsole.prototype

Again, I kept everything really simplistic to give you a good idea of how the code works. I too have spent time in javascript books where the author's intent isn't to educate but to demonstrate how great their coding skills are. What is going on in their code is not blatantly obvious to the casual observer.

The Main Driver Code

Ok, let's draw some sprites. The source code js/MainGame.js contains the demo code to create a Game Console object in javascript and render to it. The code is logically straight forward with no sneaky coding demos. The demo code loads a background image and a sprite. That's it. Nothing fancy.



Source Listing 3. Loading and Blitting Sprites in Javascript

// create a Game Console object
var gc = new GameConsole(592,448);
gc.initGC();

// Background image
// load the image
var backgroundReady = false;
var backgroundImage = new Image();
backgroundImage.onload = function () {
    backgroundReady = true;
};
backgroundImage.src = "img/Level_1_warehouse.png";


// load the sprite
// load Sprite Sheet
var sprite = {};
var spriteSheetReady = false;
var spriteSheetImage = new Image();
spriteSheetImage.onload = function () {
    spriteSheetReady = true;
};
spriteSheetImage.src = "img/aliensvspredator_ltlinnkurosawa_sheet_sclx2.png";
var spriteWidth = spriteSheetImage.width /4;
var spriteHeight = 118;

gc.renderOffscreenGC(backgroundImage);

sprite.x = 250;
sprite.y = 250;

gc.bitBlitOffscreenGC(spriteSheetImage, 0, 144, spriteWidth, spriteHeight, sprite.x, sprite.y, spriteWidth, spriteHeight); 
gc.flipGC();

The code can be downloaded from GitHub   https://github.com/retrogamecode/tutorials.
Unfortunately, I can't upload the images there. So I will add them at the end of this article where you can grab it and download it.

Walking through the not so obvious code....

var sprite = {}; 


Creates a simple javascript object I will use for storing values for the sprite's current x (sprite.x), and y, (sprite.y) position. 

gc.renderOffscreenGC(backgroundImage);

sprite.x = 250;
sprite.y = 250;

gc.bitBlitOffscreenGC(spriteSheetImage, 0, 144, spriteWidth, spriteHeight, sprite.x, sprite.y, spriteWidth, spriteHeight); 
gc.flipGC();

In the bitBlit operation, the sprite.x and sprite.y are the destination positions I will draw the sprite. The x=0, and y =144 values for the bitBlit are values I pulled from photoshop when examining the sprite's location. It's a quick a dirty demo.

Since this is retro game coding we are blitting!!!! A bit blit is bit block transfer. We're kicking it old school here. Even though most low end PCs now have far more computing power than the CRAY computer of the early 90s. Anyhow, video game consoles of ages since past had hardware blitters in them for this operation. We're doing it in our software.

When the blit is completed -we FLIP the offscreen buffer to the visible buffer with the class method,

gc.flipGC();


Play around with the code and the sprites.


Source Listing 4. The HTML source file index.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>RetroGameCode</title>
    </head>
    <h1>Game Console Offscreen Tutorial</h1>
        
        <script src="js/GameConsoleClass.js"></script>
        <script src="js/MainGame.js"></script>
    <body>
</html>

That's it for this demo. Have fun coding! Next venture we will move the sprites and do cell animation!



img/aliensvspredator_ltlinnkurosawa_sheet_sclx2.png

img/Level_1_warehouse.png

Tuesday, March 19, 2013

Creating a Console Game Screen Using HTML5

 After some time off from playing with this code from tutorials 1-3, I decided to go back and optimize and clean up a few things before we move on. This edition of retrogamecode we're going to look at creating a dynamic game console interface using HTML 5 canvas object.

I ripped out a lot of code for the examples to stand on their own and be clear to you the student or the developer.

Here are the basic javascript code snippets for creating a game console using the HTML5 canvas object.

You can download the entire HTML source file from github: create_html5_canvas.html

Source Listing 1. Defining a Super Nintendo Game Console in Javascript

// game console variables
var gc_canvas;                    // 'gc' 'game console'
var gc_context;                   // gc context
var gc_width = 592, gc_height = 448;   // game console viewport width and height Super Nintendo parameters
var gc_light_lavender = '#f0eef9';  // color light lavender
var gc_lavender = '#f0e7f9';   // color lavender
var gc_black = '#000000';    // color black

We're creating a Super Nintendo console of 592 pixels wide by 448 pixels high. The var gc_canvas is our game console HTML5 canvas object. Let's take a look at the javascript function we'll use to do this.

Source Listing 2. Dynamically creating an HTML5 canvas object in Javascript

// function: initGameConsoleCanvas
// description:
//    initialize the canvas element
//    and the drawing context API 
//    components.
//
function initGameConsoleCanvas( width, height) {
    
    // create the HTML5 canvas object
    gc_canvas = document.createElement( 'canvas' );   
    gc_context = gc_canvas.getContext( '2d' );
    
    // attach the canvas to the HTML document object
    document.body.appendChild( gc_canvas );
    gc_canvas.width = width;
    gc_canvas.height = height;
    
}
// END initGameConsoleCanvas(width, height)

We're going to need to a function to clear the screen for animation. And for right now we just want to throw something out on the screen to see if our game console canvas is working. The following function erases the game console canvas and fills the screen black.


Source Listing 3. Erase the HTML5 canvas object using Javascript

// function: clearGameConsoleCanvas
// description:
//    erase the game console using the 
//    specified color.
//
function clearGameConsoleCanvas(width, height, color) {
    // Fill the screen with a black background
   gc_context.fillStyle = color; //'#000000';
   gc_context.clearRect(0, 0, width, height);
   gc_context.beginPath();
   gc_context.rect(0, 0, width, height);
   gc_context.closePath();
   gc_context.fill();
}
// END clearGameConsoleCanvas(width, height, color)

Putting it all together, we call the document.onload() function to initialize the game screen as the web page initializes.

Source Listing 3. Erase the HTML5 canvas object using Javascript
// initialize on document load
window.onload = function() {

  // create the HTML 5 game console canvas 
  initGameConsoleCanvas(gc_width, gc_height);
  
  // initialize the game console canvas to black
  clearGameConsoleCanvas(gc_width, gc_height, gc_black);  
        
}
// END window.onload()

That's basically it. I hope by stripping out all the code the initialization of the game console canvas makes perfect sense to you. Ideally, all the game console code will be moved out to a javascript source file and then sourced in from the HTML file. We will do this in the next tutorial as we move along.

Have fun scripting! Go make some games.

Sunday, June 10, 2012

Part 02 Scrolling Background Bitmap with HTML 5

In tutorial 01, we learned a few basics about HTML 5. It has a canvas element and javascript has a context API we use to draw to the canvas. We're going to build upon the simple script from tutorial 01 and have the background scroll. Our offscreen background bitmap, Level_1_warehouse.png, is 977 pixels wide by 478 pixels high. Our game console viewport is 592 pixels wide and 448 pixels high. The background bitmap file is wider and taller than our viewport.

The object is to drag the viewport along the bitmap to give a moving background effect. We're keeping this example to a simple bitmap scroll. When you get the hang of this your renderGamConsole() function we wrote in tutorial 01 can be modified to draw sprites on multiple layers with different sprite velocities. This method rendering is called parallax scrolling. You can render clouds or a star field on the first plane of animation to giving a motion feel. Then draw a slower scrolling city scape in the background as you have a spaceship battle aliens. This method of drawing sprites and backgrounds on multiple layers is called parallax scrolling. For now we're going to focus on a simple background scroll. You can see in our screen shot from the Capcom game, Alien vs. Predators, that we have the background drawn first, then the Lt. Linn sprite and the alien boss, then the sprite health bars are drawn. There are about 3 layers of animation is the screen grab. So when we render this we will draw the background  layer first, the sprite layer(s) next, and then the status bar sprites last so they are foremost in the screen. Giving the feel of depth in the final rendering.

Code Cleanup

If you have been following along from tutorial 01 you'll notice I did a quick code cleanup. I separated functions and moved around some of the logic so it's easier to understand as we work on tutorial 02. There are basically only two new function calls in this code, checkgameConsoleInputDevice() and updatePlayer(). I added these two functions so we can manually control the bitmap scrolling using the input from a keyboard with the right and left arrow keys. I am using the jQuery library for keyboard input. This can actually change as I experiment and find which library works best with input to mobile devices and computers as well. For now we're just up and running with jQuery.

The keyboard code using jQuery is here.

Source Listing 1. Checking the game console input devices.

// function: checkGameConsoleInputDevice
// description:
//    jQuery routine to check the keyboard 
//    input.
//
function checkGameConsoleInputDevice() {


    $(document).keydown(function(evt) {
        if (evt.keyCode == 39) {
            if (bgx < 380) {
            gamePadMove = 'RIGHT';
            //bgx++;
            }
        } else if (evt.keyCode == 37){
            if (bgx > 0) {
            gamePadMove = 'LEFT';
            //bgx--;
            }
        }
    });         

    $(document).keyup(function(evt) {
        if (evt.keyCode == 39) {
            gamePadMove = 'NONE';
        } else if (evt.keyCode == 37){
            gamePadMove = 'NONE';
        }
    }); 
}
// END checkGameConsoleInputDevice


As you can see, the checkGameConsole() function is the basics for running a game pad or controller. We don't care if it is arrow icons on a mobile device or right and left arrows on a keyboard. This is the skeleton of our final function for checking the input values on our game console. I am setting flags if the game pad was pressed or in this simple case if the right or left arrow was pressed. These values are passed on to the game for updating the values in the game.

In function updatePlayer(), we listen for the gamePadMove flags to be set. When we act on the flag needing update, we then reset the flag.

Source Listing 2. Updating the gmae position
// function: updatePlayer
// description:
//    based on the input from
//    checkGameConsoleInputDevice
//    update the player position.
//
function updatePlayer() {

    if (gamePadMove == 'RIGHT') {
      bgx += 5;
      gamePadMove = 'NONE';
    }  
    if (gamePadMove == 'LEFT') {
      bgx -= 5;
      gamePadMove = 'NONE';
    }  
  // end switch gamePadMove
  
}
// END updatePlayer

The bgx variable is a global variable used to identify the current x-position of the origin x-component of the viewport. So as the viewport slides back and forth over the background we update the bgx variable. Essentially, this is the bottom-left corner of the viewport rectangle as slide across the bitmap.

Scrolling Background

Now we get to the topic of interest, scrolling the background. We are going to scroll the background to the right or to the left based on user input. Now that our background is non-static and it moves, we need to update how we render the bitmap in renderGameConsole() function.


Source Listing 3. Changes for scrolling bitmaps.
// function: renderGameConsole
// description:
//   draws the content to the 
//   game console canvas.
//
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, width, height, 0, 0, gc_canvas.width, gc_canvas.height);
  } 
    catch (e) {
  };
    
}
// END renderGameConsole


Running the Code in Your Sandbox

You'll need to copy the code below and paste it into an editor. Save the file as tutorial_02.html. Your development environment should be a simple directory, with one subdirectory, called img. Place the background art file into this directory. Your setup should look like this.

The entire source code for this example is presented here. We're using the same art file from tutorial 01. You can download the Level_1_warehouse.png art file here.

Source Listing 4. The whole tamale!


<html>
<title>retrogamecode tutorial 02 scrolling background</title>
<head>

</head>
<body>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>

<script type="text/javascript">
// 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/

// http://www.retrogamecode.blogspot.com
// author: michael norton


//------------------------------------------
// Global Variables Declarations
//------------------------------------------

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

var gamePadMove;                // game console device input

//------------------------------------------
// Functions
//------------------------------------------

// Game Console Functions

// function: initGameConsoleCanvas
// description:
//    initialize the canvas element
//    and the drawing context API 
//    components.
//
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 init

// function: clearGameConsole
// description:
//   clears the game console before each
//   call to renderGameConsole.
//
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();
}
// END clearGameConsole

// function: renderGameConsole
// description:
//   draws the content to the 
//   game console canvas.
//
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, width, height, 0, 0, gc_canvas.width, gc_canvas.height);
  } 
    catch (e) {
  };
    
}
// END renderGameConsole

// function: checkGameConsoleInputDevice
// description:
//    jQuery routine to check the keyboard 
//    input.
//
function checkGameConsoleInputDevice() {


    $(document).keydown(function(evt) {
        if (evt.keyCode == 39) {
            if (bgx < 380) {
            gamePadMove = 'RIGHT';
            //bgx++;
            }
        } else if (evt.keyCode == 37){
            if (bgx > 0) {
            gamePadMove = 'LEFT';
            //bgx--;
            }
        }
    });         

    $(document).keyup(function(evt) {
        if (evt.keyCode == 39) {
            gamePadMove = 'NONE';
        } else if (evt.keyCode == 37){
            gamePadMove = 'NONE';
        }
    }); 
}
// END checkGameConsoleInputDevice

// Game specific functions

// function: loadImageAssets
// description:
//    loads the background art file
//    from local disk.
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: updatePlayer
// description:
//    based on the input from
//    checkGameConsoleInputDevice
//    update the player position.
//
function updatePlayer() {

    if (gamePadMove == 'RIGHT') {
      bgx += 5;
      gamePadMove = 'NONE';
    }  
    if (gamePadMove == 'LEFT') {
      bgx -= 5;
      gamePadMove = 'NONE';
    }  
  // end switch gamePadMove
  
}
// END updatePlayer

// function: animate
// description:
//   this is the main game lopp
//   function. It calls itself 
//   recursively.
function animate() {
    requestAnimFrame( animate );

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



// main game loop
initGameConsoleCanvas();
loadImageAssets();
animate();

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