UberGamerMonkey.com

Adventures in TypeScript 04

2016-11-16

Welcome back for another day of adventure! Today we will be adding bricks so our ball can smash them to pieces! If you don't remember our last adventure (Adventures in TypeScript 03), we implemented a paddle and made it so if the ball hits the bottom of the canvas, it resets.

You can see the final outcome of the Breakout clone here. As well as see all the code in my GitHub repo.

Game.ts - Brick it to me

Now we have our paddle moving about and able to smack the ball around, let's give the ball something to smash into! We will be using the same iEntity interface for our bricks. So let's start there, shall we?

class cBrick implements iEntity {
    public x: number = 0;
    public y: number = 0;
    public velX: number = 0;
    public velY: number = 0;
    public width: number = 0;
    public height: number = 0;
    public color: string = "white";
    public active: boolean = true;

    public draw = (): void => {

    }

    public update = (): void => {

    }
}

Much like the paddle, we need more properties such as the size of the brick as well as what color. I also added a boolean for active. This will let the game know if it has been hit or not. Drawing the brick is pretty much like drawing the paddle, except we aren't going to stroke it. The update will just check to see if the ball has hit the brick or not. If so, it will bounce the ball and set the brick to be inactive. Let's also setup a constructor to handle creating the brick.

constructor(x: number, y: number, width: number, height: number, color: string = "white")
{
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.color = color;
}

public draw = (): void => {
    if(this.active) {
        // draw the nice little brick!
        ctx.save();
        ctx.beginPath();
        ctx.rect(this.x, this.y, this.width, this.height);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.closePath();
    }
}

public update = (): void => {
    if(this.active) {
        if(ball.x + ball.radius > this.x && ball.x - ball.radius < this.x + this.width && ball.y + ball.radius > this.y && ball.y - ball.radius < this.y + this.height) {
            ball.velY = -ball.velY;
            this.active = false;
        }
    }
}

We are checking to make sure the brick is active on draw and update. We don't want to show the brick if it has been hit, as well as keep checking for collision every update. Now since we are going to be adding quite a few bricks, we should probably put them in an Array of sorts. While we are at it, since the bricks, ball and paddle are all iEntitys, we will create an array of those.

var entity_array: Array<iEntity> = new Array<iEntity>();

function gameLoop()
{
    // lets keep the game loop going!
    requestAnimationFrame(gameLoop);

    // fill to black!
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    var entity: iEntity;
    for(var i: number = 0; i < entity_array.length; i++) {
        entity = entity_array[i];
        entity.update();
        entity.draw();
    }
}

window.onload = () => {
    // grab our canvas, and get a 2d context
    canvas = <HTMLCanvasElement>document.getElementById('gameCanvas');
    ctx = canvas.getContext("2d");

    // let's handle the user input
    document.addEventListener('keydown', keyDownHandler);
    document.addEventListener('keyup', keyUpHandler);

    ball.velX = 3;
    ball.velY = 3;
    paddle.velX = 5;

    entity_array.push(ball);
    entity_array.push(paddle);

    // run the game!
    gameLoop();
}

Now that we have a nice array in place to handle all the updates and draws of our entities, lets add some bricks to smash. To make things easier along down the road, lets come up with a value for each brick, and parse it so we can create a nice brick map. I'm using the character 'R' for a red brick, 'B' for blue, 'G' for green, 'Y' for yellow, and 'W' for white. Also, let's figure out a way to know when to start a new row. For this, I am using the '|' character. Let's create a function that will create new bricks, and add them to the entity_array that we created above.

function loadBricks() {
    var brickMap: string = "RRRRRRRRRRRRRR|BBBBBBBBBBBBBB|GGGGGGGGGGGGGG|YYYYYYYYYYYYYY|WWWWWWWWWWWWWW|YYYYYYYYYYYYYY|GGGGGGGGGGGGGG|BBBBBBBBBBBBBB|RRRRRRRRRRRRRR";
    var xpos: number = 35;
    var ypos: number = 35;
    var pad: number = 5;
    var bwidth: number = 25;
    var bheight: number = 10;

    for(var c: number = 0; c < brickMap.length; c++) {
        switch(brickMap[c]) {
            case 'R':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "red");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'B':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "blue");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'G':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "green");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'Y':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "yellow");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'W':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "white");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case '|':
            {
                xpos = 35;
                ypos += bheight + pad;
            }
            break;
        }
    }
}

Now all that is left is to call the newly loadBricks function in our window.onload event handler before we start the gameLoop.

window.onload = () => {
    // grab our canvas, and get a 2d context
    canvas = <HTMLCanvasElement>document.getElementById('gameCanvas');
    ctx = canvas.getContext("2d");

    // let's handle the user input
    document.addEventListener('keydown', keyDownHandler);
    document.addEventListener('keyup', keyUpHandler);

    ball.velX = 3;
    ball.velY = 3;
    paddle.velX = 5;

    entity_array.push(ball);
    entity_array.push(paddle);

    loadBricks();

    // run the game!
    gameLoop();
}

If you compile and run it now, you should see a nice set of bricks ready to be smashed by the ball. Here is what game.ts should look like up to this point.

var canvas: HTMLCanvasElement;
var ctx: CanvasRenderingContext2D;

// base interface for our entities that we want to draw on the screen
// and handle in the game.
interface iEntity {
    draw(): void;
    update(): void;
    x: number;
    y: number;
    velX: number;
    velY: number;
}

// class for the ball entity
class cBall implements iEntity {
    public x: number = 0;
    public y: number = 0;
    public velX: number = 0;
    public velY: number = 0;
    public radius: number = 0;
    public color: string = "white";

    constructor(x: number, y: number, radius: number, color: string = "white")
    {
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.color = color;
    }

    public draw = (): void => {
        // draw a nice circle!
        ctx.save();
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.closePath();
    }

    public update = (): void => {
        // update the balls position
        this.x += this.velX;
        this.y += this.velY;

        // did we hit the left or right bounds?
        // if so, lets bounce back the opposite direction.
        if(this.x > canvas.width - this.radius || this.x < this.radius) {
            this.velX = -this.velX;
        }

        // did we hit the ceiling?
        // if so, let's bounce back down.
        if(this.y < this.radius) {
            this.velY = -this.velY;
        }else if(this.y > paddle.y - this.radius) {
            // check to see if the ball is near the height of the paddle
            // if so, let's check to see if it is in the same area on the X axis as the paddle,
            // and check to see if the ball is still above the paddle.
            if(this.y < paddle.y && this.x + this.radius > paddle.x && this.x - this.radius < paddle.x + paddle.width) {
                // bounce the ball back up!
                // if already going down (if hits paddle at odd angle, and gets stuck, we just want it to go up,
                // and not bounce back n forth until out of paddle.)
                if(this.velY > 0)
                    this.velY = -this.velY;
            }else if(this.y > canvas.height - this.radius) {
                // reset the ball back to start.
                this.x = 240;
                this.y = 290;
            }
        }
    }
}

// class for the paddle entity (aka, the player)
class cPaddle implements iEntity {
    public x: number = 0;
    public y: number = 0;
    public velX: number = 0;
    public velY: number = 0;
    public width: number = 0;
    public height: number = 0;
    public color: string = "white";
    public strokeColor: string = "#363636";
    public moveRight: boolean = false;
    public moveLeft: boolean = false;

    constructor(x: number, y: number, width: number, height: number, color: string = "white")
    {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = color;
    }

    public draw = (): void => {
        // draw the nice little paddle!
        ctx.save();
        ctx.beginPath();
        ctx.rect(this.x, this.y, this.width, this.height);
        ctx.fillStyle = this.color;
        ctx.fill();
        ctx.strokeStyle = this.strokeColor;
        ctx.lineWidth = 1;
        ctx.strokeRect(this.x,this.y,this.width, this.height);
        ctx.closePath();
    }

    public update = (): void => {

        // are we moving right or left?
        if(this.moveRight) {
            this.x += this.velX;
        }else if(this.moveLeft) {
            this.x -= this.velX;
        }

        // make sure the user can't go off the X axis bounds.
        if(this.x > canvas.width - this.width) {
            this.x = canvas.width - this.width;
        }

        if(this.x < 0) {
            this.x = 0;
        }
    }
}

class cBrick implements iEntity {
    public x: number = 0;
    public y: number = 0;
    public velX: number = 0;
    public velY: number = 0;
    public width: number = 0;
    public height: number = 0;
    public color: string = "white";
    public active: boolean = true;

    constructor(x: number, y: number, width: number, height: number, color: string = "white")
    {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.color = color;
    }

    public draw = (): void => {
        if(this.active) {
            // draw the nice little brick!
            ctx.save();
            ctx.beginPath();
            ctx.rect(this.x, this.y, this.width, this.height);
            ctx.fillStyle = this.color;
            ctx.fill();
            ctx.closePath();
        }
    }

    public update = (): void => {
        if(this.active) {
            if(ball.x + ball.radius > this.x && ball.x - ball.radius < this.x + this.width && ball.y + ball.radius > this.y && ball.y - ball.radius < this.y + this.height) {
                ball.velY = -ball.velY;
                this.active = false;
            }
        }
    }
}

var entity_array: Array<iEntity> = new Array<iEntity>();
var ball: cBall = new cBall(240, 290, 5);
var paddle: cPaddle = new cPaddle(215, 300, 50, 10, "gray");

function gameLoop()
{
    // lets keep the game loop going!
    requestAnimationFrame(gameLoop);

    // fill to black!
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    var entity: iEntity;
    for(var i: number = 0; i < entity_array.length; i++) {
        entity = entity_array[i];
        entity.update();
        entity.draw();
    }
}

function keyDownHandler(e: KeyboardEvent) {
    // right arrow == 39
    // left arrow == 37
    if(e.keyCode == 39) {
        paddle.moveRight = true;
    }else if(e.keyCode == 37) {
        paddle.moveLeft = true;
    }
}

function keyUpHandler(e: KeyboardEvent) {
    if(e.keyCode == 39) {
        paddle.moveRight = false;
    }else if(e.keyCode == 37) {
        paddle.moveLeft = false;
    }
}

function loadBricks() {
    var brickMap: string = "RRRRRRRRRRRRRR|BBBBBBBBBBBBBB|GGGGGGGGGGGGGG|YYYYYYYYYYYYYY|WWWWWWWWWWWWWW|YYYYYYYYYYYYYY|GGGGGGGGGGGGGG|BBBBBBBBBBBBBB|RRRRRRRRRRRRRR";
    var xpos: number = 35;
    var ypos: number = 35;
    var pad: number = 5;
    var bwidth: number = 25;
    var bheight: number = 10;

    for(var c: number = 0; c < brickMap.length; c++) {
        switch(brickMap[c]) {
            case 'R':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "red");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'B':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "blue");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'G':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "green");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'Y':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "yellow");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case 'W':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, "white");
                xpos += bwidth + pad;
                entity_array.push(b);
            }
            break;
            case '|':
            {
                xpos = 35;
                ypos += bheight + pad;
            }
            break;
        }
    }
}

window.onload = () => {
    // grab our canvas, and get a 2d context
    canvas = <HTMLCanvasElement>document.getElementById('gameCanvas');
    ctx = canvas.getContext("2d");

    // let's handle the user input
    document.addEventListener('keydown', keyDownHandler);
    document.addEventListener('keyup', keyUpHandler);

    ball.velX = 3;
    ball.velY = 3;
    paddle.velX = 5;

    entity_array.push(ball);
    entity_array.push(paddle);

    loadBricks();

    // run the game!
    gameLoop();
}

It's coming together quit nicely. For the next tutorial, we will be implementing scores and lives to our game.

About Me

I write code for a living. I also enjoy games (making and playing), music (listening and producing), and a few other things.

Tags

© 2022 UberGamerMonkey.com. All rights reserved