Skip to content

Level loading

A "level" in Modoki is just a scene file — there's no separate level format. loadScene(path) is the one entry point for moving between levels: it fetches the new scene, spawns it into an isolated world, and atomically swaps it in. There's no unloadScene() to call yourself — loading the next level already says what should go away.

The problem base scenes solve

Say you have Level1.json and Level2.json, each with the same camera, lighting, HUD, and player-spawn rig, differing only in their level layout. Without base scenes you'd duplicate that shared rig into every level file — and duplicated state doesn't just waste disk space, it resets: a Time entity in each file means your elapsed-time clock jumps back to zero on every level swap.

A base scene fixes this. Give a level a baseScene reference (an ordinary scene asset, set from the level's Inspector) and that base loads additively alongside the level, into the same world — and it survives a swap to another level that shares the same base. Author the shared rig once, in the base; each level file becomes only what's actually per-level.

Setting a base scene

  1. Open the level scene in the Assets panel.
  2. In the Inspector, set its Base Scene field to the shared base scene asset.
  3. Save. Every level pointing at the same base now shares one authored copy of that rig.

In the Hierarchy, entities that came from the base render as a collapsed 🔗 Base group above your level's own entities, with a dirty dot if that base has unsaved edits. You can edit base entities right there in the Hierarchy — the edit is staged in the live world and, on save, routed back to the base's own file, not the level's.

Bases can nest

A base scene can itself declare a baseScene — an engine-wide base under a game-wide base under a level, for example. Modoki resolves the whole chain, root-most base first, so a level's own entities always win over anything it shares with its bases.

What happens on a level swap

When you call loadScene(nextLevel), Modoki resolves the new level's base-scene chain and compares it to what's currently loaded:

  • A base that's in both the old and new chain is carried — its live entities are kept, not re-read from the file. This is why a shared Time entity keeps ticking, a shared camera keeps its current framing, and any runtime state on that rig survives.
  • Anything new to the chain is loaded fresh from its file.
  • Anything that drops out of the chain is torn down — its entities go away and the scene-scoped resources it alone was using are released (see below).

So swapping Level1 → Level2 when both share Base.json: the base carries across untouched, Level1's own entities go away, and Level2's own entities spawn fresh.

Keeping an entity across ANY swap

Base scenes solve "shared rig authored once." For a one-off entity you want to survive a swap even without a shared base — a persistent HUD toast, an in-flight network connection object — mark it Persistent. A persistent entity is snapshotted out of the old world and respawned into the new one on every single load, regardless of what either scene's base chain looks like.

Persistent entities must be root entities (no parent) and must be pure ECS data — trait fields only. Anything living outside traits (a closure, an in-flight tween, a Web Audio node) does not survive, since there's nothing there for the snapshot to capture.

Asset unloading — automatic, scene-scoped

Every asset a scene references — models, materials, textures, HDR environments, prefabs — is owned by the scene, not by the individual entities using it. Loading a scene acquires every asset it needs; when that scene (or the whole level, if it's a base) drops out of the chain, its assets are released and disposed only if no other loaded scene still needs them.

That refcounting is why swapping between two levels that share, say, the same tree model or the same skybox HDR doesn't cause a visible re-download or pop-in — the shared asset was never released in the first place, because the incoming level's assets are acquired before the outgoing level's are released. It's also why an unused level's assets genuinely free their GPU memory once nothing references them anymore: there's no manual "unload this texture" step to remember.

Practical checklist

  • Shared rig across levels (camera, lights, HUD, player, Time) → put it in a base scene, reference it from every level.
  • A one-off entity that must survive any swap, base or not → mark it Persistent.
  • Nothing special → a level's own entities load fresh and unload automatically when you leave it; you don't need to do anything for that to work correctly.
  • Don't parent a level entity under a base entity (or vice versa) — the editor warns and rejects it, since it breaks which file the entity's edits are saved into.

For the underlying mechanics — the two-world swap, the resource refcounting internals, and the SceneManager API — see the Scene loading reference.

Next: Animation & curve editor

Built with Modoki.