In this tutorial, I’ll teach you how to make a simple Sonic themed infinite runner game in TypeScript using the KAPLAY game library.
You can play the game here. The final source code for it is available here.
In this game, you need to collect as many rings as possible. Jumping on enemies grants you a bonus of 10 rings. Continually jumping on enemies without touching the ground gives you a multiplier on each enemy. The first enemy grants you +10, the second +(10 x 2), the third +(10 x 3), etc…
The challenge comes from the game getting progressively faster, making it harder to avoid getting hit by enemies. I really recommend playing the game to have a better idea of what we’re going to build.
Prerequisites
I expect that you have basic familiarity with TypeScript or are, at the very least, competent in JavaScript. We’re going to write the game in TypeScript, but it’s going to be very easy TypeScript code. Since we will rarely, if ever, need to write custom types. You should be familiar with Node.js and NPM and have them installed on your machine before following this tutorial. I do not expect you to know KAPLAY.
Table of Contents
What is KAPLAY?
KAPLAY is an open source library for making games in JavaScript and TypeScript.
Compared to the more popular Phaser JavaScript game framework, its API is a lot simpler making it easier for beginners to pick up. It also offers a lot of premade functionality out of the box. You spend therefore, less time reinventing the wheel.
For those familiar with the Godot game engine, you might be accustomed to the concept of nodes which acts as building blocks containing out of the box functionality. KAPLAY has a somewhat similar concept to nodes called components.
You can check the official KAPLAY website for more info here.
Project Setup
Create a folder called sonic-runner
. cd
within using your terminal of choice and run :
npm create vite@latest .
This command will scaffold a TypeScript project within the existing folder using the popular build tool Vite.
Once the command runs, you’ll be presented with the following :
> npx
> create-vite .
│
◆ Select a framework:
│ ● Vanilla
│ ○ Vue
│ ○ React
│ ○ Preact
│ ○ Lit
│ ○ Svelte
│ ○ Solid
│ ○ Qwik
│ ○ Angular
│ ○ Others
└
Pick “Vanilla”. You’ll be presented with another menu to chose between JavaScript and TypeScript. Select TypeScript.
> npx
> create-vite .
│
◇ Select a framework:
│ Vanilla
│
◆ Select a variant:
│ ● TypeScript
│ ○ JavaScript
└
Now let’s install the KAPLAY library. We will install the specific version called v3001.0.16. Run the following :
npm install kaplay@3001.0.16
This version is the latest stable version available at the time this tutorial was written. As long as you stick with this specific version, you shouldn’t have issues following this tutorial years into the future.
Once installed, take a look at your package.json
file in your project’s folder. You’ll notice that KAPLAY was added to your dependencies.
{
"name": "sonic-runner-ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.7.2",
"vite": "^6.3.1"
},
"dependencies": {
"kaplay": "^3001.0.16"
}
}
When we used the npm create
command, Vite created a bunch of files. Make sure that you have the following project structure. Remove everything within the src
folder as we’re going to write everything from scratch.
- node_modules // should be git ignored in .gitignore
- public
- src
- .gitignore
- index.html
- package-lock.json
- package.json
- tsconfig.json
Once everything is removed in src
, create two files within. One named kaplayCtx.ts
and the other main.ts
. You should have the following project structure.
- node_modules // should be git ignored in .gitignore
- public
- src
|- kaplayCtx.ts
|- main.ts
- .gitignore
- index.html
- package-lock.json
- package.json
- tsconfig.json
Finally, the last step of our setup is to bring in our game assets. Copy the contents of the public
folder provided in the game’s final source code available on GitHub. The repo can be accessed by clicking here.
After having done so, you should have the following files in your public folder.
- public
|- fonts
|- mania.ttf
|- graphics
|- chemical-bg.png
|- motobug.png
|- platforms.png
|- ring.png
|- sonic.png
|- sounds
|- Destroy.wav
|- Hurt.wav
|- HyperRing.wav
|- Jump.wav
|- Ring.wav
|- vite.svg
Initializing The Canvas
Now that our setup is complete, we can initialize an HTML canvas in which the game will be rendered. This will be done through KAPLAY by initializing our context which will also allow us to use KAPLAY functions.
In kaplayCtx.ts
, write the following code :
import kaplay from "kaplay";
const k = kaplay({
width: 1280,
height: 720,
letterbox: true,
global: false,
buttons: {
jump: {
keyboard: ["space"],
mouse: "left",
},
},
touchToMouse: true,
debug: true, // set it to false when deploying the game
pixelDensity: window.devicePixelRatio,
});
export default k;
Our context is created by calling the kaplay
function and passing an object containing various options we want to configure for our game. We first set the width and the height of the canvas.
We set the letterbox
property to true
, allowing our game canvas to scale regardless of the screen size while still retaining its aspect ratio.
We set the global
property to false
so that KAPLAY functions can only be used via the constant k
. This will lead to more legible code since you’ll be able to easily detect if a KAPLAY function is in use by noticing k.nameOfTheFunction
.
We pass an object to the buttons
property to define the jump
action of our game. Since, we want to make the game playable on both desktop and mobile, we set the jump
custom action to be mapped to the space key or the left mouse click. jump
is an arbitrary name and you could have decided on something different. You can also add an arbitrary amount of actions depending on your game’s needs by adding a new property. For example :
buttons: {
jump: {
keyboard: ["space"],
mouse: "left",
},
moveLeft: {
keyboard: ["left"],
},
/// etc...
},
The touchToMouse
property is used to convert touch input to mouse clicks so that the game is playable on mobile.
The debug
property is used to toggle on/off KAPLAY’s debug mode. This mode can be accessed on the webpage the game is running on by pressing the f1
key or the fn+f1
keys on a Mac. It will display an fps counter along with the game objects’ hitboxes which makes debugging easier. We will use it later in this tutorial. Here, we’re setting debug
to true
so that the debug mode is accessible.
One thing to note is that by default, debug mode is available so you don’t need use this property initially. The reason I included it here, is that when you ultimately deploy your game, you might not want your players to access this mode. Therefore, setting this property to false
before deployment is a good idea.
Finally, to make the game display sharply regardless of the screen we set the pixelDensity
property to correspond to the pixel ratio of the current screen. You’ll notice that not using this property will make the game render slightly blurry on certain screens.
We then export the constant k
containing a reference to the context, so that we can use it elsewhere in our code.
Creating Scenes
Our game will have two scenes. The first, is where the game takes place while the second, is a game over screen displaying the player’s score.
In KAPLAY, you can create mulitple scenes using the scene
function. In our main.ts
file let’s import the KAPLAY context and create our scenes.
import k from "./kaplayCtx";
k.scene("game", () => {
// TODO
});
k.scene("game-over", () => {
// TODO
});
k.go("game");
Here we created two scenes. The scene
function takes, as the first param, the name you want to use to refer to that scene. The second param is a function that will run when the game enters that scene.
Finally, we also need to use the go
function to tell KAPLAY which scene to go to when the game starts. This function is also used to navigate to other scenes when called within a specific scene.
We will write the logic for each scene later, now let’s run our project to see if we have correctly initialized our canvas.
In your terminal, run the command :
npm run dev
VITE v6.3.4 ready in 229 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
When you open the localhost address in your browser, if everything went well, you should see a checkered canvas like shown below.
If you try to resize your window, you’ll also notice that the canvas seems to always preserve its aspect ratio. This is exactly what we want so that the game is playable on any device.
Loading Assets
In main.ts
let’s load the assets we need for the background and the platforms. This will be a good introduction to how assets are loaded in KAPLAY. Add the following code :
import k from "./kaplayCtx";
k.loadSprite("chemical-bg", "graphics/chemical-bg.png");
k.loadSprite("platforms", "graphics/platforms.png");
//... rest of the code omitted for clarity
Here we use KAPLAY’s loadSprite
function. It takes, as the first param, a string which corresponds to the name you want to use to refer to that specific sprite/image. For the second param, the function expects the path to the asset.
Note that we don’t need to add the public
folder to the path since Vite makes sure to make whatever is in that folder accessible as if it was placed at the root of the project.
Implementing an Infinite Scrolling Background
In our game, the city background must scroll indefinitely. Let’s implement this in our game scene. Add the following code in main.ts
:
k.scene("game", () => {
const bgPieceWidth = 2880;
const bgPieces = [
k.add([
k.sprite("chemical-bg"),
k.pos(0, 0),
k.opacity(0.8),
k.scale(1.5)
]),
k.add([
k.sprite("chemical-bg"),
k.pos(bgPieceWidth, 0),
k.opacity(0.8),
k.scale(1.5),
]),
];
k.onUpdate(() => {
if (bgPieces[1].pos.x < 0) {
bgPieces[0].moveTo(bgPieces[1].pos.x + bgPieceWidth * 2, 0);
const frontBgPiece = bgPieces.shift();
// so typescript shuts up
if (frontBgPiece) bgPieces.push(frontBgPiece);
}
bgPieces[0].move(-100, 0);
bgPieces[1].moveTo(bgPieces[0].pos.x + bgPieceWidth * 2, 0);
});
});
Let’s break it down.
const bgPieceWidth = 2880;
const bgPieces = [
k.add([
k.sprite("chemical-bg"),
k.pos(0, 0),
k.opacity(0.8),
k.scale(1.5)
]),
k.add([
k.sprite("chemical-bg"),
k.pos(bgPieceWidth, 0),
k.opacity(0.8),
k.scale(1.5),
]),
];
To achieve an infinite scrolling background, we need need to display the same background image twice. The trick is to reposition the first image behind the second one once it leaves the screen. The second image becomes the first and the first becomes the second. The same logic is repeated indefinitely.
I previously wrote a post specific to infinite backgrounds with easy to understand visuals. You can check it out below.
In the code above, we first set a constant holding the width of the background image when scaled by 1.5, which corresponds to 1920 x 1.5 = 2880 since the original image has a width of 1920. This is needed so that we can position the second copy right after the first one without leaving any gaps. We will display the background at 1.5 its original scale so that it looks nice within our game.
We then set an array containing the two copies. Each copy is created using KAPLAY’s add
function which creates a game object, one of KAPLAY’s major concepts.
What are Game Objects and Components in KAPLAY?
A game object represents an entity rendered on the canvas and is composed of components. These components determine various methods and features that it has access to. KAPLAY provides these components out of the box but you can also create custom ones if you wish to (We won’t in this tutorial).
k.add([
k.sprite("chemical-bg"),
k.pos(0, 0),
k.opacity(0.8),
k.scale(1.5)
]),
The add
function used to create game objects, takes an array of KAPLAY components. Let’s break down the 4 we use here :
sprite
comp : Renders a game object as a visible sprite/image.pos
comp : Sets the position of a game object on the canvas using(x, y)
coordinates. In KAPLAY and in many other game libraries, thex
coordinate increases the further you go right and they
coordinate increases the further you go down. The origin(X = 0, Y = 0)
is set at the top-left corner of the canvas.opacity
comp : Sets the opacity of a game object using a value between 0 and 1. The closer the value is to 0 the more it becomes transparent.scale
comp : Sets the scale of a game object. Is often used alongside thesprite
comp to change the sprite/image’s size on the canvas.
k.add([
k.sprite("chemical-bg"),
k.pos(bgPieceWidth, 0),
k.opacity(0.8),
k.scale(1.5)
]),
As you can see, the second copy uses the same components. However, we set the position differently so that it is rendered right behind the first one without leaving any gaps.
Implementing Movement With an onUpdate Loop
While creating game objects using the add
function will render them on the canvas, no movement will occur without an update loop.
The update loop runs every frame and is used to update our game’s logic. If your game runs at 60fps, the loop will run 60 times per second. In KAPLAY, we use the onUpdate
function to set our update loop.
k.onUpdate(() => {
if (bgPieces[1].pos.x < 0) {
bgPieces[0].moveTo(bgPieces[1].pos.x + bgPieceWidth, 0);
const frontBgPiece = bgPieces.shift();
// so typescript shuts up
if (frontBgPiece) bgPieces.push(frontBgPiece);
}
bgPieces[0].move(-100, 0);
bgPieces[1].moveTo(bgPieces[0].pos.x + bgPieceWidth, 0);
});
We first check if the background image second copy’s x
position is < 0. In that case, we can assume that the first copy is offscreen and therefore, we can safely attempt to move it behind the second copy. This is achieved by setting its position using the moveTo
KAPLAY method made available due to using the pos
comp when the game object was defined.
Using the shift
array method we delete the first element from the array and return it. Finally, we push it back to the array so that the first copy is now placed as the second element of the array making it become the second copy. Another if statement is used to make sure the result of shift()
is not undefined
before pushing it to the array again. Otherwise, TypeScript will complain and rightly so.
Regardless of all of the above, on each iteration of the loop, we move both copies to the left by using the move
KAPLAY method on the first copy and the moveTo
method on the second copy to follow the first copy. Both these methods exists because we used the pos
component when creating each respective game object.
The move
method takes two params. The first is for setting the velocity of the game object on the x
axis while the second sets the velocity on the y
axis. By setting the x
velocity to -100 and the y
velocity to 0, we’re moving only to the left at a velocity of 100 pixels/second.
If you’re accustomed to other game engines/frameworks, you might have noticed the absence of deltaTime
(corresponds to the time elapsed since the last frame in our game loop) being used to make our movement frame rate independent. This is because the move
method takes care of it under the hood.
Now, looking at your browser tab (Assuming that you have localhost running) you should see the city background scrolling indefinitely.
To make the background less distracting during gameplay, I opted to set its opacity to 0.8 instead of 1. Now, if you go back to your kaplayCtx.ts
, we can set the background of the whole webpage to be black. This will result in the city background becoming darker and therefore less distracting during gameplay.
const k = kaplay({
width: 1280,
height: 720,
letterbox: true,
global: false,
buttons: {
jump: {
keyboard: ["space"],
mouse: "left",
},
},
touchToMouse: true,
debug: true,
pixelDensity: window.devicePixelRatio,
background: [0, 0, 0], // <--- new line
});
We provide to the background
property an array of three elements each representing one of the RGB color channels. Each element’s value can vary between 0 to 255. This is what’s used to represent color. If you check your browser tab, you should notice that the city background is now darker.
Implementing an Infinite Scrolling Platform
Our game is based on one big illusion. Sonic isn’t actually running. The platform on which Sonic stands isn’t actually moving. The only things moving are the background and the platform’s image which are made to scroll indefinitely.
If we were to remove all of the game’s graphics, we would be left with a game object representing the player that can jump up and land on a static floor.
Our goal at the moment, is to implement infinite scrolling platforms the same way we did for the city background. The only difference is that the scrolling speed will increase the further the game progresses.
In main.ts
add the following code :
// previous code omitted for clarity
k.scene("game", () => {
let gameSpeed = 100;
k.loop(1, () => {
gameSpeed += 50;
});
// code for the city background omitted for clarity
const platformWidth = 2560;
const platforms = [
k.add([k.sprite("platforms"), k.pos(0, 450), k.scale(2)]),
k.add([k.sprite("platforms"), k.pos(2560, 450), k.scale(2)]),
];
k.onUpdate(() => {
// city background update logic omitted for clarity
if (platforms[1].pos.x < 0) {
platforms[0].moveTo(
platforms[1].pos.x + platformWidth,
platforms[1].pos.y
);
const frontPlatform = platforms.shift();
if (frontPlatform) platforms.push(frontPlatform);
}
platforms[0].move(-gameSpeed, 0);
platforms[1].moveTo(platforms[0].pos.x + platformWidth, platforms[0].pos.y);
});
});
We first set a gameSpeed
variable that is incremented every second using KAPLAY’s loop
function. This is similar to JavaScript/TypeScript’s setInterval
function. The first param is for setting the time between each call and the second param is the function that will be called.
Similarly to the city background, we create two copies of the platforms’ image so that we can achieve infinite scrolling. platformWidth
corresponds to the final width of the platforms’ image after scaling it twice. This is because we need to make the platforms bigger in our game to make it look good. We use this constant to know where to place the second copy behind the first one without leaving any gaps.
The update logic is also similar, the only difference this time is that the move
function takes the gameSpeed
variable as the x
velocity allowing for it to increase the platforms speed through time.
If you run the project, you should notice the platforms scrolling slowly and then gradually increase in speed as time passes.
Implementing The Sonic Game Object
Now that we have our game scene mostly done, we’re ready to work on adding Sonic to our game.
Sonic, the rings he must collect and the enemies are all entities. Let’s create a file named entities.ts
in the src
folder. Then, let’s create a game object that will hold Sonic’s logic.
src
|- kaplayCtx.ts
|- main.ts
|- entities.ts
In entities.ts
add the following code.
import k from "./kaplayCtx";
import { Vec2, GameObj } from "kaplay";
export function makeSonic(position: Vec2) {
return k.add([
k.sprite("sonic", { anim: "run" }),
k.scale(3),
k.area(),
k.anchor("center"),
k.pos(position),
k.body({ jumpForce: 1700 }),
{
setControls(this: GameObj) {
k.onButtonPress("jump", () => {
if (this.isGrounded()) {
this.play("jump");
this.jump();
k.play("jump", { volume: 0.5 });
}
});
},
setEvents(this: GameObj) {
this.onGround(() => {
this.play("run");
});
},
},
]);
}
Let’s break it down.
import k from "./kaplayCtx";
import { Vec2, GameObj } from "kaplay";
We first import the KAPLAY context to be able to use the library’s functions in this file. We then import 2 types. Vec2
and GameObj
. As you can see, KAPLAY offers TypeScript types you can use, making development in TypeScript smoother.
export function makeSonic(position: Vec2) {
return k.add([
k.sprite("sonic", { anim: "run" }),
k.scale(3),
k.area(),
k.anchor("center"),
k.pos(position),
k.body({ jumpForce: 1700 }),
{
setControls(this: GameObj) {
k.onButtonPress("jump", () => {
if (this.isGrounded()) {
this.play("jump");
this.jump();
k.play("jump", { volume: 0.5 });
}
});
},
setEvents(this: GameObj) {
this.onGround(() => {
this.play("run");
});
},
},
]);
}
We then create a function who’s sole purpose is to create and return our Sonic game object. You can think of it as our constructor. We enable the function’s caller to set the position of the game object before it’s created. Positions in KAPLAY are Vec2s (stands for vector 2). It’s a data structure offered by KAPLAY which has two elements, x
and y
.
We use the sprite
, scale
, area
, anchor
, pos
and body
components to compose our game object.
Note that generally the pos
comp can be passed the x
and y
coordinates as two distinct params or be passed a Vec2
containing both at once. In our code above, we opted for the latter while with our background and platforms game objects, we opted for the former.
In the case of the sprite
comp, we not only display a sprite
but set an animation. You might be wondering where does this “sonic” sprite and “run” anim comes from since we haven’t loaded anything like that at the moment? Indeed, we put the cart before the horse in this case. Let’s add the asset loading logic in main.ts
. It will be a great opportunity to explain how animations work in KAPLAY.
How Animations Work in KAPLAY
In main.ts, add the following :
// place this behind last loadSprite call at the top of the file.
k.loadSprite("sonic", "graphics/sonic.png", {
sliceX: 8,
sliceY: 2,
anims: {
run: { from: 0, to: 7, loop: true, speed: 30 },
jump: { from: 8, to: 15, loop: true, speed: 100 },
},
});
The loadSprite
function can take an object as an optional third param used for telling KAPLAY how to split an image into individual frames. When an image contains multiple sprites, it is often referred to as a spritesheet.
In our case, Sonic’s spritesheet looks like this.
We have a total of 16 frames which can be viewed as a grid of 2 rows and 8 columns. The object passed to loadSprite
must first tell KAPLAY how to slice the image using the sliceX
and sliceY
properties. sliceX
corresponds to the number of columns while sliceY
corresponds to the number of rows.
Then, an anims
object can be configured to set the animations we need and tell KAPLAY which animation is composed of which frames in our spritesheet.
run: { from: 0, to: 7, loop: true, speed: 30 },
If we take a look at the definition for the run
animation, the from
property holds the number of the starting frame of the anim while the to
property holds the number of the last frame.
When KAPLAY slices the image into individual frames, it assigns a number to each of them starting from 0 and counting from left to right, top to bottom.
The loop
property set to true
makes the anim run indefinitely unless manually stopped while the speed
property is used to set its frame rate.
The frame rate values used in the code above are arbitrary. I came up with these values based on what looked good during gameplay after tweaking values.
For more details on how animations work in KAPLAY check the post below.
k.sprite("sonic", { anim: "run" }),
k.scale(3),
k.area(),
k.anchor("center"),
k.pos(position),
k.body({ jumpForce: 1700 }),
Looking back at the components used for creating the Sonic game object in entities.ts
, you’ll notice that the sprite
comp takes as the first param the name of the image you want to display while the second param can be used to set the default running animation.
We use the scale
comp to increase Sonic’s sprite size by 3. Which is what looked good during gameplay.
How Hitboxes Are Created in KAPLAY
The area
comp is very useful because it allows us to easily create a hitbox for our player enabling us to call methods like onCollide
, onCollideEnd
, etc… useful for dealing with collisions.
In KAPLAY, just passing the area
comp to the components array is enough to create a box surronding the player’s sprite if the sprite
comp is also used. You might need in some cases to configure the position, width and height of the hitbox but in our game, this isn’t necessary at the moment. We will tackle this later.
Now that we have used the area
comp, we can use the anchor
comp to set the origin of our game object. By default, game objects are rendered starting from their top-left corner. This might not be very intuitive for game objects representing characters, that’s why I like using the anchor
comp in those cases to set the center as the origin instead.
Finally, the body
comp is used to give the game object a physics body allowing them to be affected by gravity. Using the body
comp is crucial since it will give us access to the jump
method which allows the game object to jump. In our body
comp usage we set the game object’s jump force to be 1700. This is an arbitrary value which I arrived at after extensive testing during gameplay.
How to Add Methods to a Game Object
I explained earlier that a game object is composed of components. However, the add
function which takes in an array of components can also be passed, to that same array, a JS object that you can use to set custom properties and methods for the resulting game object.
{
setControls(this: GameObj) {
k.onButtonPress("jump", () => {
if (this.isGrounded()) {
this.play("jump");
this.jump();
k.play("jump", { volume: 0.5 });
}
});
},
setEvents(this: GameObj) {
this.onGround(() => {
this.play("run");
});
},
},
We have an object containing two custom methods. The first one is used to set the player controls while the second is used to set what happens when Sonic hits the ground after jumping.
In these methods, we can use the this
keyword, to access the sonic game object. In TypeScript, we need to specify what this
is. This is done by creating a this
param and typing it using the GameObj
type definition imported from KAPLAY. When TypeScript is compiled to vanilla JS, this param will be removed.
setControls(this: GameObj) {
k.onButtonPress("jump", () => {
if (this.isGrounded()) {
this.play("jump");
this.jump();
k.play("jump", { volume: 0.5 });
}
});
},
Taking a closer look at our custom setControls
function, we can see that we call an onButtonPress
KAPLAY function. As the name implies, this is used for handling when a specific button is pressed. It takes, as the first param, the name of the “button” you want to listen on. The second param is the function that will fire when that “button” is clicked.
If you remember, the “jump” “button” was defined in our KAPLAY context. This is where you need to define all the “buttons” you need for your game.
const k = kaplay({
//... omitted for clarity
buttons: {
jump: {
keyboard: ["space"],
mouse: "left",
},
},
//... rest of the code omitted for clarity
if (this.isGrounded()) {
this.play("jump");
this.jump();
k.play("jump", { volume: 0.5 });
}
Within our onButtonPress
we check if the player is on top of a static game object using the isGrounded
method which is available because Sonic has a body
comp. At the moment, we haven’t defined any static game objects but we will create one later.
When the player is indeed grounded, we can set the jump animation to play, make the player jump with the jump
method and finally play a jump sound using KAPLAY’s play
function which is used to play audio.
Unfortunately, it’s very easy to confuse the play
method which is used on a game object for playing animations VS the play
function which is used for sound.
By the way, we need to load the jump sound in main.ts
for it to work here. Add the following :
// place this behind last loadSprite call at the top of the file.
k.loadSound("jump", "sounds/Jump.wav");
The reason why we make sure the player is grounded before allowing any jump logic to run is so that the player can’t jump indefinitely making them able to fly away.
setEvents(this: GameObj) {
this.onGround(() => {
this.play("run");
});
},
Our second custom method is used to set event logic. In KAPLAY, when a game object has the body
component, you have access to the onGround
event method. That method runs a function every time the player hits the ground (meaning collides on top of a static game object).
When the player jumps, the jump animation plays indefinitely. We want to switch back to the run animation as soon as the player lands back. The code above achieves this.
To reiterate, setControls
and setEvents
are custom methods we decided to create on our Sonic game object. They could have been named differently. We could have decided to only have one method that does all of the required logic. This is up to you when making your own KAPLAY games.
Adding Sonic to Our Game
Let’s create a floor on which to place Sonic.
Creating a Static Body as The Floor
Add the following code in main.ts :
// previous code omitted for clarity
// const platforms = [...]
// static body for the platforms
k.add([
k.rect(1280, 200),
k.opacity(0),
k.pos(0, 641),
k.area(),
k.body({ isStatic: true }),
]);
// k.onUpdate(() => ...)
// rest of the code omitted for clarity
To create a static game object, you simply need to create a game object with an area
and a body
component. Within the body
comp, set the isStatic
property to true
.
We use the rect
comp to create a rectangular shape and we used the opacity
comp set to 0 to make it invisible. We also don’t assign the game object to a constant since we don’t need a reference.
If you look at the webpage where the game is running (assuming your project is running), you will see that nothing has changed. However, if you activate the debug mode by pressing the f1
key (or fn+f1
keys on a Mac), you’ll see an outline representing the hitbox game object we just created.
For fun, you can try to put the opacity back to 1 and you’ll see a white rectangle at the bottom of the canvas.
Placing Sonic in The Game Scene
In main.ts
, add the following :
// previous import statements omitted for clarity
import { makeSonic } from "./entities";
// ...
k.scene("game", () => {
// ... platforms game object definition
const sonic = makeSonic(k.vec2(100, 100));
sonic.setControls();
sonic.setEvents();
// ... static body for the platforms game object definition
});
// ...
We call makeSonic
and set a position using the vec2
KAPLAY function, assign the result to a constant called Sonic which enables us to call our custom methods setControls
and setEvents
, enabling our core player game logic. Everything should work now, but wait! Why is Sonic hanging in the air?
That’s because we haven’t set our game’s gravity. In KAPLAY, gravity is set per scene using the setGravity
function and passing to it a number determining how strong it will be.
Add the following code in our “game” scene definition :
k.scene("game", () => {
k.setGravity(3100);
// ...
});
// ...
I came up with 3100 after trial and error. You need to test during gameplay to see if the jump is floaty or not and that’s how I came up with this value.
Now, if you run the project. You should see Sonic initially fall on the platforms and immediately start running. You can now jump and when you do, Sonic should curl into a ball and you should hear a jump sound. However, as soon as he hits the ground, the run animation should be the one playing.
Conclusion
Up to this point we covered a lot of KAPLAY specific concepts and we made quite a bit of progress on our Sonic game. However, there’s still work to do. In the next part of the tutorial we will :
Add rings for Sonic to collect.
Implement a scoring system.
Add Motobug enemies that Sonic can jump on for more rings.
Implement a combo system allowing Sonic to earn extra rings by continuously jumping on Motobugs before hitting the ground again.
Update : Part 2 is now available!
How to Build a Sonic Themed Infinite Runner Game in TypeScript With KAPLAY - Part 2/2
In the previous part of the tutorial, we finished implementing Sonic’s movement and jumping logic. We also implemented platforms and background infinite scrolling.
Thank you for this very helpful tutorial. Much appreciated!!! 👍✌