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:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit 9eead719b0
157 changed files with 47270 additions and 2 deletions
+191
View File
@@ -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