Quantcast
Channel: YouChew Community Blog List
Viewing all articles
Browse latest Browse all 480

tabull's Blog - It's A Monster House!

$
0
0
Sometimes you get a dumb idea in your head and if you have the ability to do whatever you want, the only solution is to code it and display it for all to see. I guess that's the main gist of how easter eggs work, if not that's what it's going to be for me. So, that's what I did a few weeks ago and added it right on our very own site, the one you are actually perusing right now. This special bit of code activates a monster house, but for those who don't know what a monster house is, should watch this nifty video.
 


Wait, no, that's not it, it's from a game.
 


Sorry, that's not it either. It's on one of them handheld type devices and it's not actually about a house.
 

There we go. Go into a random room, have it rain pokemon and change the music to fit the occasion.  The number of pokemon that enter is usually a lot more than you fight at a time and is usually quite frustrating, especially if you don't have any attacks that damage all the enemies at once and you have a partner that runs away every time it gets tapped on the shoulder, but I digress.

But why would I get the idea to recreate this here? The first part was that I learning some HTML5 and two of these new things are the canvas and native audio (that is, you don't have to download quicktime or whatever to play music on websites). The second part was that I was listening to the Gates to Infinity soundtrack and the monster house music played while I was reading up on all this. The natural response to this, of course, is to simulate one of these on the YouChew dot net forums.

I was able to do this completely with Javascript, so the only HTML I needed to add was the reference to the script and let it do its magic. The script can be found here but I'll go through a few key parts of how this was done. This was my first ever attempt at using the canvas element, so it's a bit scattered everywhere. A little background about the canvas element. It's essentially a basic drawing application that you can use to create drawings programmatically. It's all raster-based, meaning it doesn't know it's previous state and you just add on top of the drawing.

My basic thought process was to grab a random pokemon, have it drop to a spot on the page, start it's idle animation, and the repeat it for how ever many pokemon I felt like adding. So, of course the first part was to create a sprite sheet, there's no way I would create each individual sprite and swap images to make it look like it's animating. That's way too many resources for the browser to download, especially since most browsers only download at most 3 files from a single site at a time. In the end, I created three sprite sheets: One where each sprite is 32x32 and has 2 frames, one where each sprite is 32x32 and has 3 frames, and one where each sprite is 24x24 and has 2 frames. These were the final sprite sheets I put together to work with the script.
 




There would have been a bigger variety, but with each pokemon having 8 directions and the sprite sheets I used didn't have them in a nice pattern, I had to move each one manually into position. It actually took me longer to create these images than it did to write the entire script.

So, now that I got the images ready and uploaded, the first thing to do is load these images, which is where this SpriteSheet "class" comes into play
function SpriteSheet(source, dimension, frames, idleAnimation)
{
	this.source = source;
	this.dimension = dimension;
	this.frames = frames;
	this.img = null;
	this.rows = null;
	this.idleAnimation = idleAnimation;
}

SpriteSheet.prototype.load = function ()
{
	this.img = new Image();
	this.img.src = this.source;
	this.img.onload = this.loaded();
}
	
SpriteSheet.prototype.loaded = function()
{
	this.rows = this.img.height / this.dimension;
	spriteSheets.push(this);
}
This will allow me to add my spritesheets to an array and I'll know it's ready due to the onload method firing when the image has loaded, however there were some issues with this alone, so I added another check later on. The idleAnimation variable is actually an array, and I use that determine which frame to use as I thought it looked better when each frame had a different duration.

Now that I got my SpriteSheet "class" ready, I've got everything I need to set up the entire thing, which is where this bit of code falls into play, which executes as soon at the webpage is ready.
function prepareMonsterHouse()
{
	if(!canHTML5())
		return;

	spriteSheets = new Array();
	sprites = new Array();

	
	audio = new Audio(mediaLocation + 'mh.mp3');
	audio.loop = true;
	audio.preload = true;
	
	var sprite = new SpriteSheet(mediaLocation + '24x2.png', 24, 2, [0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1]);
	sprite.load();
	expectedSheetCount++;
	
	sprite = new SpriteSheet(mediaLocation + '32x3.png', 32, 3, [0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,2,2,2,2,2,1,1,1,1]);
	sprite.load();
	expectedSheetCount++;
	
	sprite = new SpriteSheet(mediaLocation + '32x2.png', 32, 2, [0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1]);
	sprite.load();
	expectedSheetCount++;
	
	waitForImages();
	
}
The canHTML() function is just a check to make sure the browser you are using can handle the canvas and audio tag. If not, then there's no reason for the user to see this and nothing happens. Otherwise, it loads the music and each sprite sheet along with some useful information, such as the dimension of each sprite and how many frames of animation each sprite has, so I can easily calculate where on the spritesheet the sprite lives. The waitForImages function was made since the image onload function didn't work entirely as I had hoped and would sometimes wouldn't be loaded and instead the music would play but no raining pokemon...which is the entire point of this thing.
function waitForImages()
{
	var loaded = true;
	for(var i = 0; i < spriteSheets.length; i++)
	{
		if(spriteSheets[i].rows === 0)
		{
			loaded = false;
		}
		
		if(spriteSheets[i].img.height > 0)
			spriteSheets[i].rows = spriteSheets[i].img.height / spriteSheets[i].dimension;
	}

	if(spriteSheets.length == expectedSheetCount && loaded)
		placeGummiImage();
	else
		setTimeout(waitForImages, 100);
	
}
Since the number of rows is calculated when the images is loaded, I used that to check if there's more than zero rows and if not, try to calculate it from the image height and dimension of each sprite. If the image isn't fully loaded the height will be zero and I'll have to check again at tenth of a second later. If everything is loaded, it's time to place the gummi image on the top right of the page.
function placeGummiImage()
{
	if(!document.getElementById('monsterHouse'))
	{
		var div = document.createElement('div');
		div.id = 'monsterHouse';
		div.style.position = 'fixed';
		div.style.top = '0px';
		div.style.right = '0px';
		
		var canvasElement = document.createElement('canvas');
		canvasElement.id = 'gameCanvas';
		canvasElement.style.height = '0px';
		canvasElement.style.width = '0px';
		
		var imgElement = document.createElement('img');
		imgElement.id = 'monsterHouseGummi';
		imgElement.onclick = startMonsterHouse;
		imgElement.src = mediaLocation + 'gummi.png';
		imgElement.style.styleFloat = "right";
		imgElement.style.cssFloat = "right";

		div.appendChild(imgElement);
		div.appendChild(canvasElement);
		
		document.body.appendChild(div);
	}
}
This is just your basic DOM manipulation, or if you want to think of it as "playing around with the HTML of the page...but not really." Right here, I'm creating the canvas element and image element for the gummi, plus giving a click event on the image to call the startMonsterHouse() function, which is where all the magic begins.
function startMonsterHouse()
{
	var elem = document.getElementById('monsterHouseGummi');
	elem.parentNode.removeChild(elem);
	
	elem = document.getElementById('monsterHouse');
	elem.style.height='100%';
	elem.style.width='100%';
	
	audio.play();
	canvas = document.getElementById('gameCanvas');
	ctx = canvas.getContext("2d");
	canvas.style.width = null;
	canvas.style.height = null;
	canvas.width = canvas.parentNode.clientWidth;
	canvas.height = canvas.parentNode.clientHeight;
	
	createPossibleSpriteLocations();
			
	createSprite();
	
	updateSprites();
}
Here, I'm removing the gummi image since it's not needed anymore and setting the canvas to be the size of the entire browser. I won't go into the code of what createPossibleSpriteLocations() does, but it subdivides the screen into 32x32 blocks and places them all into an array that I can randomly pick and then remove so another sprite doesn't choose that location. Before I go into the createSprite() function, there's another "class" I need, one of each individual sprite that will appear on the screen.
function SpriteImage(spriteIndex, rowIndex, directionIndex, x, y)
{
	this.frame = 0;
	this.spriteIndex = spriteIndex;
	this.rowIndex = rowIndex;
	this.directionIndex = directionIndex;
	this.isDroping = true;
	this.x = x;
	this.y = y;
	this.fallingY = 0;
}

SpriteImage.prototype.redraw = function()
{
	var dimension = spriteSheets[this.spriteIndex].dimension;
	var frames = spriteSheets[this.spriteIndex].frames;

	if(this.isDroping)
	{
		//update y;
		this.fallingY += 50;
		if(this.fallingY < this.y)
		{
			ctx.drawImage(spriteSheets[this.spriteIndex].img, dimension * frames * this.directionIndex, dimension * this.rowIndex, dimension, dimension,
							this.x, this.fallingY, dimension, dimension);
			return;
		}
		
		this.isDroping = false;
		dropping--;
		
	}
	
	if(!this.isDroping)
	{
		ctx.drawImage(spriteSheets[this.spriteIndex].img, dimension * (frames * this.directionIndex + spriteSheets[this.spriteIndex].idleAnimation[this.frame]), dimension * this.rowIndex, dimension, dimension,
							this.x, this.y, dimension, dimension);
		this.frame++;
		if(this.frame >= spriteSheets[this.spriteIndex].idleAnimation.length)
			this.frame = 0;
	}
	
	
}
The redraw function is the main meat of this entire thing and it does two things: first it drops the sprite from the top of the screen to the location it's passed in, then it starts its idle animation. Every sprite starts as falling, so to give the illusion that it's falling, the y value of the sprite increases 50 pixels each frame and then draws the first frame of the sprite's idle animation. The canvas context function drawImage allows me to draw one of the sprites since I can choose which part of the image to actually draw. The first argument is the image to draw, followed by the x and y positions value of the image to draw and the height and width. After that I pass in the x and y value to place on the browser along with height and width of the image, which is just the dimension again.

Once the image has gone past the location it will be staying, the isDropping flag is unset and moves to start it's idle animation cycle, which calls the same drawImage function, but uses some additional logic to pick the other frames of the sprite rather than just the first one.

Ok, so now that we have logic put in place to draw a sprite, we need to create a sprite, duh.
function createSprite()
{
	if(possibleSpriteLocations.length === 0)
		return;
		
	if(spriteSheets.length === 0)
		return;
		
	var maxX = canvas.width / 32;
	var maxY = canvas.height / 32;

	var index = Math.floor(Math.random() * possibleSpriteLocations.length);
	
	var x = possibleSpriteLocations[index][0];
	var y = possibleSpriteLocations[index][1];
	
	possibleSpriteLocations.splice(index, 1);
	
	var direction = Math.floor(Math.random() * 8);
	var arr = chooseSprite();
	var spriteIndex = arr[0];
	var rowIndex = arr[1];
	
	sprites.push(new SpriteImage(spriteIndex, rowIndex, direction, x, y));
	dropping++;
}
I won't go too much into detail with this, but it's taking a look at which of the possible sprite locations are available and randomly chooses of them. Then it picks one of the eight directions the sprite could be facing. After all that it picks a random sprite, which I won't go into as it's all math, and creates a SpriteImage with all this information and adds it to a list of sprites as well as increment the number of sprites that are dropping. I use that to make sure there aren't too many sprites dropping at once.

And now the final piece, the update loop that will continue on for all eternity (or you close the tab/browser)
var lastUpdateTime = 0;
var acDelta = 0;
var msPerFrame = 30;


function updateSprites() {
	requestAnimFrame(updateSprites);

	var delta = Date.now() - lastUpdateTime;
	if (acDelta > msPerFrame)
	{
		acDelta = 0;
		ctx.clearRect(0,0, canvas.width, canvas.height);
		for(var i = 0; i < sprites.length; i++)
			sprites[i].redraw();
			
		if(sprites.length < maxSprites && dropping < 5 && possibleSpriteLocations.length > 0)
			createSprite();
	} else
	{
		acDelta += delta;
	}

	lastUpdateTime = Date.now();
}
There's some logic in here that makes it so each frame of animation is drawn in around the same time frame. But the main thing about this is the for loop that goes through all the sprites and calls the redraw method I mentioned back. It also clears the canvas before this so the previous frame is wiped out. After that, it checks to see that the max number of sprites the draw hasn't been exceeded (I gave that a value of 1000), the number of dropping sprites hasn't exceeded 5 and there are other places to a new sprite. If that passes, a new sprite is created and starts dropping.
 
And that's all there is to it, but now comes the question of when should this go?  I originally was going to have it be a 1 in some odd number chance of happening randomly on any page, but that causes a couple problems. First it plays music and autoplaying music is never a good idea.  The second problem is that it's pokemon related, and we can't have that happening anywhere or people will throw an even bigger fit than if music was playing.  So, with that in mind, I decided it should be something the user starts, which is where the gummi image comes into play since at least in the 2nd game (never played the 1st) if you see a bunch of gummis in a room, a monster house is usually there.  I also decided that it should only happen in the pokemon related threads since it's, surprise, pokemon related.  So, I created a hook to look at the topic title and if "pokemon" or "pokémon" is in there, then add the reference to the script.  I also added it to this blog entry so go ahead and click the image on the top right of the site and see it in action.
 
This was a pretty fun easter egg to make and I was pleased with the positive reaction it got, so maybe there will be other kinds of easter eggs throughout the site at some point in the future.

Viewing all articles
Browse latest Browse all 480

Trending Articles