26.01.2009

Javascript Tetris Pt 6: Lights, Action, Music!

Full source code can be downloaded from project home at kjeldahlnilsson.net.

The game looks very prototype-ish right now. We’re not shooting for blockbuster level presentation here, but we should at least provide a bare minimum of animation and audio feedback. Let’s do something about that by adding animation, making the playing field a little more interesting, plus some support for sound effects and music.

First off; some sort animation payoff when the player clears one or more rows. We want a sort of stylized explosion to occur. Let’s create that separately first. We’ll achieve this by applying a JQuery UI effect to a div while hiding it.

test.js:

    testExplodeAnimation: function() {
        Graphics.clearGameContainer();
        var square = Graphics.createRectangleDiv("black", 400, 400, 100, 100);
        $(square).hide("explode", {},
        1000);
    }

This works, and looks cool. Now we’ll finish the Field.doRowClears() method from yesterday, including some animation when we remove the filled rows in the field.

field.js:

    doRowClears: function() {
        var rowsToClear = [];

        this.State.gridState.eachRowWithIndex(function(row, rowNumber) {
            var entireRowFilled = true;

            // Is row filled?
            for (var tile in row) {
                if (row[tile] === 0) {
                    entireRowFilled = false;
                    break;
                }
            }

            // Set row to be cleared
            if (entireRowFilled) {
                rowsToClear.push(rowNumber);
            }
        });

        if (rowsToClear.length > 0) {
            this.explodeAndClearRows(rowsToClear);
        }
    },

    explodeAndClearRows: function(rowsToExplode) {

        // Set up big animation rectangle to cover the disappearing rows
        var topRow = 1000;
        var bottomRow = 0;
        for (var row in rowsToExplode) {
            if (rowsToExplode.hasOwnProperty(row)) {
                if (topRow > rowsToExplode[row]) {
                    topRow = rowsToExplode[row];
                }
                if (bottomRow < rowsToExplode[row]) {
                    bottomRow = rowsToExplode[row];
                }
            }
        }

        var rowsTotalTopY = this.State.posY + (topRow * Piece.State.tileHeight);
        var rowsTotalBottomY = this.State.posY + (bottomRow * Piece.State.tileHeight) + Piece.State.tileHeight;
        var rowsTotalHeight = rowsTotalBottomY - rowsTotalTopY;

        var explodingRect = Graphics.createRectangleDiv("#C0ADFF", this.State.posX, rowsTotalTopY, Piece.State.tileWidth * this.WIDTH, rowsTotalHeight, 10);

        // Clear the actual tiles in grid before animating the large rectangle
        for (row in rowsToExplode) {
            if (rowsToExplode.hasOwnProperty(row)) {
                this.clearRow(rowsToExplode[row]);
            }
        }

        // Use JQuery UI effect to "explode" the big rectangle
        $(explodingRect).hide("explode", {},
        1500);

        // Clean up
        Graphics.removeNodeFromGameContainer(explodingRect);

        // Shuffle remaining higher tiles downwards in field
        for (row in rowsToExplode) {
            if (rowsToExplode.hasOwnProperty(row)) {
                this.shiftTilesDownToRowX(rowsToExplode[row]);
            }
        }
    },

    clearRow: function(rowNo) {
        for (x = 0; x < this.State.gridState.length; x++) {
            this.tileOff(x, rowNo);
        }
    },

    shiftTilesDownToRowX: function(clearedRowY) {
        for (var y = (clearedRowY - 1); y >= 0; y--) { // Start at bottom to cascade tiles
            for (var x = 0; x < this.State.gridState.length; x++) {
                // Shuffle tile state down
                if (this.isTileOn(x, y)) {
                    this.tileOff(x, y);
                    this.tileOn(x, y + 1);
                }
                else {
                    this.tileOff(x, y + 1);
                }
            }
        }

    }

We also want to improve the appearance of the playing field somewhat; solid magenta is handy for raw testing but not exactly visually appealing in a finished game. Let’s add a background graphic by updating Graphics.createFieldBackground():

graphics.js:

    createFieldBackground: function(bgcolor, x, y, width, height) {
        var rect = document.createElement('div');

        rect.style.position = "absolute";
        rect.style.top = y + "px";
        rect.style.left = x + "px";
        rect.style.zIndex = "-1";
        rect.style.height = height + "px";
        rect.style.width = width + "px";
        rect.style.backgroundColor = bgcolor;

        rect.style.backgroundImage = "url('assets/images/gameplayScreen.png')";
        rect.style.backgroundRepeat = "no-repeat";
        rect.style.backgroundPosition = "0px 0px";

        this.getGameContainer().appendChild(rect);

        return rect;
    },

collisiontestUpdatedBackground

Much better. The background picture is simply an image pulled from a random “mountains” search on Flickr, then cropped and tweaked a bit in Paint.Net.

We’ll wrap up todays installment by adding support for sound and music. I found an elegant little library called Soundmanager. Soundmanager creates and wraps a hidden Flash component in the page, enabling us to seamlessly load and play mp3 files (or other media):

test.js:

    testPlaySound: function() {
	Sound.playLandingSound();
    },

    testPlaySoundLooped: function() {
	Sound.playLoopedLandingSound();
    },

    testPlayMusic: function() {
	Sound.playAmbientMusic();
    },

We then create our Sound object, wrapping the functionality we need from SoundManager to load and play audio.

sound.js:

var Sound = {

    SoundBank: {
        rotation: "rotation",
        landing: "landing",

        clearedOneRow: "clearedOneRow",
        clearedTwoRows: "clearedTwoRows",
        clearedThreeRows: "clearedThreeRows",
        clearedFourRows: "clearedFourRows",

        ambientMusic: "ambientMusic"
    },

    loadSounds: function() {
        soundManager.createSound(this.SoundBank.rotation, 'assets/sound/rotation.mp3');
        soundManager.createSound(this.SoundBank.landing, 'assets/sound/landing.mp3');
        soundManager.createSound(this.SoundBank.clearedOneRow, 'assets/sound/clearedOneRow.mp3');
        soundManager.createSound(this.SoundBank.clearedTwoRows, 'assets/sound/clearedTwoRows.mp3');
        soundManager.createSound(this.SoundBank.clearedThreeRows, 'assets/sound/clearedThreeRows.mp3');
        soundManager.createSound(this.SoundBank.clearedFourRows, 'assets/sound/clearedFourRows.mp3');
        soundManager.createSound(this.SoundBank.ambientMusic, 'assets/sound/ambientMusic.mp3');
    },

    playAmbientMusic: function() {
        playLooped(this.SoundBank.ambientMusic);
    },

    playRotationSound: function() {
        soundManager.play(this.SoundBank.rotation);
    },

    playLandingSound: function() {
        soundManager.play(this.SoundBank.landing);
    },

    playLoopedLandingSound: function() {
        playLooped(this.SoundBank.landing);
    },

    playClearedSound: function(level) {
        if (!level || level < 4 || level > 1) {
            return;
        }

        switch (level) {
        case 1:
            soundManager.play(this.SoundBank.clearedOneRow);
            break;
        case 2:
            soundManager.play(this.SoundBank.clearedTwoRows);
            break;
        case 3:
            soundManager.play(this.SoundBank.clearedThreeRows);
            break;
        case 4:

            soundManager.play(this.SoundBank.clearedFourRows);
            break;
        }
    },

};

// Looping sound support
playLooped: function playLooped(soundID) {
    window.setTimeout(function() {
        soundManager.play(soundID, {
            onfinish: function() {
                playLooped(soundID);
            }
        });
    },
    1);
};

soundManager.onload = function() {
    Sound.loadSounds();
}

Sound effects and music can now be launched from the rest of the game logic.

We have most of what we need now; in the next installment we glue it all together to create a playable game!


PreviousNext