Full source code can be downloaded from project home at kjeldahlnilsson.net.
Part 1, 2, 3, 4, 5, 6, 7, 8
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;
},

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!