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.pngorhero.glb. - A manifest — a small
*.asset.jsondocument 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:
| URI | Conventional path | What it names |
|---|---|---|
scene://namespace/main | scenes/main.scene.json | A scene — a set of entities loaded together. |
prefab://characters/player | prefabs/characters/player.prefab.json | A prefab — a reusable entity composition. |
ui-prefab://controls/menu_button | ui/controls/menu_button.ui.prefab.json | A UI prefab — a reusable HUD subtree. |
asset://textures/brick | assets/textures/brick.asset.json | An asset — an imported resource such as a texture, model, or sound. |
asset-group://level_one | assets/level_one.asset-group.json | An asset group — a batch of assets loadable together. |
script://scripts/player.lua | scripts/player.lua | A 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/mainkeeps 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) andwrap(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.jsonmanifest.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 physicslayernamedworld. Collider shape comes from the component type, not from the asset and not from ashapefield.
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 toAssets.is_ready,Assets.progress, orAssets.cancel.
The full call set is in Assets, time & timers.
Next steps
- Asset manifests — every manifest field and type.
- Assets & streaming — import, groups, budgets, and cooking.
- Source vs generated — where sources live and what import produces.