Sitelet https://untoldengine.github.io/UntoldEngine/API/UsingRegistrationSystem/
Skip to content

Using the Registration System in Untold Engine

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.

How to Use the Registration System

Step 1: Create an Entity

Entities represent objects in the scene. Use the createEntity() function to create a new entity.

let entity = createEntity()

Step 2: Register Components

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 .untold file.
  • 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.


Step 3: Destroy an Entity

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.


Step 4: Destroy All Entities Safely

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:

  • destroyAllEntities is a deferred operation. Entities are marked for destroy first.
  • Final cleanup runs during the engine frame finalization step (finalizePendingDestroys()).
  • The completion block 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()))
    }
}

Loading Scene-Authored Data

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 .untold file — unlike setEntityMeshAsync/setEntityAnimations, it does not probe for .untoldpack. A .untoldpack manifest has no scene-level slot to begin with: a multi-model .blend scene'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 .cube grade first (ColorGradeLUTParams.shared.clear()), then re-populates it only if the asset/manifest actually has one staged.
  • If the source has no .cube staged, the engine applies no creative grade — this is not an error.
  • The .cube grade can be toggled off at runtime (e.g. to compare against the tonemap operator alone) with setPostFX(.colorGradeLUT(.enabled(false))). See Using Post-Effects.
  • A .cube can also be applied without any scene export, via setColorGradeLUT(filename:withExtension:), resolved the same way getResourceURL resolves any other asset (a LUT/ folder under assetBasePath, or the app bundle).
  • Assets exported before --bake-color-management was removed may still carry a legacy colorLUT baked whole-transform LUT; loadSceneAuthored still loads and clears it (ColorLUTParams.shared.clear()) for backward compatibility.

See Using Color Management for the full export + load workflow.