How to make a NES-style game in HTML5

Most of you must have played cool MMORPGs or other browser based games at some point in your life. Speaking of games, remember the Nintendo GameBoy games back in the good ol’ vintage-filtered days? Since we will be learning how we can create a game, why not try and make one of those games? How about “Racer” ?

nes-racer

(Almost) the classic NES Racer

Design of the game

Let’s break the game down a bit, shall we?

  1. We have pixels, not our screen pixels but the game pixels (which, as you can see, are quite larger).
  2. We have 2 states for each pixel: OFF and ON. The pixels defining the racer cars are in ON state while all others are in OFF state.
  3. We have a racer who is controlled by buttons (or additionally, keyboard).
  4. We have randomly generated bad guys.

Now while I personally would love to see you define a racer by code (i.e, draw the active pixels using Canvas API), I will leave that bit out and use pre-packaged PNGs. Why? Because I already tried rendering the elements by hand. It was slow! And resource intensive. Infact, I researched a bit and found that sometimes, it’s better to just use pre-made sprites instead of drawing them every frame. So, we need some assets.

Gathering Assets

You can download the boilerplate here.

Make a folder named ‘nesracer’ in your localhost root. Extract the zip (racer.zip) into the folder. The structure of your folder should look like this:

  • root
    • nesracer
      • sprites
      • racer.html
      • LCD_Solid.ttf

Open up racer.html inside the folder in your favorite text editor. We need a function to draw our Sprites in every frame:

function Sprite(img, width, height){
this.img = img;
this.width = width;
this.height = height;
}
Sprite.prototype = {
draw: function(ctx, x, y){
ctx.drawImage(
this.img,
0,
0,
this.width,
this.height,
x,
y,
this.width,
this.height
);
}
};

We don’t want our player going out of the screen. So we will create a function to prevent this from happening.

<br />function calcBounds(x,y){
cx = (x > (canvas.width-50))?(canvas.width-50):(x < 0)?0:x; cy = (y > (canvas.height-50))?(canvas.height-50):(y < 0)?0:y;
return [cx,cy];
}

For the collision detection function, we are going to use a function by Joseph Lenton with a bit of minor change. Credits to the original author. But if you want something a bit more sophisticated, you can always use Ninja Physics or some other physics engine.

/**
* @author Joseph Lenton - PlayMyCode.com
*
* @param first An ImageData object from the first image we are colliding with.
* @param x The x location of 'first'.
* @param y The y location of 'first'.
* @param other An ImageData object from the second image involved in the collision check.
* @param x2 The x location of 'other'.
* @param y2 The y location of 'other'.
* @param isCentred True if the locations refer to the centre of 'first' and 'other', false to specify the top left corner.
*/
function isPixelCollision( first, x, y, other, x2, y2, isCentred )
{
// we need to avoid using floats, as were doing array lookups
x = Math.round( x );
y = Math.round( y );
x2 = Math.round( x2 );
y2 = Math.round( y2 );

w = first.width,
h = first.height,
w2 = other.width,
h2 = other.height ;

// deal with the image being centred
if ( isCentred ) {
// fast rounding, but positive only
x -= ( w/2 + 0.5) << 0
y -= ( h/2 + 0.5) << 0
x2 -= (w2/2 + 0.5) << 0
y2 -= (h2/2 + 0.5) <= xMax || yMin >= yMax ) {
return false;
}

xDiff = xMax - xMin,
yDiff = yMax - yMin;

// get the pixels out from the images
pixels = first.data,
pixels2 = other.data;

// if the area is really small,
// then just perform a normal image collision check
if ( xDiff < 4 && yDiff < 4 ) {
for ( pixelX = xMin; pixelX < xMax; pixelX++ ) {
for ( pixelY = yMin; pixelY < yMax; pixelY++ ) {
if (
( pixels [ ((pixelX-x ) + (pixelY-y )*w )*4 + 3 ] !== 0 ) &&
( pixels2[ ((pixelX-x2) + (pixelY-y2)*w2)*4 + 3 ] !== 0 )
) {
return true;
}
}
}
} else {
/* What is this doing?
* It is iterating over the overlapping area,
* across the x then y the,
* checking if the pixels are on top of this.
*
* What is special is that it increments by incX or incY,
* allowing it to quickly jump across the image in large increments
* rather then slowly going pixel by pixel.
*
* This makes it more likely to find a colliding pixel early.
*/

// Work out the increments,
// it's a third, but ensure we don't get a tiny
// slither of an area for the last iteration (using fast ceil).
incX = xDiff / 3.0,
incY = yDiff / 3.0;
incX = (~~incX === incX) ? incX : (incX+1 | 0);
incY = (~~incY === incY) ? incY : (incY+1 | 0);

for ( offsetY = 0; offsetY < incY; offsetY++ ) {
for ( offsetX = 0; offsetX < incX; offsetX++ ) {
for ( pixelY = yMin+offsetY; pixelY < yMax; pixelY += incY ) {
for ( pixelX = xMin+offsetX; pixelX < xMax; pixelX += incX ) {
if ( (( pixels === undefined ) ||
( pixels2 === undefined )) ||
(( pixels [ ((pixelX-x ) + (pixelY-y )*w )*4 + 3 ] !== 0 ) &&
( pixels2[ ((pixelX-x2) + (pixelY-y2)*w2)*4 + 3 ] !== 0 ))
) {
return true;
}
}
}
}
}
}

return false;
}

And finally, we write down our code. I have explained each step of the code as comments so you must be good to go. But, you can always ask me for more details in the comments.

/**
* @author Sagnik Modak - www.staying.me/nintendo/racer.html
*
* @param px The pixels of NES in OFF state
* @param rx The image of the user-controlled racer
* @param re The image of the bad guys (basically, inverted racer)
* @param background The background pattern from px data
* @param canvas Reference to the Canvas Element
* @param ctx 2D Context of the Canvas
* @param meX X position of player
* @param meY Y position of player
* @param timerfeel To store the last time any enemy moved (so it feels like NES)
* @param lastrateupdated The last time the rate of enemy movement was increased
* @param rate The current rate at which enemies move (increases slowly)
* @param lost Boolean value for player status. Initially 'false', if lost, set to 'true'
*/
var px = new Image();
px.src = 'sprites/racer-px.png'; //the off pixels on the screen
var rx = new Image();
rx.src = 'sprites/racer.png'; //the racer
var re = new Image();
re.src = 'sprites/racer-enemy.png'; //the enemies
var background, racer, badGuys = []; //storing the patterns makes it efficient
var canvas = document.getElementById('game-canvas'); //reference to the canvas
var ctx = canvas.getContext('2d'); //2D context of canvas

var meX = 80, meY = 140; //intial position of player
var timerfeel,lastrateupdated; //so that the FPS feels like NES
var rate = 10; //the rate at which enemies progress towards you.
var lost = false; //Cannot afford to lose before you start to win right?
px.onload = function (){
createBackground(); //create background once off pixel loaded
}
rx.onload = function (){
createRacers(); //create racer once racer image loaded
}
re.onload = function(){
createBadGuys(); //create bad guys once bad guys image (WTF?) loaded
}
window.onload = function (){
startGame(); //like it says, duh?
}
function createBackground(){
background = ctx.createPattern(px,"repeat"); //create a pattern by repeating px
drawBackground(); //draw the background
}
function drawBackground(){
ctx.rect(0,0,canvas.width,canvas.height); //clear the canvas
ctx.fillStyle = background; //set pattern as fill style
ctx.fill(); //fill it
}
function createRacers(){
racer = new Sprite(rx,50,50); //racer sprite
racer.draw(ctx,80,140); //draw the racer
document.addEventListener("keydown",function (event){
redrawRacer(event); //when user presses any key, redraw racer
});
}
function createBadGuys(){
badGuys[0] = new Sprite(re,50,50); //create the bad guys
badGuys[1] = new Sprite(re,50,50);
badGuys[0].draw(ctx,0,165); //draw the bad guys
badGuys[1].draw(ctx,50,0);
badGuys[0].x = 0, badGuys[0].y = 165; //initial position of bad guys 1
badGuys[1].x = 50, badGuys[1].y = 0; //initial position of bad guy 2
}
function redrawRacer(e){
if(e.which == 37){
meX-=10; //user pressed Left Arrow Key
}else if(e.which == 39){
meX+=10; //user pressed Right Arrow Key
}else if(e.which == 38){
meY-=10; //user pressed Up Arrow Key
}else if(e.which == 40){
meY+=10; //user pressed Down Arrow Key
}else{
meX = meX, meY = meY; //no point in changing anything, just included for clarity
}
c = calcBounds(meX,meY); //see if the new position is within bounds and if not, stop at max or min allowed value.
meX = c[0];
meY = c[1];
}
function redrawRacerButtons(code){ //same as redraw racers, but this time with buttons in NES controller.
if(code == 4){
meX-=10;
}else if(code == 2){
meX+=10;
}else if(code == 3){
meY-=10;
}else if(code == 1){
meY+=10;
}else{
meX = meX, meY = meY;
}
c = calcBounds(meX,meY);
meX = c[0];
meY = c[1];
}
function redrawBadGuy(){ //draw the bad guys
badGuys[0].draw(ctx,badGuys[0].x,badGuys[0].y);
badGuys[1].draw(ctx,badGuys[1].x,badGuys[1].y);
}
function advanceBadGuys(rate){
for(i=0;i 300){ //to loop the bad guys around
cy = 0;
cx = Math.floor(Math.random()*13)*15; //random x position for bad guys
}else{
cy = y;
cx = badGuys[i].x;
}
badGuys[i].y = cy;
badGuys[i].x = cx;
if(isPixelCollision(badGuys[i],badGuys[i].x,badGuys[i].y,racer,meX,meY))
lost = true; //the racer collided with the bad guys, you lost
}
}
function redraw(){
if((Date.now() - timerfeel) > 1000){
advanceBadGuys(rate);
if((Date.now() - lastrateupdated) > 30000){
rate+=10;
lastrateupdated = Date.now();
}
timerfeel = Date.now();
}
drawBackground();
racer.draw(ctx,meX,meY);
redrawBadGuy();
if(!lost){
window.requestAnimationFrame(redraw);
}else{
gameOver(); //whoops, try again?
}
}
function startGame(){
lastrateupdated = timerfeel = Date.now();
redraw();
}
function restartGame(){
meX = 80, meY = 140;
timerfeel,lastrateupdated;
rate = 10;
lost = false;
createBackground();
createRacers();
createBadGuys();
startGame();
}
function gameOver(){
drawBackground();
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.font = '32px LCDPixels';
ctx.fillText("Game Over",canvas.width/2,canvas.height/2);
ctx.font = '16px LCDPixels';
ctx.fillText("Press Start to begin",canvas.width/2,(canvas.height/2)+20);
}

And there you have it. Save the file and open it in your browser (with localhost, ofcourse).

You can see the entire source code at –

www.staying.me/nintendo/racer.html

And that’s all for today, folks. Thank you for reading. If you want to learn how to code a game AI for a complex game, stay tuned till Monday. If you have any comments or queries, go ahead and tap it out in the comments down below. A rating would go a long way.

Yours truely,

Sagnik

Advertisements

2 Comments

  1. “Do you mind if I quote a couple of your posts as long as I provide credit and sources back to your website? My blog is in the very same niche as yours and my visitors would genuinely benefit from a lot of the information you provide here. Please let me know if this okay with you. Thank you!”

    Like

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.