@katforge/forge
The game engine runtime. Manages the game loop, input mapping, asset loading, and rendering (via Pixi.js). Provides Vue composables so game systems bind directly to reactive state.
npm install @katforge/forge
Architecture
flowchart TB
subgraph forge["@katforge/forge"]
Loop["Loop<br/>(fixed timestep)"]
Input["Input<br/>(keyboard, gamepad)"]
Assets["Assets<br/>(sprites, audio)"]
Renderer["Renderer<br/>(Pixi.js)"]
end
subgraph vue["Vue composables"]
useGameLoop
useQuery
useComponent
useObserver
end
subgraph anvil["@katforge/anvil"]
World
end
Loop -->|tick| World
useGameLoop -->|bind| Loop
useQuery -->|reactive| World
Renderer -->|draw| World
Input -->|actions| WorldLoop runs the fixed-timestep update + variable-render schedule. Input translates raw keyboard / gamepad events into named actions, polled by systems each tick. Assets is a typed loader that resolves keys to sprites and audio buffers. Renderer wraps Pixi.js and the sprite-syncing helper. The Vue composables in the middle layer (useGameLoop, useQuery, useComponent, useObserver) bridge the loop to reactive state so components re-render automatically when world data changes.
Game loop
Fixed timestep with variable render interpolation:
import { Loop } from '@katforge/forge';
const loop = new Loop ({
fps: 60,
update: (dt) => world.tick (dt),
render: (alpha) => renderer.draw (world, alpha)
});
loop.start ();
Vue composable
import { useGameLoop } from '@katforge/forge';
const { world, running, fps } = useGameLoop ({
fps: 60,
setup: (world) => { /* spawn initial entities */ },
update: (world, dt) => { /* run systems */ },
render: (world, alpha) => { /* draw */ }
});
A system wired into the loop
A typical movement system reads input each tick, mutates Velocity, and lets a downstream system apply velocity to position:
import { Position, Velocity } from './components';
function movementInput (world, input, dt) {
for (const [ vel ] of world.query (Velocity, Player)) {
vel.dx = input.held ('move_right') ? 200 : input.held ('move_left') ? -200 : 0;
vel.dy = input.held ('move_down') ? 200 : input.held ('move_up') ? -200 : 0;
}
}
function applyVelocity (world, dt) {
for (const [ pos, vel ] of world.query (Position, Velocity)) {
pos.x += vel.dx * dt;
pos.y += vel.dy * dt;
}
}
useGameLoop ({
fps: 60,
update: (world, dt) => {
movementInput (world, input, dt);
applyVelocity (world, dt);
}
});
System ordering inside update is the contract — there's no auto-scheduler. Two systems that touch the same component should be sequenced explicitly.
Input
import { Input } from '@katforge/forge';
const input = new Input ({
actions: {
move_up: [ 'KeyW', 'ArrowUp' ],
move_down: [ 'KeyS', 'ArrowDown' ],
attack: [ 'Space' ],
interact: [ 'KeyE' ]
}
});
// In a system
if (input.pressed ('attack')) { /* ... */ }
if (input.held ('move_up')) { /* ... */ }
Assets
import { Assets } from '@katforge/forge';
const assets = new Assets ();
await assets.load ([
{ key: 'hero', type: 'sprite', url: '/sprites/hero.png' },
{ key: 'slash', type: 'audio', url: '/sfx/slash.ogg' }
]);
const heroTexture = assets.get ('hero');
Renderer
Wraps Pixi.js with ECS integration. The createSpriteSystem auto-syncs Pixi sprites with entities that have Position and SpriteComponent:
import { PixiRenderer, createSpriteSystem } from '@katforge/forge';
const renderer = new PixiRenderer ({ width: 1280, height: 720 });
const spriteSystem = createSpriteSystem (renderer, world);
Reactive queries (Vue)
import { useQuery, useComponent } from '@katforge/forge';
// Reactive list of entities matching Position + Health
const entities = useQuery (world, Position, Health);
// Reactive single component for a specific entity
const hp = useComponent (world, playerId, Health);
// hp.current is a Vue ref