// start the game
kaboom()
// 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: "sinko",
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),
doubleJump(),
// 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)
})
// get a list of all game objs with tag "bomb"
const allBombs = get("bomb")
// without args returns all current objs in the game
const allObjs = get()
// Destroy all game obj with tag "fruit"
every("fruit", destroy)
every((obj) => {})
// mainly useful when you want to make something to draw on top
readd(froggy)
// every time froggy collides with anything with tag "fruit", remove it
froggy.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 "froggy" sprite at (100, 200)
add([
pos(100, 200),
sprite("froggy"),
])
// blue frog
add([
sprite("froggy"),
color(0, 0, 255)
])
// minimal setup
add([
sprite("froggy"),
])
// with options
const froggy = add([
sprite("froggy", {
// start with animation "idle"
anim: "idle",
}),
])
// play / stop an anim
froggy.play("jump")
froggy.stop()
// manually setting a frame
froggy.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: "sink", // there're 4 built-in fonts: "apl386", "apl386o", "sink", and "sinko"
}),
])
// 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("froggy"),
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
origin("center"),
])
add([
sprite("froggy"),
// Define custom area with width and height
area({ width: 20, height: 40. }),
])
// set origin to "center" so it'll rotate from center
add([
rect(40, 10),
rotate(45),
origin("center"),
])
// froggy jumpy
const froggy = add([
sprite("froggy"),
// body() requires "pos" and "area" component
pos(),
area(),
body(),
])
// when froggy is grounded, press space to jump
// check out #BodyComp for more methods
onKeyPress("space", () => {
if (froggy.isGrounded()) {
froggy.jump()
}
})
// run something when froggy falls and hits a ground
froggy.onGround(() => {
debug.log("oh no!")
})
add([
sprite("rock"),
pos(30, 120),
area(),
solid(),
])
// only do collision checking when a block is close to player for performance
onUpdate("block", (b) => {
b.solid = b.pos.dist(player.pos) <= 64
})
// enemy throwing feces at player
const projectile = add([
sprite("feces"),
pos(enemy.pos),
area(),
move(player.pos.angle(enemy.pos), 1200),
cleanup(),
])
add([
pos(1200, 80),
outview({ hide: true, pause: true }),
])
// destroy when it leaves screen
const bullet = add([
pos(80, 80),
move(LEFT, 960),
cleanup(),
])
// 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()
})
// 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 froggy = add([
sprite("froggy"),
])
// certain assets related data are only available when the game finishes loading
onLoad(() => {
debug.log(froggy.width)
})
onCollide("sun", "earth", () => {
addExplosion()
})
// 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", () => {
froggy.move(-SPEED, 0)
})
// .jump() once when "space" is just being pressed
onKeyPress("space", () => {
froggy.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("froggy", "sprites/froggy.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("froggy", "froggy.png")
loadSprite("apple", "https://kaboomjs.com/sprites/apple.png")
// slice a spritesheet and add anims manually
loadSprite("froggy", "froggy.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 bitmap font called "04b03", with bitmap "fonts/04b03.png"
// each character on bitmap has a size of (6, 8), and contains default ASCII_CHARS
loadFont("04b03", "fonts/04b03.png", 6, 8)
// load a font with custom characters
loadFont("cp437", "cp437.png", 6, 8, {chars: "☺☻♥♦♣♠"})
// load only a fragment shader from URL
loadShader("outline", null, "/shaders/outline.glsl", true)
// default shaders and custom shader format
loadShader("outline",
`vec4 vert(vec3 pos, vec2 uv, vec4 color) {
// predefined functions to get the default value by kaboom
return def_vert()
}`,
`vec4 frag(vec3 pos, vec2 uv, vec4 color, sampler2D tex) {
// turn everything blue-ish
return def_frag() * vec4(0, 0, 1, 1)
}`, false)
load(new Promise((resolve, reject) => {
// anything you want to do that stalls the game in loading state
resolve("ok")
}))
// add froggy to the center of the screen
add([
sprite("froggy"),
pos(center()),
// ...
])
// rotate froggy 100 deg per second
froggy.onUpdate(() => {
froggy.angle += 100 * dt()
})
// equivalent to the calling froggy.move() in an onKeyDown("left")
onUpdate(() => {
if (isKeyDown("left")) {
froggy.move(-SPEED, 0)
}
})
// shake intensively when froggy collides with a "bomb"
froggy.onCollide("bomb", () => {
shake(120)
})
// camera follows player
player.onUpdate(() => {
camPos(player.pos)
})
// defining 3 layers, "ui" will be drawn on top most, with default layer being "game"
layers([
"bg",
"game",
"ui",
], "game")
// use layer() comp to define which layer an obj belongs to
add([
text(score),
layer("ui"),
fixed(),
])
// without layer() comp it'll fall back to default layer, which is "game"
add([
sprite("froggy"),
])
onHover("clickable", (c) => {
cursor("pointer")
})
loadSprite("froggy", "sprites/froggy.png")
// use sprite as cursor
regCursor("default", "froggy")
regCursor("pointer", "apple")
// toggle fullscreen mode on "f"
onKeyPress("f", (c) => {
fullscreen(!isFullscreen())
})
// 3 seconds until explosion! Runnn!
wait(3, () => {
explode()
})
// 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.pause()
music.play()
// 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(0, 3) // will give either 0, 1, or 2
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)
}
})
// 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)
})
addLevel([
" $",
" $",
" $$ = $",
" % ==== = $",
" = ",
" ^^ = > = &",
"===========================",
], {
// define the size of each block
width: 32,
height: 32,
// define what each symbol means, by a function returning a component list (what will be passed to add())
"=": () => [
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: "froggy",
pos: vec2(100, 200),
frame: 3,
})
drawText({
text: "oh hi",
size: 48,
font: "sink",
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("froggy")
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),
})
// 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 modef5
to take screenshotf6
to toggle recordingf7
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)