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).
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.
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:
- 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.