Building Flappy Bird in Kaboom.js
Flappy Bird was a smash hit game for mobile phones back in 2013-2014. The inspiration behind the app was the challenge of bouncing a ping pong ball on a paddle for as long as possible without letting it drop to the ground or shoot off into the air. At the peak of its success, the game creator unexpectedly removed it from all app stores, saying that he felt guilty that the game had become addictive for many people. In the wake of the removal, many clones were made to fill the gap left by the original Flappy Bird. After a few months, the original author released new versions of the game.
Let's take a trip back to 2014 and create our own clone of Flappy Bird using Kaboom! By remaking a game, you can not only learn how to make games, but also extend and change the game in any way you like.
This article is based on this video tutorial, with a few small differences. Mainly, the Flappy assets (graphics and sound) are no longer available by default in the Replit Kaboom asset library, but that's OK because we've included them as a download here, so you can still use them.
Creating a new project in Replit
Head over to Replit and create a new repl. Choose Kaboom as your project type. Give this repl a name, like "Flappy!".
After the repl has booted up, you should see a main.js
file under the "Code" section. This is where we'll start coding. There is already some code in this file, but we'll replace that.
Download the sprites and asset files we need for the game, and unzip them on your computer. In the Kaboom editor, click the "Files" icon in the sidebar. Now drag and drop all the sprites (image files) into the "sprites" folder, and all the sounds (MP3 files) into the "sounds" folder. Once they have uploaded, you can click on the "Kaboom" icon in the sidebar, and return to the "main" code file.
Initializing Kaboom
In the "main" code file, delete all the example code. Now we can add reference to Kaboom, and initialize it:
import kaboom from "kaboom";
kaboom();
Let's import the game assets (graphics and sound). We can use Kaboom's loadSprite
and loadSound
functions:
loadSprite("birdy", "sprites/birdy.png");
loadSprite("bg", "sprites/bg.png");
loadSprite("pipe", "sprites/pipe.png");
loadSound("wooosh", "sounds/wooosh.mp3");
The first argument in each load
function is the name we want to use to refer to the asset later on in our code. The second parameter is the location of the asset to load.
Adding scenes
Scenes are like different stages in a Kaboom game. There are generally three scenes in games:
- The intro scene, which gives some info and instructions, and waits for the player to press "start".
- The main game, where we play.
- An endgame, or game over scene, which gives the player their score or overall result, and allows them to start again.
For this tutorial, we'll omit the intro scene, since we already know what Flappy bird is and how to play it, but you can add your own intro scene later!
Let's add the code for defining each scene:
scene("game", () => {
// todo.. add scene code here
});
scene("gameover", (score) => {
// todo.. add scene code here
});
go("game")
Notice in the gameover
scene definition, we add a custom parameter, score
. This is so that we can pass the player's final score to the end game scene to display it.
To start the whole game off, we use the go
function, which switches between scenes.
Building the game world
Now that we have the main structure and overhead functions out of the way, let's start adding in the characters that make up the Flappy world. In Kaboom, characters are anything that makes up the game world, including floor, platforms, etc., and not only the players and bots. They are also known as "game objects".
We'll start with the background, using the bg.png
image we added earlier. Add this code to the game
scene section:
add([
sprite("bg", {width: width(), height: height()})
]);
Here we use the add
function to add a new character to the scene. The add
function takes an array of components that we can use to give each game character special properties. In Kaboom, every character is made up of one or more components. There are built-in components for many properties, like sprite
, which gives the character an avatar; body
, which makes the character respond to gravity; and solid
, which makes the character solid, so other characters can't move through it.
Since the background doesn't need to do much, just stay in the back and look pretty, we only use the sprite
component, which displays an image. The sprite
component takes the name of the sprite, which we set when we loaded the sprite earlier, and optionally, the width and height that it should be displayed at on the screen. Since we want the background to cover the whole screen, we need to set the width
and height
of the sprite to the width and height of the window our game is running in. Kaboom provides the width()
and height()
functions to get the window dimensions.
If you press the "Run" button at the top of your repl now, you should see the background of the Flappy world come up in the output section of the repl:
Great! Now let's add in the Flappy Bird. Add this code to the game
scene:
const player = add([
// list of components
sprite("birdy"),
scale(2),
pos(80, 40),
area(),
body(),
]);
We use the same add
function we used for adding the background. This time, we grab a reference, const player
, to the returned game object. This is so we can use this reference later when checking for collisions, or flapping up when the player taps the space bar.
You'll also notice that the character we are adding here has many more components than just the sprite
component we used for the background. We already know what the sprite
component does, here is what the rest are for:
- The
scale
component makes the sprite larger on screen by drawing it at2
times the sprite's normal image size. This gives a nice pixelated look, while also making it easier to spot the bird. - The
pos
component sets the position on the screen that the character should initially be at. It takes X and Y coordinates to specify a position. - The
area
component gives the sprite an invisible bounding box around it, which is used when calculating and detecting collisions between characters. We'll need this so that we can detect if Flappy flies into the pipes. - The
body
component makes the character subject to gravity. This means Flappy will fall out of the sky if the player doesn't do anything.
Press command + s
(Mac) or control + s
(Windows/Linux) to update the game output window. You should see Flappy added and fall out of the sky very quickly:
Making Flappy fly
Our next task is to save Flappy from plummeting to their death by giving control to the player to flap Flappy's wings. We'll use the spacebar for this. Kaboom has an onKeyPress
function, which fires a callback with custom code when the specified key is pressed. Add this code to the game
scene to make Flappy fly when the space
key is pressed:
onKeyPress("space", () => {
play("wooosh");
player.jump(400);
});
In the callback handler, we first play
a sound of flapping wings to give the player feedback and add some interest to the game. Then we use the jump
method, which is added to our player character through the body
component we added earlier. The jump
function makes a character accelerate up sharply. We can adjust just how sharp and high the jump should be through the number we pass as an argument to the jump method – the larger the number, the higher the jump. Although Flappy is technically not jumping (you normally need to be on a solid surface to jump), it still has the effect we need.
Update the game output window, and if you press the spacebar now, you'll be able to keep Flappy in the air! Remember to quickly click in the output window as the game starts, so that it gains focus and can detect player input such as pressing the space
key.
Adding in the pipes
Now we can get to the main part of the game – adding in the moving pipes that Flappy needs to fly through.
Here is a diagram of the layout of the pipes in the game.
We want to move the pipe gap, and therefore the pipes, up and down for each new pipe pair that is created. This is so we don't have the gap at the center point of the screen constantly – we want it to be slightly different for each pipe pair that comes along. We do want to keep the gap size consistent though.
Let's start by having the pipe gap in the center of the screen. We'll give the pipe gap a size PIPE_GAP
. Then to place the pipes, the bottom of the upper pipe should be PIPE_GAP/2
pixels above the center point of the window, which is height()/2
. Likewise, the top of the lower pipe should be PIPE_GAP/2
pixels below the center point of the window, again which is height()/2
.
This way, we place the pipe so that the pipe gap is in the center of the window. Now we want to randomly move this up or down for each new pair of pipes that comes along. One way to do this is to create a random offset, which we can add to the midpoint to effectively move the midpoint of the window up or down. We can use the Kaboom rand
function to do this. The rand
function has two parameters to specify the range in which the random number should be.
Let's put that all together. The Y-position of the lower pipe can be calculated as:
height()/2 + offset + PIPE_GAP/2
Remember, the top of the window is y=0
, and the bottom is y=height()
. In other words, the lower down on the screen a position is, the higher its y
coordinate will be.
For the upper pipe, we can calculate the point where the bottom of the pipe should be like this:
height()/2 + offset - PIPE_GAP/2
Kaboom has an origin
component that sets the point a character uses as its origin. This is topleft
by default, which works well for our lower pipe, as our calculations above are calculating for that point. However, for the upper pipe, our calculations are for the bottom of the pipe. Therefore, we can use the origin
component to specify that.
Since we want the pipes to come from the right of the screen toward the left, where Flappy is, we'll set their X-position to be the width()
of the screen.
To identify and query the pipes later, we add the text tag "pipe"
to them.
Finally, since we need to create many pipes during the game, let's wrap all the pipe code in a function that we will be able to call at regular intervals to make the pipes.
Here is the code from all those considerations and calculations. Insert this code to the game
scene:
const PIPE_GAP = 120;
function producePipes(){
const offset = rand(-50, 50);
add([
sprite("pipe"),
pos(width(), height()/2 + offset + PIPE_GAP/2),
"pipe",
area(),
]);
add([
sprite("pipe", {flipY: true}),
pos(width(), height()/2 + offset - PIPE_GAP/2),
origin("botleft"),
"pipe",
area()
]);
}
Now we need to do a few more things to make the pipes appear and move.
To move the pipes across the screen, we can use the onUpdate
function to update all pipes' positions with each frame. Note that we only need to adjust the x
position of the pipe. Add this code to the game
scene part of your code:
onUpdate("pipe", (pipe) => {
pipe.move(-160, 0);
});
Next we'll generate pipes at a steady rate. We can use the loop
function for this. Add the following to the game
scene part of the code:
loop(1.5, () => {
producePipes();
});
This calls our producePipes()
function every 1.5
seconds. You can adjust this rate, or make it variable to increase the rate as the game progresses.
Update the game output window now and you should see the pipes being generated and moving across the screen. You can also fly Flappy, although crashing into the pipes does nothing for now.
Flappy is flapping and the pipes are piling on. The next task is to detect when Flappy flies past a pipe, increasing the player's score.
Adding in scoring
When Flappy flies past a pipe, the player's score is incremented. To do this, we'll need to keep track of which pipes have gone past Flappy. Let's modify the pipe-generating function producePipes
to add a custom property called passed
to the pipes. It should look like this now:
function producePipes() {
const offset = rand(-50, 50);
add([
sprite("pipe"),
pos(width(), height() / 2 + offset + PIPE_GAP / 2),
"pipe",
area(),
{passed: false}
]);
add([
sprite("pipe", { flipY: true }),
pos(width(), height() / 2 + offset - PIPE_GAP / 2),
origin("botleft"),
"pipe",
area(),
]);
}
Next, we'll add in a variable to track the score
, and a text element to display it on screen. Add this code to the game
scene:
let score = 0;
const scoreText = add([
text(score, {size: 50})
]);
Now we can modify the onUpdate()
event handler we created earlier for moving the pipes. We'll check if any pipes have moved past Flappy, and update their passed
flag, so we don't count them more than once. We'll only add the passed
flag to one of the pipes, and detect it, so as not to add a point for both the upper and lower pipe. Update the onUpdate
handler as follows:
onUpdate("pipe", (pipe) => {
pipe.move(-160, 0);
if (pipe.passed === false && pipe.pos.x < player.pos.x) {
pipe.passed = true;
score += 1;
scoreText.text = score;
}
});
This checks any pipe that we haven't marked as passed
(passed === false
) to see if it has passed Flappy (pipe.pos.x < player.pos.x
). If the pipe has gone past, we add 1
to the score and update the score text onscreen.
If you update the game output window now, you should see the score increase as you fly past each pipe.
Collision detection
Now that we have scoring, the last thing to do is collision detection – that is, checking if Flappy has splatted into a pipe. Kaboom has a collides
method that is added with the area
collider component. We can use that to call a function when the player collides with any character with the "pipe"
tag. Add this code to the game
scene:
player.collides("pipe", () => {
go("gameover", score);
});
In the collision handler, we use the go
function to switch to the gameover
scene. We don't have anything in that scene yet, so let's update that to show a game over message and the score. We can also keep track of the high score to compare the player's latest score to. Update the gameover
scene as follows:
let highScore = 0;
scene("gameover", (score) => {
if (score > highScore) {
highScore = score;
}
add([
text(
"gameover!\n"
+ "score: " + score
+ "\nhigh score: " + highScore,
{size: 45}
)
]);
onKeyPress("space", () => {
go("game");
});
});
First, we create a highScore
variable where we can track the top score across multiple game plays. Then, in our gameover
scene, we check if the latest score passed in is bigger than the highScore
we have recorded. If it is, the highScore
is updated to the latest score.
To show a "game over" message, and the player's score along with the high score, we use the add
function to add a text
component to a new game object or character. We also make the font size
large-ish for this message.
Let's include a quick way for the player to play again and try to beat their score. We use the onKeyPress
to listen for the player pressing the space
bar. In our key-press handler, we go
back to the main game
scene, to start the game all over again.
We also need to end the game if Flappy flies too high out of the screen, or plummets down off the screen. We can do this by adding a handler for the player's onUpdate
event, which is called each frame. Here we can check if Flappy's position is beyond the bounds of the game window. Add this code to the game
scene:
player.onUpdate(() => {
if (player.pos.y > height() + 30 || player.pos.y < -30) {
go("gameover", score);
}
});
This gives a margin of 30 pixels above or below the window, to take account of Flappy's size. If Flappy is out of these bounds, we go
to the gameover
scene to end the game.
Update the game output window again and test it out. If you fly into a pipe now, or flap too high, or fall out of the sky, you should be taken to the game over screen:
Next steps
Here are some ideas you can try to improve your clone of the Flappy Bird game:
- Make the game play faster as the player gets a higher score. You can do this by updating the speed that the pipes move by making the speed parameter passed to the
pipe.move
method a variable, which increases as the player score increases. - Add some different types of obstacles, other than the pipes, for Flappy to try to avoid.
- Use the Kaboom sprite editor to create your own graphics for your Flappy world!
- Add in some more sound effects and play some game music using the
play
function.