Skip to content

Runtime

The runtime layer provides the scene graph (Scene + Node), transforms, and update/render traversal.

Scene

A Scene: - Owns the root node, the root UI, and the active camera. - Creates nodes via a shared NodeRegistry. - Updates all Updatable components each frame, then runs LateUpdatable components. - Draws all Drawable components each frame. - Updates UI elements and renders them after the scene graph. - Queues node destruction for safe cleanup.

createNode() registers a scene-owned node but does not add it to the scene tree. Parent every created node with addChild(node), rootNode()->addChild(node), or another node's addChild(node) before relying on update or render traversal.

Key methods: - createNode(name) - findNode(name) - destroyNode(node) - destroyUIElement(element) - updateComponents(dt) and render(queue) - lateUpdateComponents(dt) - updateUI(dt) and rootUI() - screenToWorld() and worldToScreen() - plotNodeToUI() and plotUIToNode() for mapping scene and UI positions - mouseToWorld() - lighting() for ambient and directional model lighting

Node

A Node is a tree element with: - A Transform component built-in. - A collection of custom components. - A list of children nodes.

Components can be added with addComponent<T>(). If a component implements Drawable it is rendered. If it implements Updatable it receives per-frame updates. If it implements LateUpdatable, it receives a second pass after all regular component updates and physics stepping.

Use getComponent<T>(), removeComponent(pointer), or removeComponents<T>() to query or remove attached components. isActive on either a node or component suppresses its runtime participation. Scene-owned nodes should be destroyed with Scene::destroyNode() so destruction occurs at the safe end-of-frame point. removeChild() and clearChildren() only detach nodes; they do not destroy the registry-owned nodes. A later addChild() may reattach a detached node or reparent an attached one.

auto* group = createNode("group");
addChild(group);  // Attach to this scene's root.

auto* item = createNode("item");
group->addChild(item);

auto* renderer = item->addComponent<snowpulse::SpriteRenderer>();
if (item->getComponent<snowpulse::SpriteRenderer>() == renderer) {
    item->removeComponent(renderer);
}

group->removeChild(item);
rootNode()->addChild(item);
destroyNode(item);  // Deferred until the safe cleanup point.

Frame Traversal Order

Per frame, runtime traversal order is:

  1. Scene::update(dt) (application scene logic)
  2. Scene::updateComponents(dt) (Updatable + physics step)
  3. Scene::lateUpdateComponents(dt) (LateUpdatable)
  4. Scene::updateUI(dt)
  5. EventBus::dispatchQueued()
  6. Deferred node and UI-element destruction

Transform

Transform provides position, rotation, scale, and cached matrices: - localMatrix and worldMatrix are updated by Scene::updateWorldTransforms().

Coordinate Conversion and Lighting

screenToWorld(), worldToScreen(), and mouseToWorld() use the active camera. plotNodeToUI() converts a node's world position into the coordinate space used by the scene UI, while plotUIToNode() maps an element's computed world pivot back into scene coordinates.

const glm::vec2 worldPoint = scene()->screenToWorld({ 320.0f, 180.0f });
const glm::vec2 screenPoint = scene()->worldToScreen(worldPoint);
const glm::vec2 pointerWorld = scene()->mouseToWorld();
const glm::vec2 labelPosition = scene()->plotNodeToUI(node);
const glm::vec2 elementWorld = scene()->plotUIToNode(element);

Each scene owns a SceneLighting value:

auto& lighting = scene()->lighting();
lighting.ambientColor = { 0.75f, 0.82f, 1.0f };
lighting.ambientIntensity = 0.5f;
lighting.directionalDirection = { -0.35f, -0.5f, -0.8f };
lighting.directionalColor = { 1.0f, 0.92f, 0.78f };
lighting.directionalIntensity = 0.8f;

Lighting is consumed by model rendering. Normal 2D sprite and UI batches keep their existing unlit shader behavior.

UI Root

Scene creates a UIRoot when a UIElementRegistry is present in AppContext. Add UI elements to scene()->rootUI().

auto* ui = context()->uiRegistry();
auto* root = scene()->rootUI();

auto* label = ui->create<snowpulse::UIText>("status");
label->font(assetManager()->loadFont("fonts/04b_19.json"));
label->text = "Ready";
root->addChild(label);

Sample Usage

class MyScene : public snowpulse::Scene {
public:
    bool init() override {
        auto* visual = createNode("visual");
        auto* sprite = visual->addComponent<snowpulse::SpriteRenderer>();
        sprite->sprite(appContext()->assetManager()->loadSprite("images/icon.png"));
        addChild(visual);
        return true;
    }

    void update(const float& dt) override {
        (void)dt;
    }

    void shutdown() override {}
};

For map-centric scenes, attach TiledRenderer to a node and keep application spawning and binding in application-side systems (for example, navigation or trigger binders).