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.
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 tutorialInstall 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 nacatamalonindex.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 });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 });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 });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