Asset Loading
Load assets into asset manager. These should be at application top.
loadRoot(path?: string) => string
Set the url loader root
loadRoot("/assets/");
loadSprite("froggy", "sprites/froggy.png"); // will resolve to "/assets/sprites/froggy.png"
loadSprite(id: string, src: SpriteLoadSrc, conf?: SpriteLoadConf) => Promise<SpriteData>
Load a sprite into the asset manager
// due to browser policies you'll need a static file server to load local files, e.g.
// - (with python) $ python3 -m http.server $PORT
// - (with caddy) $ caddy file-server --browse --listen $PORT
// - https://github.com/vercel/serve
// - https://github.com/http-party/http-server
loadSprite("froggy", "froggy.png");
loadSprite("froggy", "https://kaboomjs.com/assets/sprites/mark.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,
},
},
});
loadSound(id: string, src: string) => Promise<SoundData>
Load a sound
loadSound("shoot", "horse.ogg");
loadSound("shoot", "https://kaboomjs.com/assets/sounds/scream6.mp3");
loadFont(id: string, src: string, gw: number, gh: number, chars?: string) => Promise<FontData>
Load a font
// default character mappings: (ASCII 32 - 126)
// const ASCII_CHARS = " !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
// load a bitmap font called "04b03", with bitmap "04b03.png"
// each character on bitmap has a size of (6, 8), and contains default ASCII_CHARS
loadFont("04b03", "04b03.png", 6, 8);
// load a font with custom characters
loadFont("CP437", "CP437.png", 6, 8, "☺☻♥♦♣♠");
loadShader(name: string, vert?: string, frag?: string, isUrl?: boolean) => Promise<ShaderData>
Load a shader
// 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) {
return def_frag();
}`, true);
Objects
Game Object is the basic unit of Kaboom, each game object uses components to compose their data and behavior.
add(comps: ReadonlyArray<T | Tag | CustomData>) => GameObj<T>
add a game object to scene
// a game object consists of a list of components
const player = add([
// a 'sprite' component gives it the render ability
sprite("froggy"),
// a 'pos' component gives it a position
pos(100, 100),
// a 'body' component makes it fall and gives it jump()
body(),
// raw strings are tags
"player",
"killable",
// custom fields are assigned directly to the returned obj ref
{
dir: vec2(-1, 0),
dead: false,
speed: 240,
},
]);
player.action(() => {
player.move(player.dir.scale(player.speed));
});
player.hidden = false; // if this obj renders
player.paused = true // if this obj updates
// runs every frame as long as player is not 'destroy()'ed
player.action(() => {
player.move(100, 0);
});
// provided by 'sprite()'
player.play("jump"); // play a spritesheet animation
console.log(player.frame); // get current frame
// provided by 'pos()'
player.move(100, 20);
console.log(player.pos);
// provided by 'body()'
player.jump(320); // make player jump
update the object, the callback is run every frame
player.action(() => {
player.move(SPEED, 0);
});
add a component to a game object
// rarely needed since you usually specify all comps in the 'add()' step
obj.use(scale(2, 2));
check if obj exists in scene
// sometimes you might keep a reference of an object that's already 'destroy()'ed
// use exists() to check if they were
if (obj.exists()) {
child.pos = obj.pos.clone();
}
if obj has certain tag(s)
if (obj.is("killable")) {
destroy(obj);
}
listen to an event
// when obj is 'destroy()'ed
obj.on("destroy", () => {
add([
sprite("explosion"),
]);
});
// runs every frame when obj exists
obj.on("update", () => {
// ...
});
// custom event from comp 'body()'
obj.on("grounded", () => {
// ...
});
trigger an event
obj.on("grounded", () => {
obj.jump();
});
// mainly for custom components defining custom events
obj.trigger("grounded");
destroy(obj: GameObj<any>) => void
remove a game object from scene
collides("bullet", "killable", (b, k) => {
// remove both the bullet and the thing bullet hit with tag "killable" from scene
destroy(b);
destroy(k);
score++;
});
destroyAll(tag: string) => void
destroy every obj with a certain tag
destroyAll("enemy");
get(tag?: string) => GameObj<any>[]
get a list of obj reference with a certain tag
const enemies = get("enemy");
const allObjs = get();
every(f: (obj: GameObj<any>) => T) => T[]
run a callback on every obj with a certain tag
// equivalent to destroyAll("enemy")
every("enemy", (obj) => {
destroy(obj);
});
// without tag iterate every object
every((obj) => {
// ...
});
revery(f: (obj: GameObj<any>) => T) => T[]
like every but runs in reversed order
readd(obj: GameObj<any>) => GameObj<any>
re-add an object to the scene
// remove and add froggy to the scene without triggering events tied to "add" or "destroy"
// so it'll be drawn on the top of the layer it belongs to
readd(froggy);
helper for adding a sprite
addSprite("mark", {
pos: vec2(80, 80),
body: true,
origin: "center",
flipX: true,
tags: [ "player", "killable" ],
});
// is equivalent to
add([
sprite({ flipX: true }),
body(),
origin("center"),
"player",
"killable",
]);
helper for adding a text
addText("oh hi", 32 {
pos: vec2(80, 80),
});
// is equivalent to
add([
text("oh hi", 32),
pos(80, 80),
]);
helper for adding a rect
addRect(100, 100);
// is equivalent to
add([
rect(32, 32),
]);
Components
Built-in components. Each component gives the game object certain data / behaviors.
pos() => PosComp
object's position
const obj = add([
pos(0, 50),
// also accepts Vec2
// pos(vec2(0, 50)),
]);
// get the current position in vec2
console.log(obj.pos);
// move by velocity (pixels per second, dt() is multiplied)
obj.move(100, 100);
// move to a destination 10 pixels per second
obj.moveTo(vec2(120), 10);
// teleport to destination
obj.moveTo(vec2(120));
scale() => ScaleComp
scale
const obj = add([
scale(2),
// also accepts Vec2
// scale(vec2(2, 2)),
]);
// get the current scale in vec2
console.log(obj.scale);
rotate(a: number) => RotateComp
rotate
const obj = add([
rotate(2),
]);
obj.action(() => {
obj.angle += dt();
});
color() => ColorComp
color
const obj = add([
sprite("froggy"),
// give it a blue tint
color(0, 0, 1),
// also accepts Color
// color(rgba(0, 0, 1, 0.5))
]);
obj.color = rgb(1, 0, 0); // make it red instead
sprite(id: string, conf?: SpriteCompConf) => SpriteComp
sprite rendering component
const obj = add([
// sprite is loaded by loadSprite("froggy", src)
sprite("froggy"),
]);
const obj = add([
sprite("froggy", {
animSpeed: 0.3, // time per frame (defaults to 0.1)
frame: 2, // start frame (defaults to 0)
}),
]);
// get / set current frame
obj.frame = obj.numFrames() - 1;
// play animation
obj.play("jump");
// stop the anim
obj.stop();
console.log(obj.curAnim());
console.log(obj.width);
console.log(obj.height);
obj.on("animEnd", (anim) => {
if (anim === "jump") {
obj.play("fall");
}
});
// could change sprite for anim if you don't use spritesheet
obj.changeSprite("froggy_left");
text(t: string, size?: number, conf?: TextCompConf) => TextComp
text rendering component
const obj = add([
// content, size
text("oh hi", 64),
]);
const obj = add([
text("oh hi", 64, {
width: 120, // wrap when exceeds this width (defaults to 0 - no wrap)
font: "proggy", // font to use (defaults to "unscii")
}),
]);
// update the content
obj.text = "oh hi mark";
rect(w: number, h: number) => RectComp
rect rendering component
const obj = add([
// width, height
rect(50, 75),
pos(25, 25),
color(0, 1, 1),
]);
// update size
obj.width = 75;
obj.height = 75;
area(p1: Vec2, p2: Vec2) => AreaComp
a rectangular area for collision checking
const obj = add([
sprite("froggy"),
// empty area() will try to calculate area from visual components like sprite(), rect() and text()
area(),
// override to a smaller region
area(vec2(6), vec2(24)),
]);
// callback when it collides with a certain tag
obj.collides("collectable", (c) => {
destroy(c);
score++;
});
// similar to collides(), but doesn't pass if 2 objects are just touching each other (checks for distance < 0 instead of distance <= 0)
obj.overlaps("collectable", (c) => {
destroy(c);
score++;
});
// checks if the obj is collided with another
if (obj.isCollided(obj2)) {
// ...
}
if (obj.isOverlapped(obj2)) {
// ...
}
// register an onClick callback
obj.clicks(() => {
// ...
});
// if the obj is clicked last frame
if (obj.isClicked()) {
// ...
}
// register an onHover callback
obj.hovers(() => {
// ...
});
// if the obj is currently hovered
if (obj.isHovered()) {
// ...
}
// check if a point is inside the obj area
obj.hasPt();
// pushOutAll resolves all collisions with objects with 'solid'
// for now this checks against all solid objs in the scene (this is costly now)
obj.pushOutAll();
body(conf?: BodyCompConf) => BodyComp
component for falling / jumping
const player = add([
pos(0, 0),
// now player will fall in this gravity world
body(),
]);
const player = add([
pos(0, 0),
body({
// force of .jump()
jumpForce: 640,
// maximum fall velocity
maxVel: 2400,
}),
]);
// body() gives obj jump() and grounded() methods
keyPress("up", () => {
if (player.grounded()) {
player.jump(JUMP_FORCE);
}
});
// and a "grounded" event
player.on("grounded", () => {
console.log("horray!");
});
solid() => SolidComp
mark the obj so other objects can't move past it if they have an area and pushOutAll()
const obj = add([
sprite("wall"),
solid(),
]);
// need to call pushOutAll() (provided by 'area') to make sure they cannot move past solid objs
player.action(() => {
player.pushOutAll();
});
origin(o: Origin | Vec2) => OriginComp
the origin to draw the object (default topleft)
const obj = add([
sprite("froggy"),
// defaults to "topleft"
origin("topleft"),
// other options
origin("top"),
origin("topright"),
origin("left"),
origin("center"),
origin("right"),
origin("botleft"),
origin("bot"),
origin("botright"),
origin(vec2(0, 0.25)), // custom
]);
layer(l: string) => LayerComp
specify the layer to draw on
layers([
"bg",
"game",
"ui",
], "game");
add([
sprite("sky"),
layer("bg"),
]);
// we specified "game" to be default layer above, so a manual layer() comp is not needed
const player = add([
sprite("froggy"),
]);
const score = add([
text("0"),
layer("ui"),
]);
Events
kaboom uses tags to group objects and describe their behaviors, functions below all accepts the tag as first arguments, following a callback
action(cb: () => void) => EventCanceller
calls every frame for a certain tag
// every frame move objs with tag "bullet" up with speed of 100
action("bullet", (b) => {
b.move(vec2(0, 100));
});
action("flashy", (f) => {
f.color = rand(rgb(0, 0, 0), rgb(1, 1, 1));
});
// plain action() just runs every frame not tied to any object
action(() => {
console.log("oh hi")
});
render(cb: () => void) => EventCanceller
calls every frame for a certain tag (after update)
// define custom drawing for objects with tag "weirdo"
render("weirdo", (b) => {
drawSprite(...);
drawRect(...);
drawText(...);
});
// plain render() just runs every frame
// with plain action() and render() you can opt out of the component / obj system and use you own loop
render(() => {
drawSprite(...);
});
collides(t1: string, t2: string, f: (a: GameObj<any>, b: GameObj<any>) => void) => EventCanceller
calls when objects collides with others
collides("enemy", "bullet", (e, b) => {
destroy(b);
e.life--;
if (e.life <= 0) {
destroy(e);
}
});
// NOTE: Objects on different layers won't collide! Collision handlers won't pick them up.
overlaps(t1: string, t2: string, f: (a: GameObj<any>, b: GameObj<any>) => void) => EventCanceller
calls when objects collides with others
// similar to collides(), but doesn't pass if 2 objects are just touching each other (checks for distance < 0 instead of distance <= 0)
overlaps("enemy", "bullet", (e, b) => {
destroy(b);
e.life--;
if (e.life <= 0) {
destroy(e);
}
});
on(event: string, tag: string, cb: (obj: GameObj<any>) => void) => EventCanceller
add lifecycle events to a tag group
// called when objs with tag "enemy" is added to scene
on("add", "enemy", (e) => {
console.log("run!!");
});
// per frame (action() is actually an alias to this)
on("update", "bullet", (b) => {
b.move(100, 0);
});
// per frame but drawing phase if you want custom drawing
on("draw", "bullet", (e) => {
drawSprite(...);
});
// when objs get 'destroy()'ed
on("destroy", "bullet", (e) => {
play("explosion");
});
Input
input events
keyDown(k: string, f: () => void) => EventCanceller
runs every frame when specified key is being pressed
// trigger this every frame the user is holding the "up" key
keyDown("up", () => {
player.move(0, -SPEED);
});
keyPress(k: string, f: () => void) => EventCanceller
runs once when specified key is just pressed
// only trigger once when the user presses
keyPress("space", () => {
player.jump();
});
keyPressRep(k: string, f: () => void) => EventCanceller
runs once when specified key is just pressed (will trigger multiple time if it's being held depending on the system's keyboard timer)
keyRelease(k: string, f: () => void) => EventCanceller
runs once when specified key is just released
charInput(f: (ch: string) => void) => EventCanceller
runs when user inputs text
// similar to keyPress, but focused on text input
charInput((ch) => {
input.text += ch;
});
mouseDown(f: (pos: Vec2) => void) => EventCanceller
runs every frame when left mouse is being pressed
mouseClick(f: (pos: Vec2) => void) => EventCanceller
runs once when left mouse is just clicked
mouseRelease(f: (pos: Vec2) => void) => EventCanceller
runs once when left mouse is just released
keyIsDown(k: string) => boolean
check if a key is being held down
// trigger this every frame the user is holding the "up" key
action(() => {
if (keyIsDown("w")) {
// TODO
}
});
keyPress("s", () => {
if (keyIsDown("meta")) {
// TODO
} else {
// TODO
}
})
keyIsPressed(k: string) => boolean
check if a key is just pressed last frame
keyIsPressedRep(k: string) => boolean
check if a key is just pressed last frame (will trigger multiple time if it's being held)
keyIsReleased(k: string) => boolean
check if a key is just released last frame
mouseIsDown() => boolean
check if mouse is being held down
mouseIsClicked() => boolean
check if mouse is just pressed last frame
mouseIsReleased() => boolean
check if mouse is just released last frame
Math
math types & utils
vec2() => Vec2
creates a vector 2
vec2() // => { x: 0, y: 0 }
vec2(1) // => { x: 1, y: 1 }
vec2(10, 5) // => { x: 10, y: 5 }
const p = vec2(5, 10);
p.x // 5
p.y // 10
p.clone(); // => vec2(5, 10)
p.add(vec2(10, 10)); // => vec2(15, 20)
p.sub(vec2(5, 5)); // => vec2(0, 5)
p.scale(2); // => vec2(10, 20)
p.dist(vec2(15, 10)); // => 10
p.len(); // => 11.58
p.unit(); // => vec2(0.43, 0.86)
p.dot(vec2(2, 1)); // => vec2(10, 10)
p.angle(); // => 1.1
rgba(r: number, g: number, b: number, a: number) => Color
creates a color from red, green, blue and alpha values (note: values are 0 - 1 not 0 - 255)
const c = rgba(0, 0, 1, 1); // blue
p.r // 0
p.g // 0
p.b // 1
p.a // 1
c.clone(); // => rgba(0, 0, 1, 1)
rgb(r: number, g: number, b: number) => Color
shorthand for rgba() with a = 1
rand(a: T, b: T) => T
generate random value
rand() // 0.0 - 1.0
rand(1, 4) // 1.0 - 4.0
rand(vec2(0), vec2(100)) // => vec2(29, 73)
rand(rgb(0, 0, 0.5), rgb(1, 1, 1)) // => rgba(0.3, 0.6, 0.9, 1)
randSeed(seed: number) => number
set seed for rand generator
randSeed(Date.now());
makeRng(seed: number) => RNG
create a seedable random number generator
const rng = makeRng(Date.now());
rng.gen(); // works the same as rand()
choose(lst: T[]) => T
get random element from array
chance(p: number) => boolean
rand(0, 1) <= p
lerp(from: number, to: number, t: number) => number
linear interpolation
map(v: number, l1: number, h1: number, l2: number, h2: number) => number
map number to another range
wave(lo: number, hi: number, t: number) => number
sin motion between 2 numbers
rad2deg(rad: number) => number
convert radians to degrees
deg2rad(deg: number) => number
convert degrees to radians
Draw
Raw immediate drawing functions (you prob won't need these)
render(cb: () => void) => EventCanceller
use a generic draw loop for custom drawing
scene("draw", () => {
render(() => {
drawSprite(...);
drawRect(...);
drawLine(...);
});
});
drawSprite(id: string | SpriteData, conf?: DrawSpriteConf) => void
draw a sprite
drawSprite("car", {
pos: vec2(100),
scale: 3,
rot: time(),
frame: 0,
});
drawRect(pos: Vec2, w: number, h: number, conf?: DrawRectConf) => void
draw a rectangle
drawRect(vec2(100), 20, 50);
drawLine(p1: Vec2, p2: Vec2, conf?: DrawLineConf) => void
draw a rectangle
drawLine(vec2(0), mousePos(), {
width: 2,
color: rgba(0, 0, 1, 1),
z: 0.5,
});
drawText(txt: string, conf?: ) => void
draw a rectangle
drawText("hi", {
size: 64,
pos: mousePos(),
origin: "topleft",
});
Level
helpers on building tiled maps
takes a level drawing and turns them into game objects according to the ref map
const characters = {
"a": {
sprite: "ch1",
msg: "ohhi how are you",
},
};
const map = addLevel([
" a ",
" ===",
" ? * ",
" ==== ^^ ",
"===================",
], {
width: 11,
height: 11,
pos: vec2(0, 0),
// every "=" on the map above will be turned to a game object with following comps
"=": [
sprite("ground"),
solid(),
"block",
],
"*": [
sprite("coin"),
solid(),
"block",
],
// use a callback for dynamic evauations per block
"?": () => {
return [
sprite("prize"),
color(0, 1, rand(0, 1)),
"block",
];
},
"^": [
sprite("spike"),
solid(),
"spike",
"block",
],
// any catches anything that's not defined by the mappings above, good for more dynamic stuff like this
any(ch) {
if (characters[ch]) {
return [
sprite(char.sprite),
solid(),
"character",
{
msg: characters[ch],
},
];
}
},
});
// query size
map.width();
map.height();
// get screen pos through map index
map.getPos(x, y);
// destroy all
map.destroy();
// there's no spatial hashing yet, if too many blocks causing lag, consider hard disabling collision resolution from blocks far away by turning off 'solid'
action("block", (b) => {
b.solid = player.pos.dist(b.pos) <= 20;
});
Scene
Use scenes to define different parts of a game, e.g. Game Scene, Start Scene,
scene(id: SceneID, def: SceneDef) => void
define a scene
scene("main", () => {
// all objs are bound to a scene
add(/* ... */)
// all events are bound to a scene
keyPress(/* ... */)
});
scene("gameover", () => {
add(/* ... */)
});
go("main");
go(id: SceneID, args: ...) => void
switch to a scene
// go to "paused" scene when pressed "p"
scene("main", () => {
let score = 0;
keyPress("p", () => {
go("gameover", score);
})
});
scene("gameover", (score) => {
// display score passed by scene "main"
add([
text(score),
]);
});
Custom Component
a component describes a single unit of data / behavior
const player = add([
sprite("froggy"),
// custom components are used just like other components
health(12),
]);
// create a custom component that handles health
function health(hp) {
return {
// comp id (if not it'll be treated like custom fields on the game object)
id: "health",
// comp dependencies (will throw if the host object doesn't contain these components)
require: [],
// custom behaviors
hurt(n) {
hp -= n ?? 1;
// trigger custom events
this.trigger("hurt");
if (hp <= 0) {
this.trigger("death");
}
},
heal(n) {
hp += n ?? 1;
this.trigger("heal");
},
hp() {
return hp;
},
};
}
// listen to custom events from a custom component
player.on("hurt", () => { ... });
// decoupled discrete logic
player.collides("enemy", () => player.hurt(1));
const boss = add([
health(12),
]);
boss.collides("bullet", () => {
boss.hurt(1);
});
boss.on("death", () => {
makeExplosion();
wait(1, () => {
destroy(enemy);
});
});
// another custom component that enables drag and drop
function drag() {
// private states
let draggin = false;
let removeEvent;
return {
id: "drag",
// we need the 'pos' and 'click' methods from "pos" and "area" comps
require: [ "pos", "area" ],
// LIFE CYCLE, called when the object is add()-ed
add() {
this.clicks(() => {
draggin = true;
});
// TODO: remove this event when destroyed
removeEvent = mouseRelease(() => {
draggin = false;
});
},
// LIFE CYCLE, called every frame
update() {
if (draggin) {
this.pos = mousePos();
}
},
// LIFE CYCLE, called when object is destroy()-ed
destroy() {
removeEvent();
},
};
}
// for more custom component examples, look at the implementations of the built-in comps in kaboom.ts