Vienna-WebGPU-Engine

Node Type System

Overview

Scene nodes are plain C++ classes, but scenes are saved as data (JSON). The node type system bridges the two: a registry maps a stable type-name string to how a node is constructed and how its data is read and written. This lets the editor list every available node type (built-in and project-defined) and lets scenes round-trip through files. When a saved scene references a type the current build does not have, it loads anyway and preserves the data instead of failing.

Key features:

Architecture

Components

Scene JSON format

Each node is one object:

{
  "type": "Light",
  "name": "Sun",
  "enabled": true,
  "transform": { "position": [0,0,0], "euler": [50,-30,0], "scale": [1,1,1] },
  "props": { "lightType": "Directional", "color": [1,1,1], "intensity": 2.0 },
  "children": []
}

type, name, enabled, transform, and children are handled by the serializer for every node. props is the type-specific block produced and consumed by that type’s registry hooks (light data, camera parameters, a model path, a custom node’s own members). A camera also gets a top-level "main": true when it is the scene’s main camera, since that is a scene-level fact a node cannot know about itself.

Asset paths inside props (e.g. a model path) are stored relative to the scene folder when the asset lives inside it, absolute otherwise, so a scene folder stays portable across machines.

Registering a custom node type

A project registers its node type once at startup (after the engine is initialized, so the built-ins are present). The factory is explicit because not every node is default-constructible.

#include "engine/scene/NodeTypeRegistry.h"

engine::scene::NodeTypeRegistry::instance().registerType<EnemyNode>(
    "EnemyNode",  // stable key written to scene files - do not rename casually
    "Enemy",      // label shown in the editor's Add menu
    [] { return std::make_shared<EnemyNode>(); },                 // factory
    [](const engine::scene::nodes::Node &n, nlohmann::json &props,
       const engine::scene::NodeSerializeContext &) {
        const auto &e = dynamic_cast<const EnemyNode &>(n);
        props["hp"] = e.hp;
    },
    [](engine::scene::nodes::Node &n, const nlohmann::json &props,
       const engine::scene::NodeSerializeContext &) {
        auto &e = dynamic_cast<EnemyNode &>(n);
        e.hp = props.value("hp", 100);
    });

The serialize/deserialize arguments are optional - omit them for a node with no data of its own. NodeSerializeContext::sceneFolder is available for types that need to store paths relative to the scene (the built-in Model type uses it). The cast from Node& must be dynamic_cast, not static_cast, because Node is a virtual base.

After this call, the type appears in the editor’s Add menu, round-trips through scene files, and degrades to a preserving placeholder anywhere it is not registered.

Missing types and export portability

A node is data plus behavior. The data (transform, properties, hierarchy) always travels in the JSON. The behavior is compiled C++ - it can only run in a build that compiled that type. You cannot put runnable code in a scene file.

So when a scene that uses EnemyNode is opened in a build that never registered EnemyNode:

This mirrors how Unity handles a missing script (the serialized data is kept and reconnects when the script returns). To make the behavior itself portable to a build that did not compile it, ship the type’s C++ source so the target build compiles it, or add a scripting layer - neither is part of this system.

File map