Making HTML5 Canvas Canvas

Sunday, September 08, 2013

Part 6. The snake game

Now that we know the basics to make a game, it's time to detail some aspects and shape our code to one of the classic games: The Snake.

Let's start commenting all the code about the walls, so they don't make harder the development tests. Later, you will choose whether you include them or not on your final game.

The most essential in the Snake game, is that the main character grows when it eats an apple. To do this, we will use an array for the body, just like the one with the walls. Therefore, we will change the "player" variable, for an array called "body":
//var player=new Rectangle(40,40,10,10);
var body=new Array();
And every use of "player" in our code, will be changed for "body[0]", which would be the snake's head. So, where we moved "player.x" and "player.y", now we will move "body[0].x" and "body[0].y". We can us the "replace" command in out text editor (usually Edit » Search and Replace) to do this task automatically.

Is also important to highlight in the "paint" function, that we are not drawing any more just the head, but all the body, this done through a "for":
    for(var i=0,l=body.length;i<l;i++){
        body[i].fill(ctx);
    }
For the snake having always the same length at the start of the game, we must "cut it" to zero, and then add each part of the body as long as we wish. This is done at the "reset" function this way:
    body.length=0;
    body.push(new Rectangle(40,40,10,10));
    body.push(new Rectangle(0,0,10,10));
    body.push(new Rectangle(0,0,10,10));
Is important that the first part of the body (the head) is at the position we want it to start. The rest of the body will follow it later.

To move the body of the snake, we will do a special trick. The body must move from the back to the front, and always before moving the head, making a caterpillar effect, on which the tail goes "pushing" the rest of the body, through the following "for":
         // Move Body
        for(var i=body.length-1;i>0;i--){
            body[i].x=body[i-1].x;
            body[i].y=body[i-1].y;
        }
Doing it the other way would make the body to be always at the same site all together, and we wouldn't just "not see anything", but also, this would do a game over since the head would crash with the body all the time.

To check if the head crashes against the body, we will do it the same way we checked for the player crashing the wall:
        // Body Intersects
        for(var i=2,l=body.length;i<l;i++){
            if(body[0].intersects(body[i])){
                gameover=true;
                pause=true;
            }
        }
Observe we start counting from the second part of the body. This is because part 0 is the head and part 1, the neck. If we start from one of these, is possible that our game get stuck in a constant Game Over "with apparently no reason" (The neck, because it is too close to the head).

Finally, and very important, is making the snake grow. This is done (logically) when the head touches the apple, adding this line just before adding one to the score:
            body.push(new Rectangle(food.x,food.y,10,10));
We save and update the page. With this, our game concludes, and we have a simple and complete classic snake game.

Media


Before we actually conclude this little tutorial, we will learn how to include external media to our game, I mean, images and sound, something very important on game development.

If we didn't work with images since the beginning, it's because the important fact to highlight that the size of our images is independent to the size of our rectangles at our game.

Right now, our rectangles are 10x10 pixels size, and if we include images that are bigger or smaller, this could give the appearance that objects at the canvas are touching themselves or not, when at the code it's actually happening the other way. That is why it is extremely important that the images have always the size of our rectangles at the game (except of course, when advanced techniques are used for visual effects).

Let's start opening our favourite image editor (This can be MS Paint or whichever we have), and in a 10x10 pixels area, we will make two draws: Our food (a fruit) and a part of the body of the snake (I made a circle with a spot, but you can draw it as you want).

These two images ("body.png" and "fruit.png") will be save in a folder named "assets", in the same folder where our code is (is extremely important that the "assets" folder is always at the same place as the code, or the images won't be loaded). Now, we will declare our images variables and give them the path to the source:
var iBody=new Image(),iFood=new Image();

iBody.src='assets/body.png';
iFood.src='assets/fruit.png';
Modify now the "paint" function, commenting where we painted the body and food rectangles, and painting the images we made instead:
    for(var i=0,l=body.length;i<l;i++){
        //body[i].fill(ctx);
        ctx.drawImage(iBody,body[i].x,body[i].y);
    }
    //food.fill(ctx);
    ctx.drawImage(iFood,food.x,food.y);
When we save and update, we can see now that our graphics are displayed now on the canvas instead of the coloured rectangles. We can do something similar to put a background image to our game.

Finally, let's add some sound. Every time we eat a fruit, we will play a sound, and when we die, we will play another. You can find these sounds for your game on the web. We declare them this way:
var aEat=new Audio(),aDie=new Audio();

aEat.src='assets/chomp.m4a';
aDie.src='assets/dies.m4a';
And to play them, we add at the given intersection checking (with the food and with the body), the next calls:
    aEat.play();
    aDie.play();
This way, we conclude our little tutorial, which I hope is very helpful and grateful for you. If you have any questions, you can leave them below; be sure I'll reply your comments! Also, you can send me the links to the games you make thanks to this tutorial, which I'll be glad to know.

Happy codding!

Assets:


(Use left click and "Save link as" to save these files).

Final code:

[Canvas not supported by your browser]
window.addEventListener('load',init,false);
var canvas=null,ctx=null;
var lastPress=null;
var pause=true,gameover=true;
var dir=0,score=0;
var body=new Array();
var food=new Rectangle(80,80,10,10);
//var wall=new Array();
var iBody=new Image(),iFood=new Image();
var aEat=new Audio(),aDie=new Audio();

iBody.src='assets/body.png';
iFood.src='assets/fruit.png';
aEat.src='assets/chomp.m4a';
aDie.src='assets/dies.m4a';

//wall.push(new Rectangle(100,50,10,10));
//wall.push(new Rectangle(100,100,10,10));
//wall.push(new Rectangle(200,50,10,10));
//wall.push(new Rectangle(200,100,10,10));

var KEY_ENTER=13;
var KEY_LEFT=37;
var KEY_UP=38;
var KEY_RIGHT=39;
var KEY_DOWN=40;

function random(max){
    return Math.floor(Math.random()*max);
}

function init(){
    canvas=document.getElementById('canvas');
    canvas.style.background='#000';
    ctx=canvas.getContext('2d');
    run();
    repaint();
}

function run(){
    setTimeout(run,50);
    act();
}

function repaint(){
    requestAnimationFrame(repaint);
    paint(ctx);
}

function reset(){
    score=0;
    dir=1;
    body.length=0;
    body.push(new Rectangle(40,40,10,10));
    body.push(new Rectangle(0,0,10,10));
    body.push(new Rectangle(0,0,10,10));
    food.x=random(canvas.width/10-1)*10;
    food.y=random(canvas.height/10-1)*10;
    gameover=false;
}

function act(){
    if(!pause){
        // GameOver Reset
        if(gameover)
            reset();

        // Move Body
        for(var i=body.length-1;i>0;i--){
            body[i].x=body[i-1].x;
            body[i].y=body[i-1].y;
        }
     
        // Change Direction
        if(lastPress==KEY_UP&&dir!=2)
            dir=0;
        if(lastPress==KEY_RIGHT&&dir!=3)
            dir=1;
        if(lastPress==KEY_DOWN&&dir!=0)
            dir=2;
        if(lastPress==KEY_LEFT&&dir!=1)
            dir=3;

        // Move Head
        if(dir==0)
            body[0].y-=10;
        if(dir==1)
            body[0].x+=10;
        if(dir==2)
            body[0].y+=10;
        if(dir==3)
            body[0].x-=10;

        // Out Screen
        if(body[0].x>canvas.width-body[0].width)
            body[0].x=0;
        if(body[0].y>canvas.height-body[0].height)
            body[0].y=0;
        if(body[0].x<0)
            body[0].x=canvas.width-body[0].width;
        if(body[0].y<0)
            body[0].y=canvas.height-body[0].height;
     
        // Wall Intersects
        //for(var i=0,l=wall.length;i<l;i++){
        //    if(food.intersects(wall[i])){
        //        food.x=random(canvas.width/10-1)*10;
        //        food.y=random(canvas.height/10-1)*10;
        //    }
        //  
        //    if(body[0].intersects(wall[i])){
        //        gameover=true;
        //        pause=true;
        //    }
        //}
     
        // Body Intersects
        for(var i=2,l=body.length;i<l;i++){
            if(body[0].intersects(body[i])){
                gameover=true;
                pause=true;
                aDie.play();
            }
        }
     
        // Food Intersects
        if(body[0].intersects(food)){
            body.push(new Rectangle(food.x,food.y,10,10));
            score++;
            food.x=random(canvas.width/10-1)*10;
            food.y=random(canvas.height/10-1)*10;
            aEat.play();
        }
    }
    // Pause/Unpause
    if(lastPress==KEY_ENTER){
        pause=!pause;
        lastPress=null;
    }
}

function paint(ctx){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    //ctx.fillStyle='#0f0';
    for(var i=0,l=body.length;i<l;i++){
        //body[i].fill(ctx);
        ctx.drawImage(iBody,body[i].x,body[i].y);
    }
    //ctx.fillStyle='#999';
    //for(var i=0,l=wall.length;i<l;i++){
    //    wall[i].fill(ctx);
    //}
    //ctx.fillStyle='#f00';
    //food.fill(ctx);
    ctx.drawImage(iFood,food.x,food.y);
 
    ctx.fillStyle='#fff';
    //ctx.fillText('Last Press: '+lastPress,0,20);
    ctx.fillText('Score: '+score,0,10);
    if(pause){
        ctx.textAlign='center';
        if(gameover)
            ctx.fillText('GAME OVER',150,75);
        else
            ctx.fillText('PAUSE',150,75);
        ctx.textAlign='left';
    }
}

document.addEventListener('keydown',function(evt){
    lastPress=evt.keyCode;
},false);

function Rectangle(x,y,width,height){
    this.x=(x==null)?0:x;
    this.y=(y==null)?0:y;
    this.width=(width==null)?0:width;
    this.height=(height==null)?this.width:height;
 
    this.intersects=function(rect){
        if(rect!=null){
            return(this.x<rect.x+rect.width&&
                this.x+this.width>rect.x&&
                this.y<rect.y+rect.height&&
                this.y+this.height>rect.y);
        }
    }
 
    this.fill=function(ctx){
        if(ctx!=null){
            ctx.fillRect(this.x,this.y,this.width,this.height);
        }
    }
}

window.requestAnimationFrame=(function(){
    return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        function(callback){window.setTimeout(callback,17);};
})();

No comments:

Post a Comment