Skip to main content

API

Versión alfa: Esta es la documentación del motor v2. Las funciones y APIs pueden cambiar.

La mayoría de las funciones de Floaty se parecen mucho a las de Pico-8. La diferencia principal es que accedes a ellas a través del método scope() en lugar de tenerlas disponibles globalmente. Esto funciona mejor para JavaScript ejecutándose en un navegador web.

Core

scope(callback)

Proporciona acceso a todas las funciones del motor a través de un closure. Así es como empiezas a usar el motor:

engine.scope(({ start, cls, spr }) => {
    start({
        target: document.querySelector('.game'),
        draw() {
            cls();
            spr('player', 10, 10);
        },
    });
});

start({ ... })

Inicia el bucle del juego. Pasa un objeto con las siguientes propiedades:

  • target - El elemento DOM donde se renderiza
  • sprites - Objeto con las definiciones de sprites
  • sounds - Objeto con las definiciones de sonidos
  • init - Función que se llama una vez al iniciar el juego
  • update(time, frame) - Función que se llama en cada tick del juego con la marca de tiempo actual y el conteo de fotogramas
  • draw(time, frame) - Función que se llama en cada fotograma para renderizar con la marca de tiempo actual y el conteo de fotogramas
  • shutdown - Función que se llama cuando el motor se detiene (opcional)
  • background - Color CSS para el área alrededor del canvas (opcional)

Aquí tienes un ejemplo completo mostrando el ciclo de vida del juego:

engine.scope(({ start, cls, circfill }) => {
    let x = 20;
    let y = 20;
    let dx = 1;
    let dy = 1;

    const sprites = {};
    const sounds = {};

    function init() {
        x = 20;
        y = 20;
    }

    function update() {
        x += dx;
        y += dy;

        if (x <= 4 || x >= 124) {
            dx *= -1;
        }

        if (y <= 4 || y >= 124) {
            dy *= -1;
        }
    }

    function draw() {
        cls(1);
        circfill(x, y, 4, 8);
    }

    start({
        sprites,
        sounds,
        init,
        update,
        draw,
        background: '#1D2B53',
    });
});

fps(callback)

Registra un callback que recibe los fotogramas por segundo actuales en cada fotograma. Úsalo para crear un contador de FPS o monitor de rendimiento.

engine.scope(({ start, cls, text, fps }) => {
    let currentFps = 0;

    fps((value) => {
        currentFps = value;
    });

    start({
        target: document.querySelector('.game'),
        sprites: {},
        sounds: {},
        draw() {
            cls();
            text(`${currentFps} fps`, 1, 1, 7);
        },
    });
});

time + frame

Tanto update como draw reciben (time, frame) como argumentos. time es la marca de tiempo transcurrida en milisegundos. frame es el número total de ticks del juego desde que empezó. Usa frame para temporización determinista (ej. ciclos de animación) y time para efectos suaves en tiempo real.

engine.scope(({ start, cls, circfill }) => {
    const colors = [8, 9, 10, 11, 12];

    start({
        target: document.querySelector('.game'),
        sprites: {},
        sounds: {},
        draw(time, frame) {
            cls();

            // cycle through colors every 15 frames
            const colorIndex = Math.floor(frame / 15) % colors.length;
            circfill(64, 64, 16, colors[colorIndex]);
        },
    });
});

Dibujo

cls(color = 0)

Limpia la pantalla con el color especificado (0-15). Se usa más comúnmente al inicio de la función draw.

pset(x, y, color) + pget(x, y)

pset establece un píxel en las coordenadas (x, y) al color especificado (0-15). pget lee el color de vuelta. Ambos se ajustan por el desplazamiento de cámara actual (ver sección Cámara), así que pget(x, y) lee lo que pset(x, y) escribió.

// inside draw()
for (let x = 0; x < 128; x++) {
    for (let y = 0; y < 64; y++) {
        pset(x, y, Math.floor(x / 8) % 16);
    }
}

// read back a pixel color
const color = pget(8, 8);
text(`color: ${color}`, 2, 80, 7);

spr(key, x, y, w = 1, h = 1, flipX = false, flipY = false)

Dibuja un sprite en la posición dada. Usa w y h para dibujar sprites más grandes (múltiplos de 8x8). Usa flipX y flipY para reflejar el sprite.

const sprites = {
    // prettier-ignore
    arrow: [
        -1, -1, -1,  7,  7, -1, -1, -1,
        -1, -1,  7,  7,  7, -1, -1, -1,
        -1,  7, -1,  7,  7, -1, -1, -1,
         7, -1, -1,  7,  7,  7,  7,  7,
         7, -1, -1,  7,  7,  7,  7,  7,
        -1,  7, -1,  7,  7, -1, -1, -1,
        -1, -1,  7,  7,  7, -1, -1, -1,
        -1, -1, -1,  7,  7, -1, -1, -1,
    ],
};

// inside draw()
spr('arrow', 20, 60);
spr('arrow', 40, 60, 1, 1, true); // flipped horizontally

line(x1, y1, x2, y2, color = 7)

Dibuja una línea desde (x1, y1) hasta (x2, y2) en el color especificado.

rect(x1, y1, x2, y2, color = 7)

Dibuja un rectángulo sin relleno desde (x1, y1) hasta (x2, y2) en el color especificado.

rectfill(x1, y1, x2, y2, color = 7)

Dibuja un rectángulo relleno desde (x1, y1) hasta (x2, y2) en el color especificado.

circ(x, y, r, color = 7, thickness = 1)

Dibuja un círculo sin relleno centrado en (x, y) con radio r. Usa thickness para dibujar un anillo más grueso.

circfill(x, y, r, color = 7)

Dibuja un círculo relleno centrado en (x, y) con radio r.

text(str, x, y, color = 7)

Dibuja texto en la posición (x, y) en el color especificado.

const message = 'game over';
const size = textSize(message);
const x = Math.floor((128 - size.x) / 2);
const y = Math.floor((128 - size.y) / 2);

// draw a background panel behind the text
rectfill(x - 4, y - 4, x + size.x + 4, y + size.y + 4, 2);
text(message, x, y, 7);

textSize(str)

Obtiene las dimensiones del texto. Devuelve un objeto con las propiedades x (ancho) e y (alto).

textPixels(str, x, y, color = 7)

Obtiene los datos de píxeles del texto en la posición (x, y) en el color especificado. Devuelve un array de coordenadas [x, y, color].

Entrada

btn(key)

Verifica si un botón está siendo presionado actualmente. Devuelve true/false. Acepta cualquier valor de KeyboardEvent.key (ej. 'ArrowUp', 'ArrowLeft', 'z', 'x'). Llámalo sin argumentos para detectar cualquier tecla presionada.

btnp(key)

Verifica si un botón acaba de ser presionado (no mantenido). Devuelve true solo en el primer fotograma en que se presiona la tecla. Úsalo sin argumentos para detectar cualquier pulsación de tecla.

// inside update()

// btn() is true every frame while held
if (btn('ArrowUp')) {
    y -= 1;
}
if (btn('ArrowDown')) {
    y += 1;
}
if (btn('ArrowLeft')) {
    x -= 1;
}
if (btn('ArrowRight')) {
    x += 1;
}

// btnp() is true only on the first frame
if (btnp('z')) {
    dash();
}

mouse()

Obtiene la posición actual del ratón en el canvas. Devuelve un objeto con propiedades x e y (en coordenadas del canvas, no de la pantalla).

mdown()

Verifica si el botón del ratón está siendo presionado actualmente. Devuelve true/false.

mup()

Verifica si el botón del ratón está suelto actualmente. Devuelve true/false.

click()

Verifica si se hizo clic con el ratón en este fotograma. Devuelve true/false. Nota: se consume al leerlo — llámalo una vez por fotograma y guarda el resultado si lo necesitas en varios lugares.

dragging()

Verifica si el ratón está siendo arrastrado (botón presionado mientras se mueve). Devuelve true/false.

// inside update()
const pos = mouse();

if (click()) {
    pixels.push(pos);
}

if (dragging()) {
    pixels.push(pos);
}

cursor(type = 'default')

Controla el cursor del navegador sobre el canvas del juego. Llama cursor(false) para ocultarlo, cursor() para restaurar el predeterminado, o pasa cualquier valor CSS de cursor (ej. 'pointer', 'crosshair').

// inside init()
cursor(false);

// inside draw()
const pos = mouse();
circfill(pos.x, pos.y, 1, 7);

// to restore the browser cursor
cursor(); // default cursor
cursor('pointer'); // or any CSS cursor value

Sonido

sfx(key, volume = 1.0)

Reproduce un efecto de sonido por su clave en el objeto sounds. Opcionalmente pasa un multiplicador de volume (0-1). El formato de los datos de sonido determina cómo se reproduce:

  • Sintetizado: [length, [freq, vol, wave], ...]
  • Ruta de archivo de audio: '/sounds/jump.wav'
  • URI de datos: 'data:audio/wav;base64,...'

Aquí tienes un ejemplo usando el formato sintetizado:

const sounds = {
    jump: [4, [40, 0.8, 0], [50, 0.6, 0], [60, 0.4, 0]],
    coin: [3, [55, 0.7, 1], [60, 0.7, 1], [64, 0.5, 1]],
};

// inside update()
if (btnp('z')) {
    sfx('jump');
}

music(source, options = {})

Reproduce música de fondo. source puede ser una clave de sounds, una URL o un URI de datos. Pasa false para detener la reproducción. Opciones: loop (por defecto: true), volume (por defecto: 0.5, rango 0-1). Detiene automáticamente cualquier música en reproducción.

const sounds = {
    bgm: '/audio/background.mp3',
};

// start looping background music
music('bgm', { loop: true, volume: 0.5 });

// stop it later
music(false);

Recursos

queue(url)

Encola una URL para carga diferida de recursos. Devuelve un marcador de posición que load() resolverá después. Úsalo en definiciones de sprites y sonidos en lugar de datos inline. Para URLs JSON, los datos obtenidos reemplazan el marcador de posición. Para URLs de audio (wav/mp3/ogg), se almacena la cadena de URL para que el motor pueda decodificarla en el momento de reproducción.

load()

Obtiene todos los recursos encolados en paralelo y reemplaza los marcadores de posición con los datos resueltos. Llámalo con await dentro de una función init() asíncrona. Esto te permite controlar la experiencia de carga — mostrar una pantalla de carga, animar el progreso, y luego transicionar al juego.

const sprites = {
    player: queue('https://example.com/player.json'),
};

const sounds = {
    laser: queue('https://example.com/laser.json'),
    music: queue('https://example.com/track.mp3'),
};

async function init() {
    await load();
}

Sprites

fget(index, flag) + fset(index, flag, value)

fset marca un sprite con una flag (0-7). fget lee la flag de vuelta. Usa flags para etiquetar sprites con propiedades como "sólido" o "peligroso". Los sprites deben incluir un array de flags como segundo elemento: [pixels, [f0, f1, ..., f7]].

// sprites with a flags array as the second element
const sprites = {
    // prettier-ignore
    brick: [[
        5, 5, 5, 5, 5, 5, 5, 5,
        5, 4, 4, 4, 4, 4, 4, 5,
        5, 4, 4, 4, 4, 4, 4, 5,
        5, 5, 5, 5, 5, 5, 5, 5,
        5, 5, 5, 5, 5, 5, 5, 5,
        4, 4, 4, 5, 5, 4, 4, 4,
        4, 4, 4, 5, 5, 4, 4, 4,
        5, 5, 5, 5, 5, 5, 5, 5,
    ], [0, 0, 0, 0, 0, 0, 0, 0]],
};

// inside init() — mark brick as solid
fset('brick', 0, true);

// inside update() — check if tile is solid
if (fget('brick', 0)) {
    // this tile is solid
}

pal(from, to)

Intercambia colores de paleta. Llamar pal(0, 8) reemplazará todas las instancias del color 0 con el color 8. Llama pal() sin argumentos para restablecer la paleta a sus valores por defecto.

// draw with original colors
spr('character', 20, 60);

// red team: swap blue (12) for red (8)
pal(12, 8);
spr('character', 56, 60);
pal(); // reset palette

// green team: swap blue (12) for green (11)
pal(12, 11);
spr('character', 92, 60);
pal(); // reset palette

Colisiones

circlesCollide(a, b)

Verifica si dos círculos se superponen. Cada círculo es un array: [x, y, radius]. Devuelve true/false.

// each circle is [x, y, radius]
const a = [playerX, playerY, 10];
const b = [enemyX, enemyY, 10];

if (circlesCollide(a, b)) {
    // handle collision
}

boxesCollide(a, b)

Verifica si dos rectángulos se superponen. Cada caja es un array: [x, y, width, height]. Devuelve true/false.

// each box is [x, y, width, height]
const player = [playerX, playerY, 16, 16];
const wall = [80, 30, 8, 68];

if (boxesCollide(player, wall)) {
    // handle collision
}

Aleatorio

rnd([limit])

Genera un número aleatorio entre 0 y limit. Como la función rnd() de Pico-8.

randomFloat()

Genera un número decimal aleatorio entre 0 y 1.

randomIntegerBetween(min, max)

Genera un entero aleatorio entre min y max (inclusive).

randomFloatBetween(min, max)

Genera un número decimal aleatorio entre min y max.

// inside init()
for (let i = 0; i < 80; i++) {
    stars.push({
        x: randomIntegerBetween(0, 127),
        y: randomIntegerBetween(0, 127),
        speed: randomFloatBetween(0.2, 1.5),
        brightness: rnd() > 0.5 ? 7 : 6,
    });
}

// inside draw()
for (const star of stars) {
    star.y = (star.y + star.speed) % 128;
    const color = randomFloat() > 0.9 ? 5 : star.brightness;
    pset(star.x, star.y, color);
}

Cámara

camera(x = 0, y = 0)

Establece el desplazamiento de cámara. Todas las operaciones de dibujo posteriores se desplazarán por (-x, -y). Úsalo para desplazar la vista y que siga a un jugador o recorra un nivel.

cget()

Obtiene la posición actual de la cámara. Devuelve un objeto con propiedades x e y. Útil para posicionar elementos de HUD relativos a la cámara.

creset()

Restablece la cámara a (0, 0). Llámalo antes de dibujar UI fija en pantalla como barras de vida o texto de puntuación.

// inside update()
camera(playerX - 64, playerY - 64);

// inside draw()
// world draws offset by camera
spr('player', playerX, playerY);

// read camera to position HUD elements
const cam = cget();
text(`HP: ${hp}`, cam.x + 2, cam.y + 2, 7);

// reset for screen-fixed UI
creset();
text('PAUSED', 48, 60, 8);

Tilemap

mset(x, y, tile, layer = 0, mapId = 'default')

Establece un tile en la posición de cuadrícula (x, y) en la capa especificada. tile es una clave de sprite que coincide con un nombre en tu objeto sprites. Usa layer para separar tiles de fondo y primer plano. Usa mapId para gestionar múltiples mapas.

mget(x, y, layer = 0, mapId = 'default')

Obtiene el tile en la posición de cuadrícula (x, y). Devuelve 0 si no hay ningún tile establecido.

map(layer = 0, cellX = 0, cellY = 0, cellW, cellH, screenX = 0, screenY = 0, mapId = 'default')

Renderiza todos los tiles de la capa dada llamando a spr() para cada tile diferente de cero. Opcionalmente especifica una región del tilemap a dibujar con cellX, cellY, cellW, cellH, y dónde dibujarla en pantalla con screenX, screenY.

mclear(layer, mapId)

Limpia tiles. Sin argumentos, limpia todo. Con layer, limpia esa capa en todos los mapas. Con ambos layer y mapId, limpia una capa específica de un mapa específico.

// inside init()
for (let x = 0; x < 16; x++) {
    mset(x, 15, 'brick');
    mset(x, 14, 'grass');
}

// inside update()
const tileBelow = mget(
    Math.floor(playerX / 8),
    Math.floor(playerY / 8) + 1,
);

if (tileBelow === 'brick') {
    // standing on solid ground
}

// inside draw()
map(0);

// reset a layer
mclear(0);