The Registration System in the Untold Engine is an integral part of its Entity-Component-System (ECS) architecture. It provides core functionalities to manage entities and components, such as:
- Creating and destroying entities.
- Registering components to entities.
- Setting up helper functions for other systems by configuring necessary components.
Entities represent objects in the scene. Use the createEntity() function to create a new entity.
let entity = createEntity()Components define the behavior or attributes of an entity. Use registerComponent to add a component to an entity.
registerComponent(entityId: entity, componentType: RenderComponent.self)Example:
When you load a mesh for rendering, the system automatically registers the required components. For normal runtime code, use the async path:
setEntityMeshAsync(entityId: entity, filename: "model", withExtension: "untold") { success in
guard success else { return }
// RenderComponent, TransformComponent, material data, and mesh resources are ready.
}This function:
- Loads the mesh from the specified
.untoldfile. - Associates the mesh with the entity.
- Registers default components like RenderComponent and TransformComponent.
- Calls the completion handler when the mesh has been registered.
withExtension is optional — fold the extension into filename instead if you prefer:
setEntityMeshAsync(entityId: entity, filename: "model.untold") { success in ... }Passing withExtension explicitly (as above) still works exactly as before and takes priority if both are given; this applies to every filename/withExtension pair in the engine (setEntityMesh, setEntityMeshAsync, setEntityAnimations, setEntityGaussian, loadSceneAuthored, setColorGradeLUT).
Leaving withExtension out entirely (and not embedding one in filename
either) goes one step further for setEntityMesh/setEntityMeshAsync and
setEntityAnimations: instead of failing to resolve anything, each probes
for the format that needs disambiguating first, falling back to plain
.untold:
// Resolves "model.untoldpack" if the source .blend had more than one
// independent model, otherwise "model.untold" — the caller doesn't need
// to know which one the export produced.
setEntityMeshAsync(entityId: entity, filename: "model") { success in ... }
// Resolves "walk.untoldanim", or a plain "walk.untold" from before that
// extension existed.
setEntityAnimations(entityId: entity, filename: "walk", name: "Walk")This is the recommended default for both calls — reach for an explicit
withExtension only when you need to force one specific file regardless of
what else exists at that name. If both candidates exist for a given base
name (normally impossible: the exporter keeps .untold/.untoldpack and
.untoldanim/.untold single-owner per name, removing the stale one on
re-export — see Using the Untold Engine CLI), the
first-priority format wins and a warning is logged.
For immediate loading, use:
setEntityMesh(entityId: entity, filename: "model", withExtension: "untold")The immediate path is useful for tools and tests that need the mesh to be GPU-resident when the function returns.
For large streamed scenes, use setEntityStreamScene(...). The streaming/OCC path is owned by the tile manifest pipeline, not by direct StreamingComponent authoring.
To remove an entity and its components from the scene, use destroyEntity.
destroyEntity(entityId: entity)This ensures the entity is properly removed from all systems.
The entity's component objects are released when the destroy is finalized, or soon
after. A component that leaves the scene (its entity is destroyed, the component is
removed with scene.remove, or it is registered again on an entity that already has
it) first waits until nothing that reads the scene can still reach it, a render pass
that is running for example, and is then released: when entities are finalized, or at
the start of a following frame. Code that holds a component keeps it alive and may go
on using it, though the scene no longer has it.
Use destroyAllEntities(completion:) when you need to clear the world before loading new content.
destroyAllEntities {
// Safe point: pending destroys have been finalized.
// Load new content here (.untold, deserializeScene, etc).
}Important behavior:
destroyAllEntitiesis a deferred operation. Entities are marked for destroy first.- Final cleanup runs during the engine frame finalization step (
finalizePendingDestroys()). - The
completionblock runs only after that finalization step has finished.
This prevents race conditions where new entities are created while old entities are still pending destroy.
Example: clear world, then load a new .untold asset
destroyAllEntities {
let entity = createEntity()
setEntityMeshAsync(entityId: entity, filename: "office", withExtension: "untold")
}Example: playSceneAt pattern
public func playSceneAt(url: URL, completion: (() -> Void)? = nil) {
guard let scene = loadGameScene(from: url) else {
completion?()
return
}
destroyAllEntities {
deserializeScene(sceneData: scene) {
completion?()
}
// Early camera rebind during async mesh loading window.
setCamera(.active(findGameCamera()))
}
}Some data exported from Blender is scene-wide rather than per-mesh:
scene-authored lights/cameras, and a .cube creative grade LUT (from
--color-grade-lut, see Using the Exporter). None
of this is registered by a normal mesh load — setEntityMesh/
setEntityMeshAsync only bring in geometry and materials. Use
loadSceneAuthored alongside your mesh load to bring in the rest:
// From a single .untold asset (file-type: shared)
loadSceneAuthored(filename: "office", withExtension: "untold") { success in
// Scene-authored lights/cameras and any .cube grade are now registered.
}
// From a tile manifest
loadSceneAuthored(url: manifestURL) { success in
// Same, sourced from the manifest's scene_lights/scene_cameras/colorGradeLUT keys.
}Important behavior:
loadSceneAuthored(filename:withExtension:)only ever resolves a single.untoldfile — unlikesetEntityMeshAsync/setEntityAnimations, it does not probe for.untoldpack. A.untoldpackmanifest has no scene-level slot to begin with: a multi-model.blendscene's lights, cameras, and color-grade LUT are dropped at export time (see Using the Untold Engine CLI), so there is nothing for this call to load for a pack regardless of extension.- Calling either overload clears any previously-loaded
.cubegrade first (ColorGradeLUTParams.shared.clear()), then re-populates it only if the asset/manifest actually has one staged. - If the source has no
.cubestaged, the engine applies no creative grade — this is not an error. - The
.cubegrade can be toggled off at runtime (e.g. to compare against the tonemap operator alone) withsetPostFX(.colorGradeLUT(.enabled(false))). See Using Post-Effects. - A
.cubecan also be applied without any scene export, viasetColorGradeLUT(filename:withExtension:), resolved the same waygetResourceURLresolves any other asset (aLUT/folder underassetBasePath, or the app bundle). - Assets exported before
--bake-color-managementwas removed may still carry a legacycolorLUTbaked whole-transform LUT;loadSceneAuthoredstill loads and clears it (ColorLUTParams.shared.clear()) for backward compatibility.
See Using Color Management for the full export + load workflow.