Kaboom.js Tutorial!
Kaboom.js Tutorial
Hello everyone, since we were in the middle of the KABOOM JAM, I decided to write a Kaboom.js tutorial to help all the participants who are willing to give it a try.
First of all, however, I believe you should know what you're learning before you actually start learning it. So, what is kaboom.js?
kaboom.js is a JavaScript library that allows you to create web-based games very easily and in a fun manner.
For this tutorial, we will create a classic snake game (since it's a tutorial, I thought it'd be best to use a game whose concepts are familiar to most people). The final product (don't flame didn't have an artist) is available at - https://replit.com/@TheDrone7/Kaboomjs
Get started
To get started with kaboom.js, we will first create a HTML/CSS/JS Repl.
There are also kaboom repls but we won't be using that since that is specific to repl.it only and you won't be getting all those GUI features at other places. Not to mention that mode also limits your development to just the game and you won't be able to have a backend. That mode is best suited for small games only.
You can also use a node.js repl or a python repl or a repl in any language if you want to make your own backend and add features to the game that would require one but for the sake of simplicity of a tutorial, we won't have one of those.
In the newly created repl, you will see 3 files - index.html
, script.js
, and style.css
. First of all, we will configure the index.html for our basic needs of the game.
Change the value inside the
<title>
tag toSnake game
.Add the following script tags inside the
<body>
tag to import kaboom.js
Note that the following lines of code need to go at the bottom of the body tag but right above the tag that importsscript.js
.<script src="https://kaboomjs.com/lib/master/kaboom.js"></script> <script src="https://kaboomjs.com/lib/master/kit/physics.js"></script> <script src="https://kaboomjs.com/lib/master/kit/starter.js"></script> <script src="https://kaboomjs.com/lib/master/kit/level.js"></script>
Here, the first line of code will import the base library of kaboom.js
The next 3 import "kits" that let's just say, "make life easy".Your HTML might look similar to this
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width"> <title>Snake game</title> <link href="style.css" rel="stylesheet" type="text/css" /> </head> <body> <script src="https://kaboomjs.com/lib/master/kaboom.js"></script> <script src="https://kaboomjs.com/lib/master/kit/physics.js"></script> <script src="https://kaboomjs.com/lib/master/kit/starter.js"></script> <script src="https://kaboomjs.com/lib/master/kit/level.js"></script> <script src="script.js"></script> </body> </html>
- Now we're ready to use kaboom.js in our project!
Initialize a new game
We have everything set up and are ready to start creating a new game. Kaboom.js is a js library so we will be writing js code so head into your script.js
file and follow along!
To access everything kaboom.js has to offer, we first need to create an instance or just make everything global. In our case, we will go with the 2nd choice and inside our
script.js
, write our first line of code askaboom.import();
And it will make everything kaboom.js contains global.
Now that we have access to all of kaboom.js, let's get started with actually creating the game! To create a new game, we use the
init
function that initializes a new canvas for our game. Just below the import line, write the following code.init({ width: 480, height: 360, scale: 2 });
This code will initialize a new canvas with its width set to
480px
and its height set to360px
.
Thescale
factor multiplies all sizes by the provided number.
In our case, it multiplies all sizes by 2. So our actual canvas will be of the width960px
and height720px
.
Why we use thescale
factor instead of directly setting the width to960
and height to720
? Because it multiplies all sizes with that factor. What that basically means is, it will also increase the font size and the size of the asset.
The MENU
Before we get into making the actual game, we will first create a menu to get to learn some more concepts of kaboom.js
Heading back to script.js
, after initializing a new game, we will create a new scene
. Scenes are usually different parts of a game such as different levels, menus, or game-over screens. In this instance, our first scene will be the menu scene which gives the player 2 options only - Play game
and Learn more about Kaboom
.
Let us get to it then.
Write the following piece of code at the end of your
script.js
filescene("menu", () => { add([ text("Snake game"), pos(240, 80), scale(3) ]); });
Now, let's break down what this piece of code is doing. Firstly, we are creating a new
scene
using thescene()
function which takes in 2 parameters:
The first is the name of the scene, in our case, we call it "menu". We will eventually use this name to tell kaboom which scene to run and when to run it.
The second parameter is a function that defines what the scene contains and how it works. This function is looped to update the scene as per requirement.We then add a
game object
to our scene using theadd
function, which takes in 1 argument which is a list ofcomponent
s that defines our game object.Our game object here, contains a text component that says "Snake game" (defined using the
text()
function), and a position component makes it draw on the horizontal middle of the scene (480 / 2 = 240) and a little above the vertical middle. (defined by thepos()
function), and a scale component that scales it by3
(using thescale
function). This will scale it by 6 times its normal size (since ourinit
method also contained a scale factor of 2).Next, we will design the 2 buttons that we need for user interaction.
For this, we will need to add 4 more game objects to the scene as follows
add([ rect(160, 20), pos(240, 180), "button" ]); add([ text("Play game"), pos(240, 180), color(0, 0, 0) ]); add([ rect(160, 20), pos(240, 210), "button" ]); add([ text("Learn Kaboom.js"), pos(240, 210), color(0, 0, 0) ]);
Here, for each button, we add 2 game objects - one is the rectangle in the background and the other is the text displayed over it.
We also have the string
"button"
as a parameter for the rectangle background of the buttons, such strings are calledtags
and these can be very helpful as we will see in the next step.These buttons look and feel sort of dull, so let's add some interactivity to them. Sticking to keeping it simple, we will just darken the background color of these buttons when the user pointer hovers over them. For this, we will need something we call
actions
in kaboom.jsactions
are functions, that can be optionally bound to agame object
, that runs every frame.Let's add some more code for that inside our scene.
action("button", b => { if (b.isHovered()) b.use(color(0.7, 0.7, 0.7)); else b.use(color(1, 1, 1)); });
Here, we define a new
action
that runs for allgame objects
with the tagbutton
. This helps us make sure it selects the button game objects that we defined above only. Inside we use the.isHovered()
method on the button (provided byrect()
component) and change the color of the button if it's currently being hovered, we change their color (using thecolor()
function) to a light shade of grey, if the button is being hovered.Now that our buttons look fine (no, they don't look great but they do look fine), the next step is to make them respond to mouse clicks and do something! We can use another convenient function called
isClicked()
to check if the button is being clicked the last frame
action("button", b => {
if (b.isHovered())
b.use(color(0.7, 0.7, 0.7));
else
b.use(color(1, 1, 1));
if (b.isClicked())
console.log("woohoo!");
});
Then we'll need a way to assign an action to each button, we can do this by assigning custom fields to them:
add([
rect(160, 20),
pos(240, 180),
"button",
{
clickAction: () => go('game'),
},
]);
add([
rect(160, 20),
pos(240, 210),
"button",
{
clickAction: () => window.open('https://kaboomjs.com/', '_blank'),
},
]);
action("button", b => {
if (b.isHovered())
b.use(color(0.7, 0.7, 0.7));
else
b.use(color(1, 1, 1));
if (b.isClicked())
b.clickAction();
});
so if you give a plain js object in the components array, it'll assign all fields of it directly to the added/returned game object, here we use that function in the "button" action responding to mouse click
When the "Play" button is clicked, it will load up the game
scene (we have not declared or worked on this so don't worry) using the go()
function.
When the "Learn" button is clicked, a new tab will be opened and the user will be taken to https://kaboomjs.com - the kaboom.js docs.
By now, your script.js
should be looking similar to this
kaboom.import();
loadSprite("border", "/border.png");
init({
width: 480,
height: 360,
scale: 2,
});
scene("menu", () => {
add([
text("Snake game"),
pos(240, 80),
scale(3),
]);
add([
rect(160, 20),
pos(240, 180),
"button",
{
clickAction: () => go('game'),
},
]);
add([
text("Play game"),
pos(240, 180),
color(0, 0, 0)
]);
add([
rect(160, 20),
pos(240, 210),
"button",
{
clickAction: () => window.open('https://kaboomjs.com/', '_blank'),
},
]);
add([
text("Learn Kaboom.js"),
pos(240, 210),
color(0, 0, 0)
]);
action("button", b => {
if (b.isHovered()) {
b.use(color(0.7, 0.7, 0.7));
} else {
b.use(color(1, 1, 1));
}
if (b.isClicked()) {
b.clickAction();
}
});
});
The game scene
We have the game menu set up completely, thus, comes the next part - the game itself!
We first define the game scene at the end of the
script.js
file.scene("game", () => { });
But before we go any further, you will notice, you won't be seeing the menu when running the game. This is because we have not told kaboom.js what scene to load by default. We can do this by simply using the
start()
function.
At the end of yourscript.js
, add the following line of code: -start("menu")
This will tell kaboom.js to load the
"menu"
scene after initializing the game.Now if you visit your website, you'll see a menu, that looks something like this
And that is exactly what we want to see when the user loads the page. You can try clicking on the
Learn Kaboom.js
button and it should take you to the kaboom.js documentation website.Clicking on
Play game
will take you to a new blank scene that we had just defined above.We have prepared the codebase for our new scene. Let's get to work on the scene! First, inside the scene, add the following code: -
add([ rect(480, 360), color(0, 0.3, 0), pos(240, 180) ]);
This will create a green background for our game (green representing grass).
Next up, we need a visual border that our player does not cross. For this border, we will use the following image: -
Download that picture and upload it to your repl, name it
border.png
. Keep it at the root directory alongside all other files.We will then import this file into our code. For this, we will create a new
sprite
. To achieve this, add the following line of code at the beginning of yourscript.js
right after importing kaboom: -loadSprite("border", "/border.png");
This will look for a file named
border.png
at the root directory and then prepare it to be used as a sprite and name the sprite"border"
for reference.Head back in the
game
scene and after adding the background, write the following piece of code: -for (let x = 0; x < 480 / 20; x++) { for (let y = 0; y < 360 / 20; y++) { if (x == 0 || y == 0 || x == (480/20 - 1) || y == (360/20 - 1)) { add([ sprite("border"), pos(10 + 20 * x, 10 + 20 * y), scale(0.5), "wall", ]); } } }
Since the size of our border is 40px, which after scaling by
0.5
becomes 20px, we loop over our scene (divided into squares with side 20px) and if either the x or y coordinate is the last one, add the border sprite, positioned accordingly, scaled by0.5
and has the tag"wall"
. We will use this tag to detect collisions.You can optionally set the default scene to
game
temporarily so you directly see that scene while testing.Your scene after this step should look something like this.
Now we have a visual border that the player is not supposed to cross alongside a decent game background.
Next up, we will work on creating our player! Our player consists of 2 parts - the head, and a lot of tails. We will start our player off with 3 tails and 1 head. Let's create the player object then.
let tails = []; let head = add([ rect(10, 10), pos(240, 180), "head", "right" ]); for (let i = 0; i < 3; i++) { tails.push({ obj: add([ rect(10, 10), pos(230 - 10 * i, 180), color(0.5, 0.5, 0.5), "tail", "right" ]), turns: [], directions: [] }); }
Here, we first define an array called
tails
. This will help us keep track of all the tails that the player has.
Then we add a10x10
square object for our head (the color defaults to white which works perfectly for us). We also give thehead
object the"head"
and"right"
tags. The"head"
tag will allow us to check for collisions between the head and other objects.Nextly, we define the user's tails. A for loop makes 3 tails, right behind each other behind the head. We make the tails to the left of the head and add "right" to all the tails as well. Since tails can be moving in a different direction from the head at some point in time, we need to keep track of each tail's directions and points of turning separately. For each tail, we store 3 pieces of information: the tail object that is displayed to the player, the positions at which the tail needs to move in a different direction, and the new direction in which it needs to move after it turns.
If you run the game now, you'll see your player in the center of the screen. Next step - give it the moves.
For which, we will add the following piece of code within our scene:
loop(1, () => { if (head.is("right")) head.pos.x += 10; if (head.is("left")) head.pos.x -= 10; if (head.is("down")) head.pos.y += 10; if (head.is("up")) head.pos.y -= 10; for (let t of tails) { if (t.turns.length > 0) { if (t.directions[0] === 'left' || t.directions[0] === 'right') { if (t.turns[0].y === t.obj.pos.y) { t.obj.removeTag("up"); t.obj.removeTag("down"); t.obj.removeTag("left"); t.obj.removeTag("right"); t.turns.shift(); t.obj.addTag(t.directions.shift()); } } if (t.directions[0] === 'up' || t.directions[0] === 'down') { if (t.turns[0].x === t.obj.pos.x) { t.obj.removeTag("up"); t.obj.removeTag("down"); t.obj.removeTag("left"); t.obj.removeTag("right"); t.turns.shift(); t.obj.addTag(t.directions.shift()); } } } if (t.obj.is("right")) t.obj.pos.x += 10; if (t.obj.is("left")) t.obj.pos.x -= 10; if (t.obj.is("down")) t.obj.pos.y += 10; if (t.obj.is("up")) t.obj.pos.y -= 10; } });
loop
is another function provided by kaboom.js. It is similar to javascript'ssetInterval
except it takes the interval first and callback second and it takes the interval in seconds instead of milliseconds.Inside the callback that is run every
1
second, we first check which of the 4 directional tags (using thegameObject.is()
method) our head and each of the tail objects have, then move each body part of our snake by 10px (the width of each square) to make a smooth moving transition.Additionally, before moving the tail parts, we also check if they have reached a point where it needs to turn, it checks the respective co-ordinates and if they match, it removes all directional tags and adds the new direction tag from the array of directions.
Note that we use.shift()
so we also get rid of the first entry in the arrays after having made the turn.Also, we check only one co-ordinate (either x or y) of the tail before making the turn. This is because by the time the turning point was added to the array, the head may have already moved to some new point perpendicular to the line in which the tail part is moving so it may never reach the turning point and get separated from the head (obviously not something we want so... yeah).
This part won't work, for now, we'll make it work in the next step where we let the player take control of the snake.We set the position of each object manually here instead of using the
gameObject.move()
method to create a smoother moving transition (if it moved by 1 pixel instead of 10 pixels, the snake taking turns would be awfully ugly). You can try doing that by replacing:.pos.x += 10
with.move(10, 0)
.pos.x -= 10
with.move(-10, 0)
.pos.y += 10
with.move(0, 10)
.pos.y -= 10
with.move(0, -10)
Fair warning: you may want to unsee what you see after you do this.
Running the game now will move your snake to the right side. You can try if the other direction work by changing the default tag your snake's head and tails contain to something other than
"right"
.Now that we have a snake on our screen that can move. We need to let the player control its movements. For this, we will be using the
keyPress()
function that takes in 2 parameters, the first is the key that on pressing triggers the callback provided as the second parameter. We will first create a separate function that handles making the turn then usekeyPress()
4 times for each direction. Basically, add the following code at the end of yourgame
scene.const makeTurn = (d, o) => { if (head.is(o)) return; else { let turnPos = head.pos.clone(); for (let t of tails) { t.turns.push(turnPos); t.directions.push(d); } head.removeTag("up"); head.removeTag("down"); head.removeTag("left"); head.removeTag("right"); head.addTag(d); } } keyPress("left", () => makeTurn("left", "right")); keyPress("right", () => makeTurn("right", "left")); keyPress("up", () => makeTurn("up", "down")); keyPress("down", () => makeTurn("down", "up"));
Here, our
makeTurn
function takes in 2 parameters - the first is the new direction the player wants the snake to move in and the second parameter is the direction opposite to the one the player wants the snake to move in.This is because we want to make sure that the player cannot move the snake in the direction opposite to its current direction of movement. Basically, we don't want the snake to go through itself.
The first line of code inside the function handles this check. After that, we make a clone of the snake's head's current position and then tell all tails to make a turn in the new direction when they reach that position (handled in the last step up above). Then we make the head make the turn right away since the head doesn't have to follow anything.
Then we use the
keyPress
function 4 times, each time it checks for one of the 4 arrow keys and then runs themakeTurn
function as needed.AND WE ALREADY HAVE A SNAKE THAT YOU CAN CONTROL!!!
This is what your game might look like (with enough big brain applied)
Normally the next part would be to add points, but before that, there is one thing that would be best addressed now and not later.
In a snake game, the head of the snake is not supposed to collide with the border or one of its tail parts, therefore, we will first add collision detection of these parts and if they do end up colliding, we will end the game.
We will work on a game-over screen later. For now, we will only pause the game when that happens.
To achieve this, we will need
colliders
and this is where tags come into play! Kaboom.js lets you detect collisions with tags.Add the following code at the end of your
game
scenecollides("wall", "head", () => { pause(); }); collides("head", "tail", (h, t) => { if (h.isOverlapped(t)) pause(); });
This will check for any collisions between objects with the tag
"wall"
and"head"
. We don't need to worry about the tails since the tails colliding with the walls will never reach a point that the head doesn't.Next, we have another collider for the head and tails. Since the first tail and head are always in contact, we don't want to say it's game over as soon as it starts, that's where the
.isOverlapped
method comes in. Normal colliders are triggered when 2 objects are even touching each other. But we only want our game to end if the head goes over a tail, thus we use the.isOverlapped
method to check for that and only pause the game when that is the case.Now our player is almost truly complete.
The next thing we are going to have to deal with, is spawning fruits. Some things to keep in mind
- Do not spawn over a wall.
- Do not spawn under the head.
Keeping these 2 things in mind let's develop a function that can properly spawn fruits. Our fruits here will just be red
10x10
squares. For this, we write the following code at the end of thegame
scene.const spawnFruit = () => { let x = 30 + Math.floor(Math.random() * 42) * 10; let y = 30 + Math.floor(Math.random() * 30) * 10; if (head.pos.x === x && head.pos.y === y) spawnFruit(); else { add([ rect(10, 10), pos(x, y), color(1, 0, 0), "fruit" ]); } } spawnFruit();
Here, we first define a recursive function, that first generates random coordinates on the map, checks if the head is on the same coordinates, if it is, redo it, else, spawn a new fruit there.
Then we run the function once to generate the first fruit for the player to collect.
Now that we can spawn fruits, we need to let our player collect them and we also need to keep a track of how many fruits has our player collected. For this, first, create a new variable named
score
and set its value to0
inside thegame
scene. For example: -let score = 0;
and then at the end of the scene, add a new collider as shown below: -
collides("fruit", "head", (f, h) => { if (h.isOverlapped(f)) { score++; destroy(f); spawnFruit(); let lastTail = tails[tails.length - 1]; let lastTailPos = lastTail.obj.pos.clone(); let lastTailDirection = lastTail.obj.is("left") ? "left" : (lastTail.obj.is("right") ? "right" : (lastTail.obj.is("up") ? "up" : "down")); let newX = lastTailDirection === "left" ? lastTailPos.x + 10 : (lastTailDirection === "right" ? lastTailPos.x - 10 : lastTailPos.x); let newY = lastTailDirection === "up" ? lastTailPos.y + 10 : (lastTailDirection === "down" ? lastTailPos.y - 10 : lastTailPos.y); let newTail = { obj: add([ rect(10, 10), pos(newX, newY), color(0.5, 0.5, 0.5), "tail", lastTailDirection ]), turns: [...lastTail.turns], directions: [...lastTail.directions] }; tails.push(newTail); } });
Here, we again first check if the head and fruit are overlapping, then we first add to the player's score, then we destroy the fruit since it has been consumed and spawn a new fruit using the function we defined above. After this, we create a replica of the last tail of the snake and place it right behind the current last tail on the canvas and then also add it to the tails array we defined above. This will increase the snake's length every time the snake consumes a fruit.
And we now have a full-fledged snake game!
But before we call this game complete, we still have a few things left.
Most importantly, even though our player is collecting fruits and increasing the score, the score is not shown to the user at all. Let's create a new display for the score in the top-left corner of the game scene. For this, at the end of your screen add the following code
add([ rect(100, 20), pos(50, 10), color(0, 0, 0) ]); add([ text(`Score: ${score}`), pos(50, 10), "score-text" ]); action("score-text", txt => { txt.text = `Score: ${score}`; });
We first create a black background in the top-left corner for us to display the score properly, then we write over it the user's score in the format
Score: 0
and look for updates every frame using theaction()
function.This will update the displayed score every time the player eats a fruit. Now our game is truly complete!!!
Game Over Screen
We finally are done with our game! But when the game is over, the player is stuck with a paused screen that shows... nothing. It looks as if the game crashed, so we will create a game-over scene so the player knows the game is over and can move on.
For this scene, however, we won't be writing most of our code. Duplicate the menu
scene that we first created. And then rename this new scene to game-over
.
In the title, change the text displayed from Snake Game
to Game Over
.
Change the text of the Play game
button to Play again
and the text of the Learn Kaboom.js
button to Main Menu
.
Inside the mouseClick
handler, change the handler of the Learn button. Remove the following line
window.open('https://kaboomjs.com/', '_blank');
with
go('menu');
And we're almost done!
Just for a little gimmick, we will also show the score of the player on this screen. For this, first head back to the game
scene and at both places where we check for collisions, replace
pause();
with
go("game-over", score);
And done! Come back to the game-over
scene to actually display this score. The score will now be passed as a parameter to our scene function, making it look something like
scene('game-over', score => {
// ...
})
Here score
is the same value as the player's score
from our game scene. To display this, we just add the following code to our game-over
scene.
add([
text("Your score: " + score),
pos(240, 125),
scale(2)
]);
And it will now beautifully display our scores on the game over screen.
SOME IMPROVEMENTS
Even though the game is ready to be played, it's not perfect (and it was never intended to be). However, there are some improvements we can make right now to make it a better experience. The things we're adding are listed below
- Bugfix (tail detaches from the head at one special scenario)
- High scores.
So let's fix that bug. The bug is - if the player presses one directional key and presses another before the head has moved, the tails end up getting detached. To fix this, we can simply pause input for a duration of 0.75 seconds. To achieve this, we first define a new variable at the top of our game scene called lastInput
as shown below
let lastInput = Date.now();
Now inside the makeTurn()
function that we created, we can simply add a new condition and turn it into a new function as shown below: -
const makeTurn = (d, o) => {
if (Date.now() - lastInput > 750) {
if (head.is(o)) return;
else {
let turnPos = head.pos.clone();
for (let t of tails) {
t.turns.push(turnPos);
t.directions.push(d);
}
head.removeTag("up");
head.removeTag("down");
head.removeTag("left");
head.removeTag("right");
head.addTag(d);
}
}
}
and now it will only run once every 0.75 seconds at the fastest.
And for the last part, let's add high scores. These high scores will be of the player themselves only, not a public leaderboard and we will simply store it in their browser's localStorage. We will be displaying the high score only on the game-over scene so let's head to that.
At the beginning of the scene, before displaying anything, we will first look for any stored high scores, check if the stored high score is higher than the player's current score, change it to the current score if it isn't. Let it be if it is. Then display both the current score and the high score. This can be achieved with just 4 simple lines of code (other than the displaying).
let highScore = localStorage.highScore || score;
if (score >= highScore)
localStorage.highScore = score;
highScore = localStorage.highScore;
Change the text object that shows your current score be smaller and be positioned a little higher so we can also display the high score as shown below
add([
text("Your score: " + score),
pos(240, 115),
scale(1.5)
]);
add([
text("High score: " + highScore),
pos(240, 135),
scale(1.5)
]);
And that will display both your scores at once! Here's a screenshot
THE END
And that's the end of this tutorial! Note that this tutorial was made while Kaboom.js was in beta so don't be surprised if the code doesn't work for you. If you have any questions, feel free to ask in the comments section although the best option would be to check the documentation.
I don't understand where do we place the border coding?? can someone help me please?
So how do we add parameters to another scene in Replit? Directly modifying HTML file does not work as Replit will automatically modify the HTML file back.
really good
Where do we get the border image for the "loadSprite("border", "/border.png");" script?
@NVVC007 save the image he provided above (save as..) and then just drag it into your files
bonk
i’m new to coding.
✨ O O O O ✨
nice!
wow this is
Pretty pog
This is also the first time I have seen you post something other than an announcement
@EpicGamer007 NGL, I almost posted this in Announcements out of habits. Changed to Learn at the last moment before posting.
Attach repl when :(
@alanchen12 link in the beginning of the post.
oH sorry didnt read @TheDrone7
:O cool
💥noice
and thanks! :D