Welcome to part 2 of my Crafty.js How To. On the last post I started building a quick game that I made up using Crafty.js, a JavaScript-only game engine that's free to use, and that you can download from right here. It's a great framework for anyone new to game development because all you need is notepad and a browser and you're all set to start. In this post, I'll finish up that game level, with the full source at the end.
A quick recap on Part 1
The idea of the "game" was to have a single frame with a single block as our main character where it is raining at a particular speed. And the job of our character is to make it to the end without getting hit by a set number of rain drops. Our character can move left and right and jump in order to avoid the rain. When they are hit the set number of times, the counter is reset and they get transported to the beginning again. The character wins once they reach the end of the frame. It will look something like this.
It's a small game level, but it should cover some key topics, such as movement, gravity and collisions. I added some variables to the project since the first post, that will come in handy.
var screenWidth = 800;
var screenHeight = 400;
var hitCounter = 0;
Making It Rain
This is where things get a bit tricky, but fun. The rain drops are just going to be another entity with gravity set so that they fall all the way down no matter what, which means that we don't need to set any components in the gravity method to react to. We want them to go from the top of the page to the bottom. They'll be represented by a 2 x 15 blue block that will fall at the default gravity constant, giving the effect of rain.
Let's start with a single rain drop for now, to simplify things. We'll set it's starting y point at the very top of the canvas, and we'll give it a random x coordinate that's within the boundaries of the game canvas.
function drop()
{
var random_x = Math.floor((Math.random() * screenWidth) + 50);
Crafty.e('Drop, 2D, Canvas, Color, Solid, Gravity')
.attr({x: random_x, y: 0, w:2, h: 15})
.color('#000080')
.gravity();
}
If you're not familiar with entities in Crafty, then head on over to part 1 for a quick recap.
This function will generate one drop every time it is called. So we'll need to generate it continuously every couple of frames. By default Crafty runs 50 frames per second and we can make use of that because each time it enters a frame it calls the EnterFrame method. So let's make a drop fall every frame, which is alot. At 50 rain drops per second, my character is going to have a bad time, but we'll adjust that a little later down below.
Binding and Frames
Entities have another method called bind which allows us to bind a global function with that entity, so let's bind the EnterFrame function, and call our drop() method inside of that function. And again, all of this is provided to us by Crafty.js, we just have put it together and worry about our own custom code.
Crafty.bind("EnterFrame", function(){
drop();
});
We should now have a pouring rain scene. It might be moving a bit too fast though, so we can slow things down a bit by only calling the drop() method every other frame, or 25 times per second.
Crafty.bind("EnterFrame", function(){
if (Crafty.frame() % 2 == 0)
drop();
});
Notice that Crafty.frame() will return the current frame that the game is on, which does come in handy if our game is dependent on frames.
Now Destroy Those Drops
Because we're creating a drop every frame, that's 50 new entities per second that are being created on the page. Ouch. Because these entities are actual objects in JavaScript and not just virtual images that disappear unfortunately. That will very quickly bring your game to a halt, as I learned the hard way. So once our drops hit the very bottom of our game canvas, they are no longer needed. Lucky for us Crafty.js has a destroy() method that will take care of the work for us.
In order to do this, we'll have to bind our drop entities to the EnterFrame method and on each frame check it's position. Once it's y value is greater than the game boards, we can call destroy on it. I'm going to add some padding between the start of the drops and the character so that he has some buffer space in the beginning, otherwise he would be generated into the rain and lose automatically. That's what the +50 is for down below.
function drop()
{
var randomx = Math.floor((Math.random() * screenWidth) + 50);
Crafty.e('Drop, 2D, Canvas, Color, Solid, Gravity')
.attr({x: randomx, y: 0, w: 2, h: 15})
.color('#000080')
.gravity()
.gravityConst(.5)
.bind("EnterFrame", function() {
if (this.y > screenHeight)
this.destroy();
});
}
That should do it. There should now be a lonely block character in a scene of pouring rain. Next up, let's make the rain collide with the character and make a counter increment each time.
Collision Detection
More components. This is the best part about using Crafty.js. Any new functionality that you need to add will usually resolve into adding more components to your entities. For this particular case, we add the Collision component, which will make our entities fire off an event whenever it collides with another entity.
function drop()
{
var randomx = Math.floor((Math.random() * screenWidth) + 50);
Crafty.e('Drop, 2D, Canvas, Color, Solid, Gravity, Collision')
.attr({x: randomx, y: 0, w: 2, h: 15})
.color('#000080')
.gravity()
.gravityConst(.5)
.checkHits('Player')
.bind("EnterFrame", function() {
if (this.y > screenHeight)
this.destroy();
})
.bind("HitOn", function(){
});
}
First things first, you need the Collision component to make collision work for this entity. One of the methods associated with this component is checkHits which takes as a parameter the Component that you want to detect hits on. In this particular case we want to detect when any drop hits our Player character. And up next after that, we bind the HitOn method to our drop, which fires whenever the entity hits the corresponding Component that we specified. Whew. That was alot.
But it still wasn't too bad. Just 3 things and we have collision detection. What we do with this collision is up to us. For the purposes of this game, there are a few things that need to happen when a rain drop hits the character. First off, the rain drop needs to destroy itself, and secondly, the hit counter needs to increment.
function drop()
{
var randomx = Math.floor((Math.random() * screenWidth) + 50);
Crafty.e('Drop, 2D, Canvas, Color, Solid, Gravity, Collision')
.attr({x: randomx, y: 0, w: 2, h: 15})
.color('#000080')
.gravity()
.gravityConst(.5)
.checkHits('Player')
.bind("EnterFrame", function() {
if (this.y > screenHeight)
this.destroy();
})
.bind("HitOn", function(){
this.destroy();
hitCounter++;
if (hitCounter == 3)
{
player1.x = 20;
hitCounter = 0;
}
});
}
How To Win
The level ends when the user makes it to the end of the map. If at any point they get hit a set number of times, then they will get transported to the beginning of the map. So for this we'll bind another EnterFrame event to our player, and detect its position on each frame. If it matches the size of the canvas, then it is a win.
var player1 = Crafty.e('Player, 2D, Canvas, Color, Solid, Fourway, Gravity, Collision')
.attr({x: 20, y: 0, w: 30, h: 30})
.color('#F00')
.fourway(6)
.gravity('Floor')
.gravityConst(1)
.bind("EnterFrame", function(){
if (this.x == screenWidth)
{
pause();
Crafty.e('2D, DOM, Text').attr({x:screenWidth/2, y:screenHeight/2}).text("Stage 1 Clear").textFont({size:'20px', weight:'bold'});
}
});
In Conclusion
This is a very simple level I made up in an hour as I tried to learn Crafty.js, and I think it covers the basic topics pretty well. At least well enough to get a beginner up and running as fast as possible. So copy and paste the full source below, and start playing around with it and see what else you can come up with.
Updates: 5/6/2016
Just a quick update, since it looks like the original code that I wrote when this post went up was no longer working. More than likely the Crafty.js library has been updated a bit, and since I am pointing to the latest released version, that came with some issues. I noticed 2 problems occurring.
1. First off, it looks like the gravityConst function that I was adding to the raindrops was causing the drops to fall at an insanely slow pace. So what ended up happening was that since they pretty much never reached the floor, they weren't getting destroyed in memory, causing a high resource usage problem. So I removed that method, and all is well again.
2. Secondly, the player block was moving at an alarmingly slow pace, which was due to a change in the players fourway method. Which now takes the number pixels to move per frame. So I bumped that up to about 200 pixels per frame, and now it's moving at a more normal pace.
Something to watch out for, for user. It's probably safer to pick a version of Crafty and to stick with that and update slowly, as oppose to always pointing to the latest builds, which can and will cause issues eventually.
full source
<html>
<head>
<style>
#game
{
border:solid 1px black;
border-radius:8px;
}
</style>
</head>
<body>
<div id="game" style="margin:0 auto;"></div>
<script type="text/javascript" src="https://rawgithub.com/craftyjs/Crafty/release/dist/crafty-min.js"></script>
<script>
var screenWidth = 800;
var screenHeight = 400;
var hitCounter = 0;
Crafty.init(screenWidth,screenHeight, document.getElementById('game'));
Crafty.e('Floor, 2D, Canvas, Solid, Color')
.attr({x: 0, y: 350, w: screenWidth * 2, h: 10})
.color('black');
var player1 = Crafty.e('Player, 2D, Canvas, Color, Solid, Fourway, Gravity, Collision')
.attr({x: 20, y: 0, w: 30, h: 30})
.color('#F00')
.fourway(150)
.gravity('Floor')
.bind("EnterFrame", function(){
if (this.x == screenWidth)
{
pause();
Crafty.e('2D, DOM, Text').attr({x:screenWidth/2, y:screenHeight/2}).text("Stage 1 Clear").textFont({size:'20px', weight:'bold'});
}
});
var hitText = Crafty.e('2D, DOM, Text')
.attr({
x: screenWidth - 100,
y: 10
});
hitText.text('Hit:' + hitCounter);
hitText.textFont({
size: '30px',
weight: 'bold'
});
function drop()
{
var randomx = Math.floor((Math.random() * screenWidth) + 50);
Crafty.e('Drop, 2D, Canvas, Color, Solid, Gravity, Collision')
.attr({x: randomx, y: 0, w: 2, h: 15})
.color('#000080')
.gravity()
.checkHits('Player')
.bind("HitOn", function(){
this.destroy();
hitCounter++;
hitText.text("Hit: " + hitCounter);
if (hitCounter == 5)
{
player1.x = 20;
hitCounter = 0;
hitText.text("Hit: " + hitCounter);
}
})
.bind("EnterFrame", function() {
if (this.y > screenHeight)
this.destroy();
});
}
function pause()
{
Crafty.pause();
}
Crafty.bind("EnterFrame", function(){
document.getElementById("message").innerHTML = Crafty.frame();
if (Crafty.frame() % 2 == 0)
drop();
});
</script>
<input type="button" value="pause" onclick="pause()" />
<div id="message"></div>
</body>
</html>