Docs · getting started

Up and running in ten minutes

The fast track, for people who already code. Same game as the /learn tutorial, in four big steps and without explaining what a loop is.

You can't install it yet

The package is not on npm yet, so the `npm install` below fails today. In the meantime, the /learn tutorial runs the whole engine in your browser and needs none of this.

First game, or first time writing code at all? The step-by-step tutorial covers the same ground slowly, with nothing to install.

Go to the tutorial
01

Install and run

An ordinary Vite project. The engine is a dependency and nothing else: no CLI, no mandatory scaffold, no config file of its own.

npm create vite@latest my-game -- --template vanilla-ts
cd my-game
npm install nacatamalon

index.ts

import { createGame, createScene, getColor } from 'nacatamalon';

const WIDTH = 480;
const HEIGHT = 320;

const Game = () => {
    return createScene();
};

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: getColor('#12102b'),
});

game({ Game });
02

A scene is a function

createGame returns the function that starts the game; the instance comes from calling it with your scenes. A scene body runs exactly once, on mount: inside it you declare what exists and register the loops.

index.ts

import { createGame, createScene, createSprite, getColor } from 'nacatamalon';

const WIDTH = 480;
const HEIGHT = 320;

const PADDLE_W = 64;
const PADDLE_H = 10;

const Game = () => {
    createSprite({
        anchor: { x: 0, y: 0 },
        transform: { x: (WIDTH - PADDLE_W) / 2, y: HEIGHT - 24 },
        width: PADDLE_W,
        height: PADDLE_H,
        tint: getColor('#ff6b1a'),
    });

    return createScene();
};

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: getColor('#12102b'),
});

game({ Game });
03

The loop, the keyboard and the edges

useUpdate(dt => …) runs per frame and dt is in seconds: always multiply, or the game changes speed with the monitor's refresh rate. useKeyboard is polled, not evented: isDown for continuous input, justPressed for one shot per press. Mutating transform directly inside the loop is both correct and fast.

index.ts

import { createGame, createScene, createSprite, getColor, useKeyboard, useUpdate } from 'nacatamalon';

const WIDTH = 480;
const HEIGHT = 320;

const PADDLE_W = 64;
const PADDLE_H = 10;
const PADDLE_SPEED = 340;

const Game = () => {
    const keys = useKeyboard();

    const paddle = createSprite({
        anchor: { x: 0, y: 0 },
        transform: { x: (WIDTH - PADDLE_W) / 2, y: HEIGHT - 24 },
        width: PADDLE_W,
        height: PADDLE_H,
        tint: getColor('#ff6b1a'),
    });

    useUpdate((dt) => {
        if (keys.isDown('ArrowLeft')) paddle.transform.x -= PADDLE_SPEED * dt;
        if (keys.isDown('ArrowRight')) paddle.transform.x += PADDLE_SPEED * dt;

        paddle.transform.x = Math.min(Math.max(paddle.transform.x, 0), WIDTH - PADDLE_W);
    });

    return createScene();
};

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: getColor('#12102b'),
});

game({ Game });
04

Collision, lifetime, and the look

Hand-rolled AABB for two rectangles. useSpawn gives each brick an object of its own with everything it registers; useSelf + destroy take it out of the world at the end of the frame, no ghost frame, no double hit. And the period look is one line of post-processing over the finished image.

bricks.ts

import { createSprite, destroy, getColor, useSelf } from 'nacatamalon';

export const BRICK_COLS = 8;
export const BRICK_ROWS = 5;
export const BRICK_W = 52;
export const BRICK_H = 16;

const MARGIN_X = 18;
const GAP_X = 4;
const TOP = 56;
const GAP_Y = 6;

const ROW_COLORS = ['#b14bff', '#ff6b1a', '#ffb347', '#5ad1e6', '#ede6f5'];

export type BrickHandle = {
    x: number;
    y: number;
    hit: () => void;
};

export const Brick = ({ row, col, bricks }: { row: number; col: number; bricks: BrickHandle[] }) => {
    const self = useSelf();
    const x = MARGIN_X + col * (BRICK_W + GAP_X);
    const y = TOP + row * (BRICK_H + GAP_Y);

    createSprite({
        anchor: { x: 0, y: 0 },
        transform: { x, y },
        width: BRICK_W,
        height: BRICK_H,
        tint: getColor(ROW_COLORS[row]),
    });

    const brick: BrickHandle = {
        x,
        y,
        hit: () => {
            bricks.splice(bricks.indexOf(brick), 1);
            destroy(self);
        },
    };

    bricks.push(brick);
};

export const brickAt = (bricks: BrickHandle[], x: number, y: number, size: number): BrickHandle | undefined =>
    bricks.find(
        (brick) =>
            x + size > brick.x &&
            x < brick.x + BRICK_W &&
            y + size > brick.y &&
            y < brick.y + BRICK_H,
    );
usePostProcess(dither({ levels: COLOR_LEVELS.genesis }));

The game's full source is in the tutorial's last chapter. Chapter 9