Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# 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.
|
||||
|
||||
```rust
|
||||
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:
|
||||
|
||||
```rust
|
||||
# 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:
|
||||
|
||||
```rust
|
||||
# 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:
|
||||
|
||||
```rust
|
||||
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 `pub` fields** 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:
|
||||
|
||||
```rust
|
||||
# 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:
|
||||
|
||||
```rust
|
||||
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.
|
||||
|
||||
[`TypeRegistry`]: ../engine/src/reflect.rs
|
||||
[`UnknownType`]: ../engine/src/reflect.rs
|
||||
[`NoSuchEntity`]: ../engine/src/reflect.rs
|
||||
[`Missing`]: ../engine/src/reflect.rs
|
||||
[`Parse`]: ../engine/src/reflect.rs
|
||||
[`Reflect`]: ../engine/src/reflect.rs
|
||||
[`FieldInfo::type_name`]: ../engine/src/reflect.rs
|
||||
[`UnknownField`]: ../engine/src/reflect.rs
|
||||
[`FieldParse`]: ../engine/src/reflect.rs
|
||||
[`NotReflected`]: ../engine/src/reflect.rs
|
||||
Reference in New Issue
Block a user