// start the game
kaboom()
// define gravity
setGravity(2400)
// load a default sprite
loadBean()
// add character to screen, from a list of components
const player = add([
sprite("bean"), // renders as a sprite
pos(120, 80), // position in world
area(), // has a collider
body(), // responds to physics and gravity
])
// jump when player presses "space" key
onKeyPress("space", () => {
// .jump() is provided by the body() component
player.jump()
})
Play with it yourself or check out the examples in the Playground!
// Start kaboom with default options (will create a fullscreen canvas under <body>)
kaboom()
// Init with some options (check out #KaboomOpt for full options list)
kaboom({
width: 320,
height: 240,
font: "sans-serif",
canvas: document.querySelector("#mycanvas"),
background: [ 0, 0, 255, ],
})
// All kaboom functions are imported to global after calling kaboom()
add()
onUpdate()
onKeyPress()
vec2()
// If you want to prevent kaboom from importing all functions to global and use a context handle for all kaboom functions
const k = kaboom({ global: false })
k.add(...)
k.onUpdate(...)
k.onKeyPress(...)
k.vec2(...)
Game Object is the basic unit of entity in a kaboom world. Everything is a game object, the player, a butterfly, a tree, or even a piece of text.
This section contains functions to add, remove, and access game objects. To actually make them do stuff, check out the Components section.
const player = add([
// List of components, each offers a set of functionalities
sprite("mark"),
pos(100, 200),
area(),
body(),
health(8),
// Plain strings are tags, a quicker way to let us define behaviors for a group
"player",
"friendly",
// Components are just plain objects, you can pass an object literal as a component.
{
dir: LEFT,
dead: false,
speed: 240,
},
])
// .jump is provided by body()
player.jump()
// .moveTo is provided by pos()
player.moveTo(300, 200)
// .onUpdate() is on every game object, it registers an event that runs every frame
player.onUpdate(() => {
// .move() is provided by pos()
player.move(player.dir.scale(player.speed))
})
// .onCollide is provided by area()
player.onCollide("tree", () => {
destroy(player)
})
const label = make([
text("oh hi"),
])
add([
rect(label.width, label.height),
color(0, 0, 255),
children(label),
])
// get a list of all game objs with tag "bomb"
const allBombs = get("bomb")
// To get all objects use "*"
const allObjs = get("*")
// Recursively get all children and descendents
const allObjs = get("*", { recursive: true })
// every time bean collides with anything with tag "fruit", remove it
bean.onCollide("fruit", (fruit) => {
destroy(fruit)
})
// destroy all objects with tag "bomb" when you click one
onClick("bomb", () => {
destroyAll("bomb")
})
Kaboom uses a flexible component system which values composition over inheritence. Each game object is composed from a list of components, each component gives the game object certain capabilities.
Use add()
to assemble the components together into a Game Object and add them to the world.
const player = add([
sprite("froggy"),
pos(100, 200),
area(),
body(),
])
// .jump() is provided by body() component
player.jump()
// .moveTo() is provided by pos() component
player.moveTo(120, 80)
// .onCollide() is provided by the area() component
player.onCollide("enemy", (enemy) => {
destroy(enemy)
addExplosion()
})
To see what methods and properties a component offers, click on the type that the component function returns, e.g. PosComp
, which will open a panel showing all the properties and methods it'd give the game object.
To learn more about how components work or how to make your own component, check out the component demo.
// This game object will draw a "bean" sprite at (100, 200)
add([
pos(100, 200),
sprite("bean"),
])
// blue frog
add([
sprite("bean"),
color(0, 0, 255)
])
// minimal setup
add([
sprite("bean"),
])
// with options
const bean = add([
sprite("bean", {
// start with animation "idle"
anim: "idle",
}),
])
// play / stop an anim
bean.play("jump")
bean.stop()
// manually setting a frame
bean.frame = 3
// a simple score counter
const score = add([
text("Score: 0"),
pos(24, 24),
{ value: 0 },
])
player.onCollide("coin", () => {
score.value += 1
score.text = "Score:" + score.value
})
// with options
add([
pos(24, 24),
text("ohhi", {
size: 48, // 48 pixels tall
width: 320, // it'll wrap to next line when width exceeds this value
font: "sans-serif", // specify any font you loaded or browser built-in
}),
])
// i don't know, could be an obstacle or something
add([
pos(80, 120),
rect(20, 40),
outline(4),
area(),
])
add([
pos(80, 120),
circle(16),
])
add([
uvquad(width(), height()),
shader("spiral"),
])
// Automatically generate area information from the shape of render
const player = add([
sprite("bean"),
area(),
])
// Die if player collides with another game obj with tag "tree"
player.onCollide("tree", () => {
destroy(player)
go("lose")
})
// Check for collision manually every frame instead of registering an event
player.onUpdate(() => {
if (player.isColliding(bomb)) {
score += 1
}
})
add([
sprite("flower"),
// Scale to 0.6 of the generated area
area({ scale: 0.6 }),
// If we want the area scale to be calculated from the center
anchor("center"),
])
add([
sprite("bean"),
// Define area with custom shape
area({ shape: new Polygon([vec2(0), vec2(100), vec2(-100, 100)]) }),
])
// set anchor to "center" so it'll rotate from center
add([
rect(40, 10),
rotate(45),
anchor("center"),
])
// bean jumpy
const bean = add([
sprite("bean"),
// body() requires "pos" and "area" component
pos(),
area(),
body(),
])
// when bean is grounded, press space to jump
// check out #BodyComp for more methods
onKeyPress("space", () => {
if (bean.isGrounded()) {
bean.jump()
}
})
// run something when bean falls and hits a ground
bean.onGround(() => {
debug.log("oh no!")
})
// enemy throwing feces at player
const projectile = add([
sprite("feces"),
pos(enemy.pos),
area(),
move(player.pos.angle(enemy.pos), 1200),
offscreen({ destroy: true }),
])
add([
pos(player.pos),
sprite("bullet"),
offscreen({ destroy: true }),
"projectile",
])
const obj = add([
timer(),
])
obj.wait(2, () => { ... })
obj.loop(0.5, () => { ... })
obj.tween(obj.pos, mousePos(), 0.5, (p) => obj.pos = p, easings.easeOutElastic)
// this will be be fixed on top left and not affected by camera
const score = add([
text(0),
pos(12, 12),
fixed(),
])
player.onCollide("bomb", () => {
// spawn an explosion and switch scene, but don't destroy the explosion game obj on scene switch
add([
sprite("explosion", { anim: "burst", }),
stay(),
lifespan(1),
])
go("lose", score)
})
const player = add([
health(3),
])
player.onCollide("bad", (bad) => {
player.hurt(1)
bad.hurt(1)
})
player.onCollide("apple", () => {
player.heal(1)
})
player.on("hurt", () => {
play("ouch")
})
// triggers when hp reaches 0
player.on("death", () => {
destroy(player)
go("lose")
})
// spawn an explosion, destroy after 1 seconds, start fading away after 0.5 second
add([
sprite("explosion", { anim: "burst", }),
lifespan(1, { fade: 0.5 }),
])
const enemy = add([
pos(80, 100),
sprite("robot"),
state("idle", ["idle", "attack", "move"]),
])
// this callback will run once when enters "attack" state
enemy.onStateEnter("attack", () => {
// enter "idle" state when the attack animation ends
enemy.play("attackAnim", {
// any additional arguments will be passed into the onStateEnter() callback
onEnd: () => enemy.enterState("idle", rand(1, 3)),
})
checkHit(enemy, player)
})
// this will run once when enters "idle" state
enemy.onStateEnter("idle", (time) => {
enemy.play("idleAnim")
wait(time, () => enemy.enterState("move"))
})
// this will run every frame when current state is "move"
enemy.onStateUpdate("move", () => {
enemy.follow(player)
if (enemy.pos.dist(player.pos) < 16) {
enemy.enterState("attack")
}
})
const enemy = add([
pos(80, 100),
sprite("robot"),
state("idle", ["idle", "attack", "move"], {
"idle": "attack",
"attack": "move",
"move": [ "idle", "attack" ],
}),
])
// this callback will only run once when enter "attack" state from "idle"
enemy.onStateTransition("idle", "attack", () => {
checkHit(enemy, player)
})
Kaboom uses events extensively for a flat and declarative code style.
For example, it's most common for a game to have something run every frame which can be achieved by adding an onUpdate()
event
// Make something always move to the right
onUpdate(() => {
banana.move(320, 0)
})
Events are also used for input handlers.
onKeyPress("space", () => {
player.jump()
})
Every function with the on
prefix is an event register function that takes a callback function as the last argument, and should return a function that cancels the event listener.
Note that you should never nest one event handler function inside another or it might cause severe performance punishment.
// a custom event defined by body() comp
// every time an obj with tag "bomb" hits the floor, destroy it and addKaboom()
on("ground", "bomb", (bomb) => {
destroy(bomb)
addKaboom(bomb.pos)
})
// move every "tree" 120 pixels per second to the left, destroy it when it leaves screen
// there'll be nothing to run if there's no "tree" obj in the scene
onUpdate("tree", (tree) => {
tree.move(-120, 0)
if (tree.pos.x < 0) {
destroy(tree)
}
})
// This will run every frame
onUpdate(() => {
debug.log("ohhi")
})
onDraw(() => {
drawLine({
p1: vec2(0),
p2: mousePos(),
color: rgb(0, 0, 255),
})
})
const bean = add([
sprite("bean"),
])
// certain assets related data are only available when the game finishes loading
onLoad(() => {
debug.log(bean.width)
})
onCollide("sun", "earth", () => {
addExplosion()
})
onCollideUpdate("sun", "earth", () => {
runWorldEndTimer()
})
onCollideEnd("bean", "earth", () => {
worldEnd()
})
// click on any "chest" to open
onClick("chest", (chest) => chest.open())
// click on anywhere to go to "game" scene
onClick(() => go("game"))
// move left by SPEED pixels per frame every frame when left arrow key is being held down
onKeyDown("left", () => {
bean.move(-SPEED, 0)
})
// .jump() once when "space" is just being pressed
onKeyPress("space", () => {
bean.jump()
})
// Call restart() when player presses any key
onKeyPress(() => {
restart()
})
// delete last character when "backspace" is being pressed and held
onKeyPressRepeat("backspace", () => {
input.text = input.text.substring(0, input.text.length - 1)
})
// type into input
onCharInput((ch) => {
input.text += ch
})
Every function with the load
prefix is an async function that loads something into the asset manager, and should return a promise that resolves upon load complete.
loadRoot("https://myassets.com/")
loadSprite("bean", "sprites/bean.png") // will resolve to "https://myassets.com/sprites/frogg.png"
// due to browser policies you'll need a static file server to load local files
loadSprite("bean", "bean.png")
loadSprite("apple", "https://kaboomjs.com/sprites/apple.png")
// slice a spritesheet and add anims manually
loadSprite("bean", "bean.png", {
sliceX: 4,
sliceY: 1,
anims: {
run: {
from: 0,
to: 3,
},
jump: {
from: 3,
to: 3,
},
},
})
// See #SpriteAtlasData type for format spec
loadSpriteAtlas("sprites/dungeon.png", {
"hero": {
x: 128,
y: 68,
width: 144,
height: 28,
sliceX: 9,
anims: {
idle: { from: 0, to: 3 },
run: { from: 4, to: 7 },
hit: 8,
},
},
})
const player = add([
sprite("hero"),
])
player.play("run")
// Load from json file, see #SpriteAtlasData type for format spec
loadSpriteAtlas("sprites/dungeon.png", "sprites/dungeon.json")
const player = add([
sprite("hero"),
])
player.play("run")
loadAseprite("car", "sprites/car.png", "sprites/car.json")
loadBean()
// use it right away
add([
sprite("bean"),
])
loadSound("shoot", "horse.ogg")
loadSound("shoot", "https://kaboomjs.com/sounds/scream6.mp3")
// load a font from a .ttf file
loadFont("frogblock", "fonts/frogblock.ttf")
// load a bitmap font called "04b03", with bitmap "fonts/04b03.png"
// each character on bitmap has a size of (6, 8), and contains default ASCII_CHARS
loadBitmapFont("04b03", "fonts/04b03.png", 6, 8)
// load a font with custom characters
loadBitmapFont("myfont", "myfont.png", 6, 8, { chars: "☺☻♥♦♣♠" })
// default shaders and custom shader format
loadShader("outline",
`vec4 vert(vec2 pos, vec2 uv, vec4 color) {
// predefined functions to get the default value by kaboom
return def_vert();
}`,
`vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {
// turn everything blue-ish
return def_frag() * vec4(0, 0, 1, 1);
}`, false)
// load only a fragment shader from URL
loadShader("outline", null, "/shaders/outline.glsl", true)
load(new Promise((resolve, reject) => {
// anything you want to do that stalls the game in loading state
resolve("ok")
}))
// add bean to the center of the screen
add([
sprite("bean"),
pos(center()),
// ...
])
// rotate bean 100 deg per second
bean.onUpdate(() => {
bean.angle += 100 * dt()
})
// equivalent to the calling bean.move() in an onKeyDown("left")
onUpdate(() => {
if (isKeyDown("left")) {
bean.move(-SPEED, 0)
}
})
// shake intensively when bean collides with a "bomb"
bean.onCollide("bomb", () => {
shake(120)
})
// camera follows player
player.onUpdate(() => {
camPos(player.pos)
})
button.onHover((c) => {
setCursor("pointer")
})
// toggle fullscreen mode on "f"
onKeyPress("f", (c) => {
fullscreen(!isFullscreen())
})
// 3 seconds until explosion! Runnn!
wait(3, () => {
explode()
})
// wait() returns a PromiseLike that can be used with await
await wait(1)
// spawn a butterfly at random position every 1 second
loop(1, () => {
add([
sprite("butterfly"),
pos(rand(vec2(width(), height()))),
area(),
"friend",
])
})
// play a one off sound
play("wooosh")
// play a looping soundtrack (check out AudioPlayOpt for more options)
const music = play("OverworldlyFoe", {
volume: 0.8,
loop: true
})
// using the handle to control (check out AudioPlay for more controls / info)
music.paused = true
music.speed = 1.2
// makes everything quieter
volume(0.5)
// a random number between 0 - 8
rand(8)
// a random point on screen
rand(vec2(width(), height()))
// a random color
rand(rgb(255, 255, 255))
rand(50, 100)
rand(vec2(20), vec2(100))
// spawn something on the right side of the screen but with random y value within screen height
add([
pos(width(), rand(0, height())),
])
randi(10) // returns 0 to 9
randi(0, 3) // returns 0, 1, or 2
randi() // returns either 0 or 1
randSeed(Date.now())
// { x: 0, y: 0 }
vec2()
// { x: 10, y: 10 }
vec2(10)
// { x: 100, y: 80 }
vec2(100, 80)
// move to 150 degrees direction with by length 10
player.pos = pos.add(Vec2.fromAngle(150).scale(10))
// update the color of the sky to light blue
sky.color = rgb(0, 128, 255)
// animate rainbow color
onUpdate("rainbow", (obj) => {
obj.color = hsl2rgb(wave(0, 1, time()), 0.6, 0.6)
})
// decide the best fruit randomly
const bestFruit = choose(["apple", "banana", "pear", "watermelon"])
// every frame all objs with tag "unlucky" have 50% chance die
onUpdate("unlucky", (o) => {
if (chance(0.5)) {
destroy(o)
}
})
// tween bean to mouse position
tween(bean.pos, mousePos(), 1, (p) => bean.pos = p, easings.easeOutBounce)
// tween() returns a then-able that can be used with await
await tween(bean.opacity, 1, 0.5, (val) => bean.opacity = val, easings.easeOutQuad)
// bounce color between 2 values as time goes on
onUpdate("colorful", (c) => {
c.color.r = wave(0, 255, time())
c.color.g = wave(0, 255, time() + 1)
c.color.b = wave(0, 255, time() + 2)
})
Color.fromHex(0xfcef8d)
Color.fromHex("#5ba675")
Color.fromHex("d46eb3")
addLevel([
" $",
" $",
" $$ = $",
" % ==== = $",
" = ",
" ^^ = > = &",
"===========================",
], {
// define the size of tile block
tileWidth: 32,
tileHeight: 32,
// define what each symbol means, by a function returning a component list (what will be passed to add())
tiles: {
"=": () => [
sprite("floor"),
area(),
solid(),
],
"$": () => [
sprite("coin"),
area(),
pos(0, -9),
],
"^": () => [
sprite("spike"),
area(),
"danger",
],
}
})
Kaboom exposes all of the drawing interfaces it uses in the render components like sprite()
, and you can use these drawing functions to build your own richer render components.
Also note that you have to put drawXXX()
functions inside an onDraw()
event or the draw()
hook in component definitions which runs every frame (after the update
events), or it'll be immediately cleared next frame and won't persist.
onDraw(() => {
drawSprite({
sprite: "froggy",
pos: vec2(120, 160),
angle: 90,
})
drawLine({
p1: vec2(0),
p2: mousePos(),
width: 4,
color: rgb(0, 0, 255),
})
})
There's also the option to use Kaboom purely as a rendering library. Check out the draw demo.
drawSprite({
sprite: "bean",
pos: vec2(100, 200),
frame: 3,
})
drawText({
text: "oh hi",
size: 48,
font: "sans-serif",
width: 120,
pos: vec2(100, 200),
color: rgb(0, 0, 255),
})
drawRect({
width: 120,
height: 240,
pos: vec2(20, 20),
color: YELLOW,
outline: { color: BLACK, width: 4 },
})
drawLine({
p1: vec2(0),
p2: mousePos(),
width: 4,
color: rgb(0, 0, 255),
})
drawLines({
pts: [ vec2(0), vec2(0, height()), mousePos() ],
width: 4,
pos: vec2(100, 200),
color: rgb(0, 0, 255),
})
drawTriangle({
p1: vec2(0),
p2: vec2(0, height()),
p3: mousePos(),
pos: vec2(100, 200),
color: rgb(0, 0, 255),
})
drawCircle({
pos: vec2(100, 200),
radius: 120,
color: rgb(255, 255, 0),
})
drawEllipse({
pos: vec2(100, 200),
radiusX: 120,
radiusY: 120,
color: rgb(255, 255, 0),
})
drawPolygon({
pts: [
vec2(-12),
vec2(0, 16),
vec2(12, 4),
vec2(0, -2),
vec2(-8),
],
pos: vec2(100, 200),
color: rgb(0, 0, 255),
})
// text background
const txt = formatText({
text: "oh hi",
})
drawRect({
width: txt.width,
height: txt.height,
})
drawFormattedText(txt)
pushTransform()
// these transforms will affect every render until popTransform()
pushTranslate(120, 200)
pushRotate(time() * 120)
pushScale(6)
drawSprite("bean")
drawCircle(vec2(0), 120)
// restore the transformation stack to when last pushed
popTransform()
pushTranslate(100, 100)
// this will be drawn at (120, 120)
drawText({
text: "oh hi",
pos: vec2(20, 20),
})
loadShader("invert", null, `
vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {
vec4 c = def_frag();
return vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a);
}
`)
usePostEffect("invert")
// text background
const txt = formatText({
text: "oh hi",
})
drawRect({
width: txt.width,
height: txt.height,
})
drawFormattedText(txt)
By default kaboom starts in debug mode, which enables key bindings that calls out various debug utilities:
f1
to toggle inspect modef2
to clear debug consolef7
to slow downf8
to pause / resumef9
to speed upf10
to skip frameSome of these can be also controlled with stuff under the debug
object.
If you want to turn debug mode off when releasing you game, set debug
option to false in kaboom()
// pause the whole game
debug.paused = true
// enter inspect mode
debug.inspect = true
// play a random note in the octave
play("noteC", {
detune: randi(0, 12) * 100,
})
// tune down a semitone
music.detune = -100
// tune up an octave
music.detune = 1200
Color.fromHex(0xfcef8d)
Color.fromHex("#5ba675")
Color.fromHex("d46eb3")