boomka
Debug

Kaboom v3000 Beta Release

tga, 01/15/2023

We're releasing Kaboom v3000 beta! This major version update includes a lot of new features and improvements, and also breaking changes (see migration guide below).

To try the beta version, install kaboom@next

$ npm install kaboom@next
<script src="https://unpkg.com/kaboom@next/dist/kaboom.js"></script>

or use this Replit template

Note that this is a beta release, there are bugs and possiblly breaking changes before official release. Please report bugs to github issues, and general questions / discussions to github discussions.

Scene Graph

Objects can now have children with obj.add()! Children will inherit the transform (position, scale and rotation) of the parent.

const bean = add([
    sprite("bean"),
    pos(160, 120),
])

const sword = bean.add([
    sprite("sword"),
    // transforms will be relative to parent bean object
    pos(20, 20),
    rotate(20),
])

const hat = bean.add([
    sprite("hat"),
    // transforms will be relative to parent bean object
    pos(0, -10),
])

// children will be moved alongside the parent
bean.moveBy(100, 200)

// children will be destroyed alongside the parent
bean.destroy()

Check out the rotating pineapples in this live example (in case you didn't notice, it also demonstrates rotating areas!)

More Font Formats

loadFont() now loads .ttf, .otf, .woff, .woff2 fonts (any font that browser font-face supports). The function to load bitmap font is renamed to loadBitmapFont().

// ComicSans all the way!
kaboom({
    font: "ComicSans",
})

loadFont("ComicSans", "assets/fonts/ComicSans.ttf")

// Load a font with options
loadFont("apl386", "assets/fonts/apl386.ttf", { outline: 4, filter: "linear" })

// To load bitmap font is renamed to loadBitmapFont()
loadBitmapFont("4x4", "/examples/fonts/4x4.png", 4, 4)

Tweening

const t = tween(
    // start value
    obj.pos,
    // destination value
    mousePos(),
    // duration (in seconds)
    0.5,
    // how value should be updated
    (p) => obj.pos = p,
    // interpolation function (defaults to easings.linear)
    easings.easeOutElastic,
)

// can cancel the tween any time
t.cancel()

t.onEnd(() => {
    // register event that runs when tween finishes
})

// tween() returns a then-able, which can be used with await
await tween(...)
await wait(1)
await tween(...)
await tween(...)
await wait(1)

Go to tween example to play around with it! Press left / right arrow key to change the interpolation function, mouse click anywhere to set destination.

Rotated Areas

Previously Kaboom only supports unrotated AABB boxes for collision detection, now Kaboom supports collision detection and resolution between any arbitrary convex polygons (e.g. rotated rectangles).

// rotate() will also affect the area() collider box!
add([
    sprite("bean"),
    pos(200, 100),
    rotate(30),
    area(),
])

// use a custom polygon area shape (a triangle in this case)
add([
    sprite("bean"),
    pos(200, 100),
    area({ shape: new Polygon([vec2(0), vec2(100), vec2(-100, 100)]) }),
])

Check out the new collision example. Press Q / E to rotate bean, and arrow keys to move around and see how bean now reacts with others!

Improved Performance

Graphics performance improved ~3x - 50x, collision detection performance improved ~2x - 4x. Read more about this in this blog post.

Custom Loading Screen

Now you can draw your custom loading screen with onLoading() (note that this function runs every frame, you can only use the drawXXX() functions here)

// the callback is run every frame when assets are initially loading
onLoading(() => {

    // Black background
    drawRect({
        width: width(),
        height: height(),
        color: rgb(0, 0, 0),
    })

    // A pie representing current load progress
    drawCircle({
        pos: center(),
        radius: 32,
        end: map(progress, 0, 1, 0, 360),
    })

    drawText({
        text: "loading" + ".".repeat(wave(1, 4, time() * 12)),
        font: "monospace",
        size: 24,
        anchor: "center",
        pos: center().add(0, 70),
    })

})

Another option is to turn off loading screen completely, default or custom:

kaboom({
    // by doing this, unloaded assets will simply be not drawn or played until they're loaded
    loadingScreen: false,
})

Try it ou in the loader example! It also demonstrated some other loading techniques.

Post Effect

Kaboom now provides an easy way to add full screen visual effect with shader:

loadShader("pixelate", null, `
uniform float u_size;
uniform vec2 u_resolution;

vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {
    if (u_size <= 0.0) return def_frag();
    vec2 nsize = vec2(u_size / u_resolution.x, u_size / u_resolution.y);
    float x = floor(uv.x / nsize.x + 0.5);
    float y = floor(uv.y / nsize.y + 0.5);
    vec4 c = texture2D(tex, vec2(x, y) * nsize);
    return c * color;
}
`)

// use the post effect "pixelate", and send the shader uniform
usePostEffect("pixelate", {
    "u_resolution": vec2(width(), height()),
    "u_size": 4,
})

Try it out in the postEffect example. Use UP/DOWN arrow to cycle through the example effects!

Gamepad

Added gamepad support.

onGamepadButtonPress("south", () => {
    player.jump()
})

onGamepadStick("left", (v) => {
    player.move(v.scale(SPEED))
})

If you gamepad doesn't work, add your gamepad mappings to Kaboom's gamepad mapping json file.

Connect a gamepad and go nuts with this classic minigame!

Path Finding

The level API has been rewored and added path finding support. Use the new tile() component with option tile({ isObstacle: true }) to define obstacles in a level, and agent() component to

const map = addLevel([
    "##########",
    "#        #",
    "# @      #",
    "#.....   #",
    "#        #",
    "#    ....#",
    "#      $ #",
    "#        #",
    "##########",
], {
    tileWidth: 64,
    tileHeight: 64,
    tiles: {
        "$": () => [
            sprite("coin"),
            area(),
        ],
        "#": () => [
            sprite("wall"),
            tile({ isObstacle: true }),
        ],
        "@": () => [
            sprite("player"),
            tile(),
            agent({ speed: 640, allowDiagonals: true }),
            "player",
        ],
    },
})

const player = get("player")[0]

onClick(() => {
    bean.setTarget(mousePos())
})

Run this maze example to see for yourself. Click anywhere in the maze to travel to it!

Path finding is implemented by @mflerackers

More features

See complete list of changes in CHANGELOG

Migrating to v3000

  • obj._id is renamed to obj.id
const obj = add([
    pos(300, 200),
    sprite("bean"),
    area(),
])

// before
console.log(obj._id)

// v3000
console.log(obj.id)
  • origin() is renamed to anchor()
// before
add([
    sprite("bean"),
    origin("center"),
])

// v3000
add([
    sprite("bean"),
    anchor("center"),
])
  • obj.onHover() in area() comp is renamed to obj.onHoverUpdate(), obj.onHover() now only runs once when obj is hovered
const obj = add([
    pos(300, 200),
    sprite("bean"),
    area(),
])

// before
obj.onHover(() => {
    console.log("this will print every frame when obj is hovered")
}, () => {
    console.log("this will print every frame when obj is not hovered")
})

// v3000
obj.onHover(() => {
    console.log("this will run once when obj is hovered")
})

obj.onHoverUpdate(() => {
    console.log("this will run every frame when obj is hovered")
})

obj.onHoverEnd(() => {
    console.log("this will run once when obj stopped being hovered")
})
  • obj.pushOut() is renamed to obj.resolveCollision()
const player = add([
    sprite("bean"),
    pos(300, 200),
    area(),
])

// before
player.pushOut(rock)

// v3000
player.resolveCollision(rocker)
  • solid() comp becomes an option in body({ isStatic: true })
// before
add([
    sprite("bean"),
    area(),
    body(),
    solid(),
])

// v3000
add([
    sprite("bean"),
    area(),
    body({ isStatic: true }),
])
  • gravity now needs to be manually enabled, gravity() is renamed to setGravity() and getGravity()
// before, gravity will be enabled by body() component
add([
    pos(100, 100),
    sprite("bean"),
    area(),
    body(),
])

// v3000, use gravity() to manually enable gravity
setGravity(1600)

add([
    pos(100, 100),
    sprite("bean"),
    area(),
    body(),
])
  • body.weight is renamed to body.gravityScale
// before
add([
    body({ weight: 2 }),
])

// before
add([
    body({ gravityScale: 2 }),
])
  • body.doubleJump() is removed in favor of new doubleJump() component
const obj = add([
    pos(100, 100),
    sprite("bean"),
    area(),
    body(),
])

obj.doubleJump()

// after
const obj = add([
    pos(100, 100),
    sprite("bean"),
    area(),
    body(),
    doubleJump(),
])

obj.doubleJump()
  • body.onFall() is renamed to body.onFallOff(), body.onFall() now runs when body is in the air and starts to fall
gravity(1600)

const obj = add([
    pos(100, 100),
    sprite("bean"),
    area(),
    body(),
])

// before
obj.onFall(() => {
    console.log("this will print when object falls off a platform")
})

// v3000
obj.onFallOff(() => {
    console.log("this will print when object falls off a platform")
})

obj.onFall(() => {
    console.log("this will print when object is in the air and starts falling")
})
  • removed outview() in favor of offscreen(), which is less accurate but much faster
// before
add([
    sprite("flower"),
    outview({ hide: true }),
])

// v3000
add([
    sprite("flower"),
    // will hide itself when its position is 64 pixels offscreen
    offscreen({ hide: true, distance: 64 }),
])
  • removed cleanup() in favor of offscreen({ destroy: true })
// before
add([
    pos(player.pos),
    sprite("bullet"),
    cleanup(),
])

// v3000
add([
    pos(player.pos),
    sprite("bullet"),
    offscreen({ destroy: true }),
])
  • sprite.flipX and sprite.flipY becomes properties instead of functions
const bean = add([
    sprite("bean")
])

// before
bean.flipX(true)

// v3000
bean.flipX = true
  • sprite.onAnimStart() and sprite.onAnimEnd() now triggers on any animation
const bean = add([
    sprite("bean")
])

// before
bean.onAnimStart("walk", () => {
    // do something
})

// before
bean.onAnimStart((anim) => {
    if (anim === "walk") {
        // do something
    }
})
  • obj.scale now is always a Vec2

  • loadFont() now only loads .ttf, .otf, .woff etc fonts that browser support, use loadBitmapFont() to load bitmap fonts
// before
loadFont("unscii", "/examples/fonts/unscii_8x8.png", 8, 8)

// v3000
loadBitmapFont("unscii", "/examples/fonts/unscii_8x8.png", 8, 8)
loadFont("apl386", "/examples/fonts/apl386.ttf")
  • removed builtin fonts apl386, apl386o, sink and sinko, using browser built-in monospace font as default font now
// v3000, manually load these fonts if you need them
loadFont("apl386", "/examples/fonts/apl386.ttf")
loadBitmapFont("sink", "/examples/fonts/sink_6x8.png")

// use outline option for "apl386o"
loadFont("apl386", "/examples/fonts/apl386.ttf", {
    outline: 3,
})
  • changed vertex format from vec3 to vec2 (only applied in shaders)
// before
loadShader("test", null, `
vec4 frag(vec3 pos, vec2 uv, vec4 color, sampler2D tex) {
    return def_frag();
}
`)

// v3000
loadShader("test", null, `
vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {
    return def_frag();
}
`)
  • anchor (previously origin) no longer controls text alignment (only controls the anchor of the whole text area), use text({ align: "left" }) option for text alignment
// before
add([
    pos(center()),
    origin("center"),
    text("oh hi"),
])

// v3000
add([
    pos(center()),
    anchor("center"),
    text("oh hi", { align: "center" }),
])
  • changed text styling syntax to bbcode
const textOpts = {
    styles: {
        "green": {
            color: rgb(128, 128, 255),
        },
        "wavy": (idx, ch) => ({
            color: hsl2rgb((time() * 0.2 + idx * 0.1) % 1, 0.7, 0.8),
            pos: vec2(0, wave(-4, 4, time() * 6 + idx * 0.5)),
        }),
    },
}

// before
add([
    text("[oh hi].green here's some [styled].wavy text", textOpts),
])

// v3000
add([
    text("[green]oh hi[/green] here's some [wavy]styled[/wavy] text", textOpts),
])
  • changed all event handlers to return an EventController object, instead of a function to cancel
// before
const cancel = onUpdate(() => { /* ... */ })
cancel()

// v3000
const ev = onUpdate(() => { /* ... */ })
ev.paused = true
ev.cancel()
  • changed the interface for addLevel()
// before
addLevel([
    "@  ^ $$",
    "=======",
], {
    width: 32,
    height: 32,
    "=": () => [
        sprite("grass"),
        area(),
        body({ isStatic: true }),
    ],
    "$": () => [
        sprite("coin"),
        area(),
        "coin",
    ],
    any: (symbol) => {
        if (symbol === "@") {
            return [ /* ... */ ]
        }
    },
})

// v3000
addLevel([
    "@  ^ $$",
    "=======",
], {
    tileWidth: 32,
    tileHeight: 32,
    tiles: {
        "=": () => [
            sprite("grass"),
            area(),
            body({ isStatic: true }),
        ],
        "$": () => [
            sprite("coin"),
            area(),
            "coin",
        ],
    },
    wildcardTile: (symbol) => {
        if (symbol === "@") {
            return [ /* ... */ ]
        }
    },
})