Reflection / Type Registry
oxide_engine::reflect is the backbone of the engine's dual-editable types
principle: every component should be readable and writable from the editor, from
scripts, and from external tools through one representation — without each of
those callers knowing the concrete Rust type.
The TypeRegistry is that bridge. It lands in Stage 5; the editor inspector
(Stage 6) and the scripting layer (Stage 10) are its first real consumers.
The idea
ECS components are concrete Rust types. An inspector panel or a script engine,
however, only has a name ("Transform") and some text — they cannot name the
type at the call site. The registry closes that gap: register a type once, and
afterwards address it generically by name.
use oxide_engine::reflect::TypeRegistry;
use oxide_engine::prelude::*;
let mut registry = TypeRegistry::new();
registry.register::<Transform>("Transform");
registry.register::<Node>("Node");
register::<T>(name) requires T: Component + Serialize + DeserializeOwned.
Internally it stores a small set of monomorphized function pointers, so there is
no per-call generic dispatch and no dyn Any downcasting at the boundary.
Generic read / write
Once registered, any holder of the name can round-trip a component on an entity as RON text — exactly what a generic inspector or a script needs:
# use oxide_engine::reflect::TypeRegistry;
# use oxide_engine::prelude::*;
# let mut registry = TypeRegistry::new();
# registry.register::<Transform>("Transform");
let mut scene = Scene::new();
let e = scene.spawn("thing", Transform::IDENTITY);
// Read generically...
let text = registry.get_ron(scene.world(), e, "Transform").unwrap();
// ...edit the text (a script or the inspector would)...
// ...and write it back — no concrete type at the call site.
registry.set_ron(scene.world_mut(), e, "Transform", &text).unwrap();
set_ron inserts the component if absent or replaces it if present, so the same
call covers "add component" and "edit component".
Enumerating an entity's components
A generic inspector renders an entity by asking the registry which registered component types it currently carries — sorted, and again with no concrete types in hand:
# use oxide_engine::reflect::TypeRegistry;
# use oxide_engine::prelude::*;
# let mut registry = TypeRegistry::new();
# registry.register::<Transform>("Transform");
# registry.register::<Node>("Node");
# let mut scene = Scene::new();
# let e = scene.spawn("thing", Transform::IDENTITY);
for name in registry.components_on(scene.world(), e) {
let _ron = registry.get_ron(scene.world(), e, name).unwrap();
// ... render an editor for `name` from its RON text
}
has and remove round out the surface (check for / detach a component by
name). Errors are specific — UnknownType, NoSuchEntity, Missing, and
Parse — so callers can tell "no such type" from "bad text".
Per-field reflection — #[derive(Reflect)] (Stage 8.5)
Whole-value reflection is enough for serialization and scripts, but a
Unity/Godot-style inspector needs to see a component's named fields so it
can render one widget per field. The Reflect trait provides that, and
#[derive(Reflect)] generates it:
use oxide_engine::reflect::Reflect;
#[derive(Reflect, serde::Serialize, serde::Deserialize)]
struct Timer {
pub repeating: bool,
pub duration: f32,
#[reflect(skip)]
pub elapsed: f32, // runtime state — not an authored field
}
let mut t = Timer { repeating: true, duration: 2.5, elapsed: 0.0 };
// Enumerate fields (name + syntactic type) — what an inspector iterates.
for field in t.fields() {
let _value_ron = t.get_field(field.name); // Some("true"), Some("2.5"), …
// pick a widget from `field.type_name`: "bool" → checkbox, "f32" → drag, …
}
// Edit one field without touching the rest; round-trips as RON.
t.set_field("duration", "9.0").unwrap();
Selection rules (deliberate, matching the engine's public-fields-are-the- editable-surface convention):
- Only
pubfields are reflected. Private fields are implementation detail. #[reflect(skip)]excludes a public field (e.g. runtime-only state).- Each reflected field must be
serde-serializable — get/set round-trip through RON, the same representation the whole-value path uses.
FieldInfo::type_name is the field type's syntactic spelling ("f32",
"bool", "Vec3", "Handle < Font >"). The generic inspector dispatches a
widget on it and falls back to a raw RON editor for types it does not recognize.
Per-field errors are UnknownField and FieldParse.
The derive lives in the small oxide-engine-derive proc-macro crate and is
re-exported as oxide_engine::reflect::Reflect (the macro shares its name with
the trait, the same way serde's Serialize does). This is the spine of the
reflection-driven editor inspector and the dual-editable-types principle — a new
component becomes editor- and script-editable with #[derive(Reflect)] plus one
registration line, no per-type editor code.
Through the registry: fields by type name + entity
A type registered with register_reflected::<T>("Name") (instead of plain
register) exposes its fields through the TypeRegistry too, so the editor
can reach a field given only a type name + entity — no concrete type at the
call site:
# use oxide_engine::reflect::TypeRegistry;
# use oxide_engine::prelude::*;
# let mut registry = TypeRegistry::new();
registry.register_reflected::<Transform>("Transform");
# let mut scene = Scene::new();
# let e = scene.spawn("thing", Transform::IDENTITY);
for field in registry.field_infos(scene.world(), e, "Transform").unwrap() {
let _ron = registry.get_field(scene.world(), e, "Transform", field.name).unwrap();
// render a widget from field.type_name, write edits back with set_field(...)
}
Transform and Node are registered reflected by default. Field access on a
whole-value-only type returns NotReflected. Whole-value (get_ron/set_ron)
and per-field (field_infos/get_field/set_field) coexist: the registry
addresses types by name; per-field reaches fields within a value.
Enum fields — #[derive(ReflectEnum)]
Per-field reflection tells the inspector a field's type name but not, for an
enum-typed field, which values it may take. #[derive(ReflectEnum)] (unit
variants only) exposes the variant list so the inspector renders a dropdown
instead of a free-text RON box:
use oxide_engine::reflect::{ReflectEnum, TypeRegistry};
#[derive(ReflectEnum, serde::Serialize, serde::Deserialize)]
enum Facing { North, East, South, West }
let mut registry = TypeRegistry::new();
registry.register_enum::<Facing>("Facing");
assert_eq!(registry.enum_variants("Facing"), Some(["North","East","South","West"].as_slice()));
Each variant name is valid RON for that unit variant, so a chosen name writes
straight back through set_field. An enum is a field type, not a component, so
register_enum is independent of component registration.
Who owns the registry
The registry is owned by the app / module system (Stage 5): each module registers the component types it introduces, so the editor and scripts can reach every type any module added. Because names are the identity used in serialized data and UI, keep them stable across versions.