UberGamerMonkey.com

Adventures in TypeScript 06

2016-11-30

Welcome back for another day of adventure! Today we will be refactoring how we handle the brickmap, as well as adding some new brick types. If you don't remember our last adventure (Adventures in TypeScript 05), we implemented the bricks that the ball smashes.

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 again, Sam

To start things off, let's create an enum to spell out what types of bricks that the player will encounter. There will be Normal bricks, which are the ones we have already created. We also want to create a Double and Triple brick, which takes 2 and 3 hits to destroy. And finally, we will create an Unbreakable brick that can't be destroyed at all. Later when we edit the brick class, we will implement the new types.

const enum BrickType {
    Normal,
    Double,
    Triple,
    Unbreakable
}

The way we are handling the bricks works for just 1 level. It's not much of a game after you finished the level of bricks. We need to have the ability to add more than just the 1 level and load up the next when the player has finished breaking all the bricks of the said level. To start things off, we will create a new interface called iBrickMap. This will be the base interface for our cBrickMap class. We want the cBrickMap to know how many bricks that are active, an array of the bricks, and then ability to load, update, draw the bricks, and to reset when the level is over.

// base interface for our BrickMap
interface iBrickMap {
    brickCount: number;
    brickArray: Array<iEntity>;
    loadMap(map: string): void;
    updateDrawMap(): void;
    reset(): void;
}

The cBrickMap class will not implement anything extra. So let's start with the updateDrawMap and reset functions as they are the easiest. The updateDrawMap will just do that, it will go thru each brick and call the update and draw functions. It will also check if there are any bricks left to smash. If there is no more to smash, it will load up the next level. The reset function will just set the number of bricks back to 0 and clear out the brickArray.

public updateDrawMap = (): void => {
    var entity: iEntity;
    for(var b: number = 0; b < this.brickArray.length; b++) {
        entity = this.brickArray[b];
        entity.update();
        entity.draw();
    }
    if(this.brickCount <= 0) {
        levelToLoad++;
        loadNewLevel();
    }
}

public reset = (): void => {
    this.brickArray.length = 0;
    this.brickCount = 0;
}

We will need to remove our function loadBricks function and replace it with the loadMap function of the cBrickMap. Since we are adding new types of bricks to the mix, let me run down the new characters we will be looking for. If there is an '!', the next brick will be an Unbreakable brick. If there is an '@', the next brick will be a Double brick. If there is a '#', the next brick will be a Triple brick. If the character is '-', then we don't want to load a brick at all. Since we are going to start loading levels, each level can have different size bricks, padding, spacing, etc. So instead of hard coding the start x/y position, the padding value, and the brick size, we will incorporate it into the string to be parsed. The new brickMap string will have 5 numbers, separated by a ',' and then the bricks like before.

public loadMap = (map: string): void => {
    var dataArray: Array<string> = map.split(',');

    var xpos: number = parseInt(dataArray[0]);
    var ypos: number = parseInt(dataArray[1]);
    var pad: number = parseInt(dataArray[2]);
    var bwidth: number = parseInt(dataArray[3]);
    var bheight: number = parseInt(dataArray[4]);

    var brickMap: string = dataArray[5];
    var brickType: BrickType = BrickType.Normal;
    var numUnbreakable: number = 0;
    for(var c: number = 0; c < brickMap.length; c++) {
        switch(brickMap[c]) {
            case 'R':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, 10, "red", brickType);
                xpos += bwidth + pad;
                this.brickArray.push(b);
                brickType = BrickType.Normal;
            }
            break;
            case 'B':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, 10, "blue", brickType);
                xpos += bwidth + pad;
                this.brickArray.push(b);
                brickType = BrickType.Normal;
            }
            break;
            case 'G':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, 10, "green", brickType);
                xpos += bwidth + pad;
                this.brickArray.push(b);
                brickType = BrickType.Normal;
            }
            break;
            case 'Y':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, 10, "yellow", brickType);
                xpos += bwidth + pad;
                this.brickArray.push(b);
                brickType = BrickType.Normal;
            }
            break;
            case 'W':
            {
                var b: cBrick = new cBrick(xpos, ypos, bwidth, bheight, 10, "white", brickType);
                xpos += bwidth + pad;
                this.brickArray.push(b);
                brickType = BrickType.Normal;
            }
            break;
            case '|':
            {
                xpos = 35;
                ypos += bheight + pad;
                brickType = BrickType.Normal;
            }
            break;
            // brick types come before the color of brick.
            case '!':
            {
                brickType = BrickType.Unbreakable;
                numUnbreakable++;
            }
            break;
            case '@':
            {
                brickType = BrickType.Double;
            }
            break;
            case '#':
            {
                brickType = BrickType.Triple;
            }
            break;
            case '-':
            {
                // empty space
                xpos += bwidth + pad;
                brickType = BrickType.Normal;
            }
            break;
        }
    }

    this.brickCount = this.brickArray.length - numUnbreakable;
}

Notice now we are passing a BrickType to the constructor of the cBrick class. We need to modify the cBrick class to take in this new parameter. So let's add a new property called brickType, and have it be the type of our enum we created above. We are also going to change the way a Double and Triple brick will be displayed. I found a nifty function that will take a hex color and darken it or lighten it by a percentage. For a Double and Triple, let's give it a darker stroke around the brick. In the update function, we need to check if it is an Unbreakable brick. If so, we don't want to award any points for hitting it, and we don't want to make it disappear. We do want to however, is if the ball hits a Triple brick to turn it to a Double, and if it hits a Double, turn it into a Normal. And of course, if it hits a Normal, we want to get rid of the brick.

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 brickType: BrickType = BrickType.Normal;
    public active: boolean = true;
    public points: number = 0;

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

    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();
            if(this.brickType == BrickType.Unbreakable) {
                ctx.strokeStyle = "#363636";
                ctx.lineWidth = 2;
                ctx.strokeRect(this.x,this.y,this.width, this.height);
            }else if(this.brickType == BrickType.Triple) {
                ctx.strokeStyle = shadeColor2(ctx.fillStyle, -0.6);
                ctx.lineWidth = 2;
                ctx.strokeRect(this.x,this.y,this.width, this.height);
            }else if(this.brickType == BrickType.Double) {
                ctx.strokeStyle = shadeColor2(ctx.fillStyle, -0.4);
                ctx.lineWidth = 2;
                ctx.strokeRect(this.x,this.y,this.width, this.height);
            }
            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;

                if(this.brickType != BrickType.Unbreakable) {
                    score.addPoints(this.points);
                }

                if(this.brickType == BrickType.Triple) {
                    this.brickType = BrickType.Double;
                }else if(this.brickType == BrickType.Double) {
                    this.brickType = BrickType.Normal;
                }else if(this.brickType == BrickType.Normal) {
                    this.active = false;
                    brickMap.brickCount--;
                }
            }
        }
    }

    public reset = (): void => {

    }
}

function shadeColor2(color, percent) {
 var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
 return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
}

While we are refactoring, let's create an Array of iHud objects so we can have a nice collection of hud items to manage. Inside the window.onload, we can push the score and lives into this new array. Also, since we have made the cBrickMap class handle all the brick functionality, we need to update the window.onload and gameLoop to load up the level as well as updating/drawing the brick map. We will also need to instantiate a new cBrickMap object as well as keep track on what level we are loading.

var hud_array: Array<iHud> = new Array<iHud>();
var brickMap: cBrickMap = new cBrickMap();
var levelToLoad: number = 0;

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

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

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

 brickMap.updateDrawMap();

 // draw the hud
 var hud: iHud;
 for(var h: number = 0; h < hud_array.length; h++) {
 hud = hud_array[h];
 hud.draw();
 }

 // draw a border around the canvas
 ctx.strokeStyle = "grey";
 ctx.lineWidth = 2;
 ctx.strokeRect(0, 0, canvas.width, canvas.height);
}

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);

    // setup initial values for the entities.
    ball.velX = 3;
    ball.velY = 3;
    paddle.velX = 5;

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

    hud_array.push(score);
    hud_array.push(lives);

    brickMap.loadMap(levels[levelToLoad]);

    // run the game!
    gameLoop();
}

The last few items to clean up is to create a loadNewLevel function that resets the ball, paddle, brickMap, and then loads up a new level. If there is no more levels to load, it will display a "You Win!" text. We also need to fix the resetGame function to change the level back to 0, and load up the map via the brickMap object.

function resetGame() {
    entity_array.length = 0;
    hud_array.length = 0;
    paddle.reset();
    ball.reset();
    score.reset();
    lives.reset();
    brickMap.reset();
    entity_array.push(ball);
    entity_array.push(paddle);

    hud_array.push(score);
    hud_array.push(lives);

    levelToLoad = 0;

    brickMap.loadMap(levels[levelToLoad]);
}

function loadNewLevel() {
    paddle.reset();
    ball.reset();
    brickMap.reset();
    if(levelToLoad < levels.length) {
        brickMap.loadMap(levels[levelToLoad]);
    }else {
        ctx.font = "32px Arial";
        ctx.fillStyle = "white";
        ctx.fillText("You WIN!!!", canvas.width/2, canvas.height/2);
    }
}

There is just 1 last thing to do, we need the new levels. Instead of having the levels in a single string, we are going to house them in an array of strings. Each item in the array will be a string, that houses a single level. I have came up with 7 different levels, you can make your own or use these.

var levels: Array<string> = [   "35,35,5,25,10,RRRRRRRRRRRRRR|BBBBBBBBBBBBBB|GGGGGGGGGGGGGG|YYYYYYYYYYYYYY|WWWWWWWWWWWWWW|YYYYYYYYYYYYYY|GGGGGGGGGGGGGG|BBBBBBBBBBBBBB|RRRRRRRRRRRRRR",
                                "35,35,5,25,10,RRBBRRBBRRBBRR|BBRRBBRRBBRRBB|GGYYGGYYGGYYGG|YYGGYYGGYYGGYY|WWWWWWWWWWWWWW|YYGGYYGGYYGGYY|GGYYGGYYGGYYGG|BBRRBBRRBBRRBB|RRBBRRBBRRBBRR",
                                "35,35,5,25,10,YYYYYYYYYYYYYY|YY-!W!W-YY-!W!W-YY|YY-!W!W-YY-!W!W-YY|YYYYYYYYYYYYYY|YY--YYYYYY--YY|YYY--YYYY--YYY|YYYY--YY--YYYY|YYYYYY--YYYYYY|YYYYYYYYYYYYYY",
                                "35,35,5,25,10,@R@R@R@R@R@R@R@R@R@R@R@R@R@R|WWWWWWWWWWWWWW|YYYGGGYYYGGGYY|GGYYYYGGGYYYGG|BBBBBBBBBBBBBB|GGYYYYGGGYYYGG|YYYGGGYYYGGGYY|WWWWWWWWWWWWWW|@R@R@R@R@R@R@R@R@R@R@R@R@R@R",
                                "35,35,5,25,10,#B@B#B@B#B@B#B@B#B@B#B@B#B@B|RRGGRRGGRRGGRR|GGYYGGYYGGYYGG|YYWWYYWWYYWWYY|!R!RRRRR--RRRR!R!R|YYWWYYWWYYWWYY|GGYYGGYYGGYYGG|RRGGRRGGRRGGRR|WWW@W@WWWW@W@WWWWW",
                                "35,35,5,25,10,GGWWGGWWGGWWGG|RRGGRRGGRRGGRR|BBRRBBRRBBRRBB|!B!B!B!B!B!B!B!B!B!B!B!B!B!B|--------------|!B!B!B!B!B!B!B!B!B!B!B!B!B!B|RRBBRRBBRRBBRR|BBGGBBGGBBGGBB|GGYYGGYYGGYYGG",
                                "35,35,5,25,10,#R#B#G#Y#W#R#B#G#Y#W#R#B#G#Y|#W#R#B#G#Y#W#R#B#G#Y#W#R#B#G|#Y#W#R#B#G#Y#W#R#B#G#Y#W#R#B|#G#Y#W#R#B#G#Y#W#R#B#G#Y#W#R|#B#G#Y#W#R#B#G#Y#W#R#B#G#Y#W|#R#B#G#Y#W#R#B#G#Y#W#R#B#G#Y|#W#R#B#G#Y#W#R#B#G#Y#W#R#B#G|#Y#W#R#B#G#Y#W#R#B#G#Y#W#R#B|#G#Y#W#R#B#G#Y#W#R#B#G#Y#W#R" ];

And that concludes this tutorial series. Want to make this better? Try adding some juice to the game. You can find the full game's source in my GitHub repo. Stay tuned for more Adventures in TypeScript...

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