DEMIENGINEDOCUMENTATION Everything is free
Core concepts · Start here

URI schemes, conventional paths, and why IDs never change.

Assets & stable IDs

Games are made of files that are not JSON or Lua: images, models, sounds, fonts. DemiEngine calls these assets, and every asset has a permanent name — a URI — that scenes and scripts use to refer to it. This page explains what an asset is, lists every URI scheme and its conventional file path, and shows why those names are designed to stay stable.

What an asset is

An asset is an imported resource plus the settings that describe how to use it. On disk it is two things under assets/:

  • A source file — the original material you added to the project, such as brick.png or hero.glb.
  • A manifest — a small *.asset.json document that gives the asset its URI identity, names its type, points at the source, and records import settings.

The manifest is what the game references. Moving or renaming the source file is safe as long as the manifest still points at it — the URI everyone else uses does not change.

URI schemes and conventional paths

Every durable thing in a project has a URI identity. The scheme says what kind of thing it is, and the rest of the URI maps to a conventional file path so tools can find the file without a separate registry:

URIConventional pathWhat it names
scene://namespace/mainscenes/main.scene.jsonA scene — a set of entities loaded together.
prefab://characters/playerprefabs/characters/player.prefab.jsonA prefab — a reusable entity composition.
ui-prefab://controls/menu_buttonui/controls/menu_button.ui.prefab.jsonA UI prefab — a reusable HUD subtree.
asset://textures/brickassets/textures/brick.asset.jsonAn asset — an imported resource such as a texture, model, or sound.
asset-group://level_oneassets/level_one.asset-group.jsonAn asset group — a batch of assets loadable together.
script://scripts/player.luascripts/player.luaA script — a Lua gameplay file.

Path inference works the same way for every scheme: the URI scene://namespace/main becomes scenes/main.scene.json, prefab://characters/player becomes prefabs/characters/player.prefab.json, asset://textures/brick becomes assets/textures/brick.asset.json, and so on down the table. Project scene entries may omit path when the conventional path can be inferred from the scene ID; an explicit path remains valid when you need one. The mapping is also summarized in Core concepts.

Why IDs are stable

Scenes, scripts, and packages reference assets by URI, never by file path or list position. That identity is the point:

  • References survive file moves — reorganizing folders or renaming the source file does not break the fifty places that use the brick texture, because they say asset://textures/brick, not a path.
  • References survive reordering — nothing refers to "the third asset". Identity is a name, not a slot.
  • Saves and tests stay valid — a save file or a test that names scene://game/main keeps working after unrelated files move.

The practical rule: choose an asset ID once and treat it like a public name. Renaming a URI is a real rename that every reference must follow; renaming a file behind the same URI is not.

A manifest at a glance

{
  "format_version": 1,
  "id": "asset://textures/brick",
  "type": "Texture2D",
  "source": "brick.png",
  "importer": "image",
  "settings": {
    "filter": "nearest",
    "wrap": "clamp"
  }
}
  • format_version — the document format number; required on every manifest.
  • id — the stable URI this asset is known by. Matches the manifest's conventional path.
  • type — what kind of resource this is (Texture2D, Model3D, AudioClip, and so on). The type decides which fields matter and which components can use it.
  • source — the imported source file, relative to the manifest.
  • importer — which import pipeline reads the source (image, gltf-model, model, audio, font, …).
  • settings — import and runtime options for the type. For textures: filter (nearest|bilinear|trilinear) and wrap (repeat|clamp|mirror), plus optional per-platform overrides.

Importing a file writes this manifest for you:

demi asset import assets/textures/brick.png \
  --id asset://textures/brick
  • asset import — copies the source into the project and writes its *.asset.json manifest.
  • assets/textures/brick.png — the source file to import.
  • --id asset://… — the stable identity the project will reference.
  • --id asset://textures/brick — the stable identity to assign. This is the name the rest of the game will use.

Every manifest field, importer list, and settings schema is documented in Asset manifests. Loading and grouping at runtime is covered in Assets & streaming.

Referencing assets from components

Components point at assets by URI in their fields. The asset supplies the resource; the component supplies the gameplay meaning:

{
  "id": "wall",
  "components": {
    "Transform2D": { "position": [3, 0] },
    "Sprite": {
      "texture": "asset://textures/brick",
      "size": [2, 2]
    },
    "BoxCollider2D": { "size": [2, 2], "layer": "world" }
  }
}
  • Sprite.texture — an asset URI naming the image to draw. Returns nothing itself; the sprite draws that resource each frame.
  • Sprite.size — how large the sprite is drawn, in world units.
  • BoxCollider2D — the collision shape, sized to match the wall and sitting on the physics layer named world. Collider shape comes from the component type, not from the asset and not from a shape field.

Note what the asset does not carry: the brick texture has no health, no collision, no rules. A model asset is just as inert. Those belong on the entity that uses the asset — a Sprite to draw it, a collider to block movement, a LuaScript for behavior — and that combination is exactly what a prefab saves for reuse (Prefabs & instances).

From Lua, the same URIs address assets at runtime:

local Assets = require("demi.assets")

local request, err = Assets.load("asset://textures/brick")
if not request or request == 0 then error(err) end
-- Assets.is_ready(request) tells you when the texture is resident.
  • require("demi.assets") — imports the asset service; returns its function table.
  • Assets.load(uri) — starts a non-blocking load and returns a request ID plus an error string. Pass the ID to Assets.is_ready, Assets.progress, or Assets.cancel.

The full call set is in Assets, time & timers.

Next steps