chillJS - tutorials: let's create flappy bird in 15 minutes!

This tutorial is part of the chillJS GitHub repository

Setting up

First thing is to prepare our application. For better understanding we will separate our game into more files. Here is how the structure should look like:

Now with your favourite text-editor, open the index.html file, and write the following. This is a basic HTML document, which loads the required files.

<!DOCTYPE html>
<html>
<head>
	<title>Flappy bird - chillJS application</title>
	<meta charset="utf-8" />
	<link rel="stylesheet" href="chill/chill.css" />
	<link rel="stylesheet" href="styles/flappy-bird.css" />
</head>
<body>
	<div id="wrapper"></div>
	
	<script src="chill/chill.min.js"></script>
	<script src="scripts/flappy-bird.js"></script>
</body>
</html>

Add some style

Place the game in the middle of the screen with this little css code (styles/flappy-bird.css)

#wrapper {
	width: 320px;
	height: 480px;
	margin: auto;
	position: absolute;
	top: 0;
	right: 0;
	bottom: 0;
	left: 0;
	border: 1px solid #000;
}

Configure the app

In this step we define some initial data, add elements to the Scene and load images. We want it in the chill apps/flappy-bird.json file, because there is no logic, just some initialization options. There are elements, which we want to add to the Scene several times, so we should create abstract elements. This means there is an order in the steps, thus we need to organize the the configurator object like this:

{
	"include": [
		{}, // first steps here
		{}
	]
}

The array ensures the order, unlike the object, so we will call the Scene.include method two times. Now we can add the config:

{ "include": [
	{
		"screen": {
			"width": 320,
			"height": 480
		},
		
		"preload": [["./", {
			"Image": ["@images/flappy-bird.png as sheet"]
		}]],
		
		"createAbstractElement": [
			["bg", "Image", {
				"src": "#sheet",
				"y": "100%",
				"offsetY": "-100%",
				"width": 276,
				"height": 228,
				"sourceWidth": 276,
				"sourceHeight": 228
			}],
			["fg", "Image", {
				"src": "#sheet",
				"y": "100%",
				"offsetY": "-100%",
				"width": 224,
				"height": 112,
				"vX": -2,
				"sourceX": 276,
				"sourceWidth": 224,
				"sourceHeight": 112
			}],
			["pipe", "Image", {
				"src": "#sheet",
				"width": 52,
				"height": 200,
				"x": 320,
				"vX": -2,
				"sourceWidth": 52,
				"sourceHeight": 400
			}]
		]
	},
	
	{
		"insertLayer": [{
			"id": "main",
			
			"background": "#70C5CF",
			
			"insert": [
				["#bg"],
				["#bg", { "x": 276 }],
				
				["Container", "pipes"],
				
				["#fg", {
					"id": "fg1",
					"minX": -14,
					"maxX": 0
				}],
				
				["#fg", {
					"x": 224,
					"minX": 210,
					"maxX": 224
				}],
				
				["SpriteSheet", {
					"id": "bird",
					"src": "#sheet",
					"x": 50,
					"y": 200,
					"vY": -5,
					"angle": -15,
					"sourceX": 312,
					"sourceY": 230,
					"frameWidth": 34,
					"frameHeight": 24,
					"frameRate": 14,
					"frames": [{ "x": 312, "y": 230 }, { "x": 312, "y": 256 }, { "x": 312, "y": 282 }],
					"addAnimation": [["default", { "frames": [0, 1, 2] } ]],
					"set": [
						["gravity", 0.25],
						["jumpSpeed", 4.6]
					]
				}]
			]
		}]
	}
]}

There is nothing interesting here. In the first step, we set the screen width, load our only image and create some AbstractElement. In the second one, we add a Layer and all elements except the pipes. We will store the pipes in an other ContainerElement, so we can access them easier.

Implement the logic

Let's write some real code. Call Chill.out and provide our json app. Also specify which HTML element should it place in.

Chill.out(new Chill.App('./chill apps/flappy-bird.json'), 'wrapper', function(scene) {
	// code here
});

Now we should access the added elements:

var layer = scene.getLayer('main');
var pipes = layer.getElementByID('pipes');
var bird = layer.getElementByID('bird');
var score = layer.getElementByID('score');
var fg = layer.getElementByID('fg1');

Also create a collision detector:

var detector = scene.watch(bird, []);

We don't want to start the Scene until the image(s) are loaded:

scene.on('preloadComplete', scene.start);

This code listens on the "preloadComplete", and calls the Scene.start method, when it triggers. The elements should be updated, so we need to create an other listener, which fires on every tick:

scene.on('tick', function() {
	bird.vY += bird.get('gravity');
	
	if (bird.vY >= bird.get('jumpSpeed')) {
		bird.currentFrame = 1;
		bird.angle = Math.min(90, bird.angle + 15);
	} else {
		bird.angle = -15;
	}
	
	if (bird.screenYE > fg.screenY) scene.stop();
	
	pipes.each(removePipe);
});

function removePipe(pipe) {
	if (pipe.screenXE < 0) {
		layer.remove(pipe);
		detector.removeTarget(pipe);
	}
}

Increment vertical velocity of the bird by the gravity, which is defined in the json configurator file. Also add some rotation to make it more realistic. If the bird falls on the floor, stop the game, when a pipe leaves the screen, remove it from the Layer.

On click event the bird should "jump". This little code sets the vertical velocity to a negative value (jumpSpeed defined in the json).

scene.on('mousedown', function() {
	bird.vY = -bird.get('jumpSpeed');
});

Add two pipes in every two sec:

scene.addTask(function() {
	var offset = Math.random() * 100 | 0;
	
	detector.addTarget(layer.insert('#pipe', {
		y: -offset,
		sourceX: 554
	}, 'pipes'));
	
	detector.addTarget(layer.insert('#pipe', {
		y: scene.screen.height - offset,
		offsetY: "-100%",
		sourceX: 502
	}, 'pipes'));
}, 2000);

Almost done, we need to stop the Scene on collision:

bird.on('collision', function() {
	scene.stop();
});

That's all. 56 lines of code. Pretty easy!

Overview

Here is the code in one piece. You can also try it out online here.

Chill.out(new Chill.App('./chill apps/flappy-bird.json'), 'wrapper', function(scene) {
	var layer = scene.getLayer('main');
	var pipes = layer.getElementByID('pipes');
	var bird = layer.getElementByID('bird');
	var score = layer.getElementByID('score');
	var fg = layer.getElementByID('fg1');
	
	var detector = scene.watch(bird, []);
	
	scene.on('preloadComplete', scene.start);
	
	scene.on('tick', function() {
		bird.vY += bird.get('gravity');
		
		if (bird.vY >= bird.get('jumpSpeed')) {
			bird.currentFrame = 1;
			bird.angle = Math.min(90, bird.angle + 15);
		} else {
			bird.angle = -15;
		}
		
		if (bird.screenYE > fg.screenY) scene.stop();
		
		pipes.each(removePipe);
	});

	function removePipe(pipe) {
		if (pipe.screenXE < 0) {
			layer.remove(pipe);
			detector.removeTarget(pipe);
		}
	}
	
	scene.on('mousedown', function() {
		bird.vY = -bird.get('jumpSpeed');
	});
	
	scene.addTask(function() {
		var offset = Math.random() * 100 | 0;
		
		detector.addTarget(layer.insert('#pipe', {
			y: -offset,
			sourceX: 554
		}, 'pipes'));
		
		detector.addTarget(layer.insert('#pipe', {
			y: scene.screen.height - offset,
			offsetY: "-100%",
			sourceX: 502
		}, 'pipes'));
	}, 2000);
	
	bird.on('collision', function() {
		scene.stop();
	});
});