El engine provee tres clases para manejar gráficos 2D: SpriteProcessor (bajo nivel, utilidades estáticas), SpriteManager (alto nivel, orientado a objetos, agrupamiento y gestión) y EntityComposer (composición de entidades por capas).
El sistema es agnóstico del motor. SpriteProcessor.detectEngine() consulta RenderBridge para detectar el motor activo (canvas, pixi, little → canvas para sprites 2D) y convierte sprites al formato necesario.
SpriteManager es la forma más fácil y ordenada de organizar los gráficos de tu juego. Permite cargar hojas de sprites (spritesheets) y agruparlos por personaje o entorno.
Generalmente crearás una instancia global para tu juego:
const manager = new SpriteManager();
// Opcionalmente forzar el engine si detecta incorrectamente:
// const manager = new SpriteManager(PIXIEngine, 'pixi');Puedes definir cómo "cortar" una imagen grande usando load.
const spritesData = [
{ name: 'player_idle', x: 0, y: 0, width: 32, height: 32 },
{ name: 'player_walk0', x: 32, y: 0, width: 32, height: 32 },
{ name: 'player_walk1', x: 64, y: 0, width: 32, height: 32 },
{ name: 'player_walk2', x: 96, y: 0, width: 32, height: 32 },
{ name: 'player_walk3', x: 128, y: 0, width: 32, height: 32 },
];
await manager.load('assets/characters.png', spritesData, {
name: 'hero',
animations: {
idle: 'player_idle', // Simplificado: string directo
walk: { frames: 'player_walk{0-3}', speed: 10 }, // Rango con llaves
}
});Ya no necesitas escribir walk1, walk2, walk3... manualmente. Soporta tres formatos:
| Formato | Ejemplo | Resuelve |
|---|---|---|
prefijo{expr} |
walk{0-3} |
walk0, walk1, walk2, walk3 |
prefijo{expr} |
walk{1,4,6-9} |
walk1, walk4, walk6, walk7, walk8, walk9 |
| Números solos | 0-3 |
usa el nombre de animación como prefijo → walk0, walk1, walk2, walk3 |
| Números con coma | 1,4,6-9 |
usa el nombre de animación como prefijo → walk1, walk4, walk6, walk7, walk8, walk9 |
| Legacy | walk0-3 |
walk0, walk1, walk2, walk3 |
// Todas estas formas son equivalentes:
animations: {
walk: { frames: ['player_walk0', 'player_walk1', 'player_walk2', 'player_walk3'] },
walk: { frames: 'player_walk{0-3}' },
walk: { frames: 'player_walk0-3' },
walk: 'player_walk{0-3}', // versión simplificada (speed=10, loop=true)
walk: '0-3', // usa 'walk' como prefijo → walk0, walk1, walk2, walk3
}
// Rangos complejos (saltos + rangos):
animations: {
attack: 'attack{1,3,5-8}', // attack1, attack3, attack5, attack6, attack7, attack8
}Si estás usando PIXI o Canvas puro, puedes pedir la animación ya formateada:
// Obtener una animación lista para usar en PIXI o Canvas
let playerAnim = manager.createAnimationAs('hero', 'walk');
// En PIXIEngine:
PIXIEngine.addChild(playerAnim); // En PIXI es un PIXI.AnimatedSprite
playerAnim.play();
// En Canvas puro (engine.js):
update(dt) {
playerAnim.update(dt);
}
render(ctx) {
const texture = playerAnim.getTexture();
ctx.drawImage(texture, x, y);
}Nota sobre
createAnimationAs: Cuando no se pasan opciones, el motor hereda automáticamentespeed,loopyonCompletede la definición de la animación cargada conmanager.load(). En PIXI, esto significa quespeed: 10equivale a 10 fps enPIXI.AnimatedSprite.animationSpeed. Si necesitas sobrescribir algún valor al instanciar, pásalo como segundo argumento:// Sobrescribe speed, hereda loop y onComplete de la definición let anim = manager.createAnimationAs('hero', 'walk', { speed: 20 });
let domElement = manager.createAnimatedDOM('hero', 'walk');
document.getElementById('game-container').appendChild(domElement.element);
update(dt) {
domElement.update(dt);
}Con EntityComposer puedes armar personajes y objetos a partir de partes independientes (cuerpo, cabeza, arma, etc.). Cada parte puede tener su propia animación y posición relativa.
// Cargar las partes por separado o juntas
await manager.load('assets/character-body.png', bodyData, { name: 'hero_body' });
await manager.load('assets/character-head.png', headData, { name: 'hero_head' });
// Definir la composición
manager.compose('player', {
body: {
group: 'hero_body',
sprite: 'body_idle',
x: 0, y: 0, z: 0,
animations: {
idle: 'body_idle',
walk: 'body_walk{0-3}',
}
},
head: {
group: 'hero_head',
sprite: 'head_idle',
x: 0, y: -20, z: 1, // desplazado arriba, delante del cuerpo
animations: {
idle: 'head_idle',
walk: 'head_walk{0-3}',
}
},
hat: {
group: 'hero_head',
sprite: 'hat_crown',
x: -2, y: -32, z: 2, // encima de la cabeza
}
});
// Usar en el juego
const player = manager.getComposition('player');
player.setAnimations({ body: 'walk', head: 'walk' });
// En el loop:
update(dt) {
player.update(dt);
}
render(ctx) {
player.render(ctx, this.x, this.y);
}const tank = new EntityComposer(manager)
.addSlot('base', {
group: 'vehicles',
sprite: 'tank_body',
x: 0, y: 0, z: 0,
animations: { drive: 'tank_drive{1-4}' }
})
.addSlot('turret', {
group: 'vehicles',
sprite: 'tank_turret',
x: 5, y: -10, z: 1,
animations: { aim: 'tank_aim{1-3}' }
});
tank.setAnimation('turret', 'aim');| Método | Descripción |
|---|---|
addSlot(name, def) |
Agrega una capa con sprite, posición, z-index y animaciones |
setAnimation(slot, anim) |
Cambia la animación de una capa |
setAnimations({slot: anim}) |
Cambia animaciones de múltiples capas |
getTexture(slot) |
Obtiene el canvas/textura actual de una capa |
getSprite(slot) |
Obtiene el sprite o textura animada de una capa |
getSlotNames() |
Lista los nombres de todas las capas |
getSlot(slotName) |
Obtiene la configuración de una capa |
getAnimations(slotName) |
Obtiene el mapa de animaciones de una capa |
getCurrentAnimation(slotName) |
Obtiene el nombre de la animación activa en una capa |
update(dt) |
Actualiza todas las animaciones activas |
render(ctx, x, y) |
Renderiza en canvas ordenado por z |
toPIXI() |
Crea un PIXI.Container con todas las capas |
toDOM() |
Crea un HTMLElement posicionado con capas |
Si no necesitas agrupar cosas y solo quieres cortar una imagen rápida o crear una cuadrícula uniforme (como un Tileset para un mapa), usa SpriteProcessor.
Útil para mapas de tiles donde todos los cuadros miden lo mismo (ej. 32x32).
const tiles = await SpriteProcessor.processGrid('assets/tileset.png', {
spriteWidth: 32,
spriteHeight: 32,
columns: 10,
rows: 10,
scale: 2,
nameGenerator: (col, row) => `tile_${col}_${row}`
});const myPixiSprite = SpriteProcessor.toPIXI(spriteData);
const myCanvasTex = SpriteProcessor.toCanvas(spriteData);
const myHTMLElement = SpriteProcessor.toDOM(spriteData);SpriteProcessor incluye un visor de sprites en cuadrícula para depuración. Muestra todos los sprites cargados en el SpriteManager con sus nombres y animaciones.
Presiona Alt + D cuando haya un SpriteManager cargado (ej. window.spriteManager). Vuelve a presionar Alt + D o Escape para cerrar.
| Sección | Descripción |
|---|---|
| Columna izquierda | Sprites raw ordenados por número (sprite40–sprite338) + animaciones agrupadas por grupo |
| Columna derecha | Panel de previsualización (oculto hasta hacer click en un asset) |
- Sprite ampliado 256×256 con escalado automático
- Slider de rotación (0–360°) con ejes de referencia
- Controles de animación (solo para animaciones multi-frame):
- Botón Play/Pause
- Slider de frame (scrub manual)
- Slider de velocidad (1–20 fps)
- Checkbox "Bucle infinito": marcado = loop infinito, desmarcado = reproduce una vez y se detiene en el último frame (como un estado sin loop)
- Click en el botón ✕
- Presionar
DoEscape - Click fuera del overlay
Define estados con animaciones (looping o no-looping) y transiciones automáticas. Un estado sin loop reproduce la animación una vez y se detiene en el último frame. Si tiene nextState, transiciona automáticamente al terminar.
Útil para proyectiles, explosiones, animaciones de muerte, IA de enemigos, etc.
Representa un estado individual con su propia animación.
const estado = new SpriteState({
name: 'explotar',
frames: explosionFrames, // Array<HTMLCanvasElement>
speed: 12, // FPS (default 10)
loop: false, // true = infinito, false = una vez
nextState: 'done', // Estado al que transicionar al terminar (solo no-loop)
onEnter: (entity) => { /* al entrar al estado */ },
onUpdate: (entity, dt) => { /* cada frame */ },
onExit: (entity) => { /* al salir del estado */ },
onComplete: (entity) => { /* cuando la animación termina (solo no-loop) */ },
});| Propiedad | Tipo | Descripción |
|---|---|---|
name |
string |
Nombre del estado |
frames |
Array<Canvas> |
Texturas de cada frame |
speed |
number |
FPS de la animación |
elapsed |
number |
Tiempo acumulado en segundos |
currentFrame |
number |
Frame actual |
progress |
number |
Progreso 0–1 |
completed |
boolean |
¿Animación terminó? (no-loop) |
loop |
boolean |
¿Reproduce en bucle? |
| Método | Descripción |
|---|---|
update(dt) |
Avanza la animación |
getTexture() |
Canvas del frame actual |
reset() |
Reinicia al frame 0 |
Máquina de estados completa con transiciones automáticas.
const fsm = new SpriteStateMachine(
owner, // Entidad dueña (proyectil, enemigo, etc.)
{ // Mapa de estados
idle: { frames: idleFrames, loop: true },
walk: { frames: walkFrames, loop: true },
atk: { frames: atkFrames, loop: false, nextState: 'idle' },
hit: { frames: hitFrames, loop: false, nextState: 'dead',
onComplete: (e) => spawnParticles(e) },
dead: { frames: [vacio], loop: false },
},
'idle' // Estado inicial
);
// En el game loop:
update(dt) {
fsm.update(dt);
const tex = fsm.getTexture();
if (tex) ctx.drawImage(tex, x, y);
}
// Cambiar estado manualmente:
fsm.setState('hit');| Método | Descripción |
|---|---|
addState(name, config) |
Agrega o reemplaza un estado |
setState(name) |
Transiciona al estado (dispara onExit → onEnter) |
update(dt) |
Actualiza el estado actual + auto-transición si nextState |
getTexture() |
Textura del frame actual |
| Propiedad | Tipo | Descripción |
|---|---|---|
currentStateName |
string |
Estado activo |
prevStateName |
string |
Estado anterior |
stateTime |
number |
Segundos en el estado actual |
elapsed |
number |
Alias de stateTime |
progress |
number |
Progreso del estado actual (0–1) |
completed |
boolean |
¿El estado actual terminó? |
class Projectil {
constructor() {
this.fsm = new SpriteStateMachine(this, {
fly: { frames: flechaFrames, loop: true },
hit: { frames: explosionFrames, loop: false, nextState: 'done' },
done: { frames: [canvasVacio], loop: false },
});
this.fsm.setState('fly');
}
update(dt) {
if (this.fsm.currentStateName === 'fly') {
this.x += this.vx * dt;
this.y += this.vy * dt;
}
this.fsm.update(dt);
if (this.fsm.currentStateName === 'done') {
mundo.remover(this);
}
}
render(ctx) {
const tex = this.fsm.getTexture();
if (tex) ctx.drawImage(tex, this.x, this.y);
}
}