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