FERRETGame engine over Babylon.js
Ferret

@ferret/core

World, fixed-step loop, entities and components, services, event bus, logging and the debug command channel.

22 exports · source in packages/core/src

Classes

CommandChannel class

class CommandChannel
register(name: string, description: string, fn: CommandFn): void
has(name: string): boolean
list(): CommandInfo[]
async execute(name: string, args: any = {}): Promise<unknown>

packages/core/src/commands.ts:9

Component class

abstract class Component<P extends object = Record<string, any>>

A component is a plain class attached to an entity. Parameters come from the manifest (params). Hooks: onAttach (attached), onStart (first fixed step after attach), onFixedUpdate(dt) at the fixed rate, onUpdate(dt) once per rendered frame, onDetach (removed or entity destroyed).

entity: Entity
started: boolean
enabled: boolean
constructor(public params: P = {} as P)
get world(): World
get scene(): BABYLON.Scene
get node(): BABYLON.TransformNode
onAttach?(): void
onStart?(): void
onFixedUpdate?(dt: number): void
onUpdate?(dt: number): void
onDetach?(): void

packages/core/src/component.ts:9

Entity class

class Entity

An entity wraps a Babylon TransformNode and carries components.

readonly id: string
readonly components: Component[]
readonly tags: Set<string>
timeDilation: numberPer-entity time dilation (a game may slow its NPCs). 1 = normal.
destroyed: boolean
data: Record<string, any>
constructor(readonly world: World, readonly node: BABYLON.TransformNode, id?: string)
get name(): string
get position(): BABYLON.Vector3
add<C extends Component>(cls: ComponentClass<C> | string, params?: any): C
get<C extends Component>(cls: ComponentClass<C> | string): C | undefined
require<C extends Component>(cls: ComponentClass<C> | string): C
remove(c: Component): void
destroy(): void

packages/core/src/entity.ts:7

EventBus class

class EventBus

Small event bus. emit calls handlers synchronously, in subscription order.

on<T = any>(name: string, fn: Handler<T>): () => void
once<T = any>(name: string, fn: Handler<T>): () => void
off(name: string, fn: Handler): void
emit<T = any>(name: string, payload?: T): void
clear(): void

packages/core/src/events.ts:4

Logger class

class Logger
level: LogLevel
sinks: Array<(level: LogLevel, scope: string, msg: string) => void>Sinks receive every line at or above level; the OS Beroun platform adds one that posts to the shell's log.
constructor(readonly scope: string)
debug(m: string)
info(m: string)
warn(m: string)
error(m: string)

packages/core/src/log.ts:4

Registry class

class Registry<T>

A name -> constructor registry. A manifest names components and behaviours; games register the classes.

constructor(readonly kind: string)
register(name: string, item: T): this
get(name: string): T
has(name: string): boolean
names(): string[]

packages/core/src/registry.ts:2

SceneHost class

class SceneHost implements Service

The scene host: the place for a second World over a second Scene on the same Engine, so a game's interlude can be an ordinary scene with its own lights, camera and behaviours while the target level loads in the main one. Both scenes share the GL context, shaders and texture caches. Only the active world is rendered (rendering both cost 15x in WebKit, S0). The main world always advances (its services - ui, net, input - keep running, and its paused flag still decides whether the simulation steps); another world advances only while it is the active one.

readonly name: "host"
constructor(readonly engine: BABYLON.AbstractEngine)
init(world: World): void
get main(): WorldThe world the level service builds into and the game normally plays in.
get active(): WorldThe world that is advanced and rendered now.
worlds(): World[]
get(name: string): World | undefined
nameOf(w: World): string | undefined
create(name: string, opts: HostWorldOptions = {}): WorldA new world over a new scene on the shared engine. It is not shown until show. The game owns what is in it.
show(world: World): voidShow a world: from the next frame it is the one advanced and rendered. The one that was shown is not advanced any more.
showMain(): voidShow the main world again (a travel does this when the player is let in, and when a transition fails).
dispose(world?: World): voidDispose a world the game created, with its scene (all of them with no argument: the service's own dispose). If it is shown, the main world is shown first.
advance(realDt: number): voidOne real frame: advance the main world, and the active one if it is another.
render(): voidRender the active world's scene.

packages/core/src/host.ts:15

World class

class World

The World owns the Babylon scene, the fixed-step simulation loop (decoupled from rendering), the clock with time dilation, the event bus, the services and the entities.

readonly events: EventBus
readonly components: Registry<ComponentClass>
readonly behaviours: Registry<ComponentClass>
readonly commands: CommandChannel
readonly entities: Entity[]
readonly fixedStep: number
readonly maxFrame: number
timeScale: numberGlobal time scale (slow motion < 1).
time: numberSimulated seconds since the world was created (affected by timeScale, not by entity dilation).
tickCount: numberNumber of fixed steps run.
paused: boolean
constructor(readonly scene: BABYLON.Scene, opts: WorldOptions = {})
addService<S extends Service>(s: S): S
service<S extends Service = Service>(name: string): S
hasService(name: string): boolean
get alpha(): numberHow far the render frame is between the last two fixed steps (0..1), for interpolating what the camera follows.
spawn(name: string, opts: { node?: BABYLON.TransformNode; position?: BABYLON.Vector3; id?: string; tags?: string[] } = {}): EntityMake an entity over a new TransformNode (or over an existing node of the scene).
removeEntity(e: Entity): void
entity(id: string): Entity | undefined
findByName(name: string): Entity | undefined
findByTag(tag: string): Entity[]
entitiesWith<C extends Component>(cls: ComponentClass<C>): Entity[]
advance(realDt: number): numberAdvance by a real frame time: runs as many fixed steps as fit, then the per-frame updates.
step(): voidExactly one fixed step (also the way tests drive the world).
run(seconds: number): voidRun the world headless for seconds of simulated time.
dispose(): void

packages/core/src/world.ts:29

Functions

ensureBabylon function

function ensureBabylon(...parts: string[]): Promise<void>

Make sure the named parts of Babylon are loaded (core, gltf, pbr, shadows, particles, pipeline, video, debug): fetched from globalThis.FERRET_BABYLON (the folder of the parts, set by the player or the page) and imported once. Resolves at once when they are there.

packages/core/src/babylon.ts:22

hasBabylonPart function

function hasBabylonPart(part: string): boolean

Is part available now: the full build, or a part already loaded?

packages/core/src/babylon.ts:13

Interfaces

CommandInfo interface

interface CommandInfo
name: string
description: string

packages/core/src/commands.ts:7

HostWorldOptions interface

interface HostWorldOptions extends WorldOptions
clearColor?: [number, number, number]

packages/core/src/host.ts:4

Service interface

interface Service

A world-wide singleton (the rough equivalent of a UE subsystem): input, levels, zones, physics, ui ...

readonly name: string
init?(world: World): void
fixedUpdate?(dt: number): voidCalled before the components' fixed update, at the fixed rate.
update?(dt: number): voidCalled once per rendered frame.
dispose?(): void

packages/core/src/world.ts:8

WorldOptions interface

interface WorldOptions
fixedStep?: numberFixed simulation step in seconds (default 1/60).
maxFrame?: numberLongest real frame time that is simulated, in seconds; a stall is not a jump (default 0.1).

packages/core/src/world.ts:18

Types

CommandFn type

type CommandFn = (args: any) => unknown | Promise<unknown>

The debug command channel: named commands that a tool (the inspector, the model viewer, an AI agent over MCP) calls to look at and change a running world. Commands are plain JSON in, plain JSON out.

packages/core/src/commands.ts:5

ComponentClass type

type ComponentClass<C extends Component = Component> = new (params?: any) => C

packages/core/src/component.ts:26

Handler type

type Handler<T = any> = (payload: T) => void

packages/core/src/events.ts:1

LogLevel type

type LogLevel = 'debug' | 'info' | 'warn' | 'error'

packages/core/src/log.ts:1

Constants

BABYLON_VERSION const

const BABYLON_VERSION: "9.33.1"

the Babylon version this engine was built and tested with

packages/core/src/version.ts:10

FERRET_BUILD const

const FERRET_BUILD: string

the build stamp yyyymmddHHMM in the LOCAL time of the build machine, never UTC; dev when the code runs unbuilt (tests, the repository)

packages/core/src/version.ts:12

FERRET_VERSION const

const FERRET_VERSION: "0.1.0"

the engine's version (semver); every module of a release shares it

packages/core/src/version.ts:8

FERRET_VERSION_FULL const

const FERRET_VERSION_FULL: string

what to show a person: 0.1.0+202611051430

packages/core/src/version.ts:14