Documentación · empezar

Empezar en diez minutos

La ruta rápida, para quien ya programa. Mismo juego que el tutorial de /learn, en cuatro pasos grandes y sin explicaciones de qué es un bucle.

Todavía no puedes instalarlo

El paquete aún no está publicado en npm, así que el `npm install` de abajo falla hoy. Mientras tanto, el tutorial de /learn corre el motor entero en tu navegador y no necesita nada de esto.

¿Es tu primer juego, o tu primera vez programando? El tutorial paso a paso cuenta lo mismo despacio y sin instalar nada.

Ir al tutorial
01

Instala y arranca

Un proyecto de Vite normal. El motor es una dependencia y nada más: no hay CLI, ni plantilla obligatoria, ni fichero de configuración propio.

npm create vite@latest mi-juego -- --template vanilla-ts
cd mi-juego
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

Una escena es una función

createGame devuelve la función con la que se arranca; la instancia sale de llamarla con las escenas. El cuerpo de una escena se ejecuta una sola vez, al montar: dentro declaras lo que hay y registras los bucles.

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

El bucle, el teclado y los bordes

useUpdate(dt => …) corre por fotograma y dt son segundos: multiplica siempre, o el juego cambia de velocidad con la frecuencia del monitor. useKeyboard se sondea, no emite eventos: isDown para lo continuo, justPressed para un disparo por pulsación. Mutar transform directamente dentro del bucle es lo correcto y lo rápido.

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

Colisión, ciclo de vida, y el aspecto

AABB a mano para dos rectángulos. useSpawn da a cada ladrillo un objeto propio con todo lo que registra; useSelf + destroy lo quitan del mundo, al final del fotograma, sin fotograma fantasma y sin doble impacto. Y el aspecto de época es una línea de post-proceso sobre la imagen terminada.

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 }));

El código completo del juego está en el último capítulo del tutorial. Capítulo 9