FERRETGame engine over Babylon.js
Ferret

Levels and packages

A level is a folder (or a .ferret package) with a glTF scene and a manifest that says which nodes are gameplay entities, where the player may start and how the level looks and behaves. Artists move things in Unreal or in the editor; because the manifest refers to nodes by name, the wiring survives a re-export.

The manifest#

manifest.json (LevelManifest) sits next to scene.glb:

{
  "version": 1,
  "name": "Yard",
  "scene": "scene.glb",
  "navmesh": "navmesh.bin",
  "defaultSpawn": "gate",
  "spawns": [{ "id": "gate", "position": [0, 0, 8], "yaw": 3.14 }],
  "entities": [
    { "id": "door1", "node": "Door_Main", "behaviour": "Door", "params": { "slide": [0, 2.4, 0] } },
    { "id": "key",   "node": "Key",       "behaviour": "Pickup", "params": { "item": "brass-key" } }
  ],
  "colliders": [{ "node": "Fence_Decor", "collide": false }],
  "environment": { "sun": { "intensity": 3 }, "fog": { "density": 0.01 } },
  "physics": { "capabilities": ["gravity", "friction"], "gravity": { "vector": [0, -9.81, 0] } }
}
FieldMeaning
scene, navmesh, collisionfile names; a collision mesh of simple shapes replaces the visible meshes as solids
collisionChunksthe solid shapes outside the play area in chunks (loadRadius, unloadRadius, chunks: a file and its bounds each); a chunk is fetched when the player is within loadRadius of its bounds and released beyond unloadRadius; needs collision
navBuildhow to build the NavMesh when navmesh.bin is missing: agent size, climb, slope, cell size; bounds (one {min, max} box in world metres, or a list: the NavMesh is built inside the union, like several Unreal NavMeshBoundsVolumes); exclude (boxes where nothing is walkable, like NavModifierVolumes with the null area; yaw, radians about +y, turns a box about its centre)
entitiesManifestEntity: a node to wrap (or a new empty one), a behaviour or several components, params, tags, a transform
spawns, defaultSpawnwhere the player may start; the level is entered at defaultSpawn when none is named
zonesnamed zones for the ZoneService
movementthe player's movement values for this level, over Ferret's defaults
animGraphsanimation graphs by name
collidersper-node overrides: make a node solid or walk-through whatever the export says
environmentsun, ambient light, fog, sky, fixed effects (EnvironmentDef)
physicsrealism for this level: materials, pair rules, capabilities and their values (Physics overview)

validateManifest returns a list of problems (path and message); an empty list means the manifest is usable. The same check runs at load time and in the editor.

The level service#

LevelService is the level registry and travel. A game registers LevelDefs, each with a builder that gets a LevelContext. travel(id) shows the loading screen, removes the old level, builds the new one, places the player at a spawn and hides the loading screen again; it reports with events (level:loading, level:loaded, ...). Everything a builder creates (nodes, lights, materials, entities) is removed again when the level goes, so levels do not leak into each other.

A LevelDoor in the level is the usual way into another one: using it calls travel.

Packages#

installPackages teaches a LevelService to play packages: a folder with a manifest, or a single .ferret file. PackageMount registers the package's levels (the first package's levels as <id>, later ones as <pkgid>/<id>). Packages are loaded from a URL, from a File, or from bytes; the pkg.* commands add and remove them at run time (Commands).

A package can be signed and its files encrypted; the loader checks signatures, hashes and paths before anything is used, and a package that carries code runs under a CodePolicy. validatePackage runs the package's own checks. See @ferret/assets for the format and its security checks.

Code in a package#

A package of kind library (or a game) may carry code: one bundled ES module under code/, named in ferret.json as "code": { "entry": "code/index.js" } (a game may name it as entry.code), that exports install(ctx). installCode gives a mount a CodeLoader; require(id) (or the command pkg.require { id }) runs a package's code:

  1. the code of its dependencies first (each must be open, or opened through the loader's open hook, e.g. from the catalog, and fit its version range);
  2. the package must pass its CodePolicy: it comes from where the game does (origin), or it carries a valid signature (from a trusted key when the game keeps a list);
  3. the module is read once and checked against its sha256 in the manifest, and those bytes are imported (a blob URL), so what runs is what was checked;
  4. install(ctx) is called once. ctx has the world, the engine's API as ctx.ferret, the package (ctx.pkg), ctx.url(path) for its files and ctx.require(dep) for a dependency's exports. It may return { dispose }.

The module does not import the engine: it is built with @ferret/* and Babylon as externals and reaches them through ctx.ferret and the global BABYLON, so it runs against the shared engine (lib/ferret/ferret.js, globalThis.Ferret). A game that bundles its own copy of the engine passes its API as the loader's ferret, or library code is refused with that reason. A page runs one version of a package's code: a module cannot be unloaded, so another version needs a reload. pkg.loaded lists what runs; code:loaded and code:failed are the events. provides names what a library's code registers (capabilities, kinds, flowSources, forms, commands, levelKeys), so a level can be resolved to the packages it needs without running them.

Environment#

applyEnvironment builds a level's look in the scene (sun, ambient light, fog, sky, effects) and returns an Environment to dispose when the level goes. Quality tiers decide which effects are on.