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
+232
View File
@@ -0,0 +1,232 @@
# Scene Graph & Entity System
The `oxide_engine::scene` module is the world model every later system plugs
into. It pairs a lightweight ECS ([`hecs`](https://docs.rs/hecs)) with a
parent/child [`Transform`](math.md) hierarchy, so entities can hold arbitrary
components *and* live in a spatial tree.
This document is the usage reference for the module as delivered in **Stage 3**.
For the math types it builds on, see [math.md](math.md); for coordinate and
units conventions, see [conventions.md](conventions.md).
## Importing
```rust
use oxide_engine::scene::{Scene, Node, Entity, DespawnPolicy, SceneError};
// or, for the common types, via the prelude:
use oxide_engine::prelude::*; // Scene, Node, Entity, DespawnPolicy, SceneError, Transform, …
```
The full ECS is re-exported as `oxide_engine::hecs` so you share one copy of
`Entity` and the query API with the engine.
## Mental model
- An **entity** is a `hecs::Entity` handle — a small `Copy` id.
- Every entity created through the scene carries a [`Node`](#node) (name +
enabled flag) and a **local** [`Transform`](math.md).
- The **hierarchy** (which entity parents which) is owned by the `Scene`, not
stored as components. This keeps child ordering deterministic and makes
reparenting cheap.
- A **local** transform is what you author. A **world** transform is the local
composed with every ancestor: `world = parent_world * local`. The scene
resolves these on demand; it does not cache them.
| Type | Role |
|------|------|
| [`Scene`](#scene) | Owns entities + hierarchy; spawn, despawn, reparent, query, resolve transforms |
| [`Node`](#node) | Per-entity metadata: `name`, `enabled` |
| [`DespawnPolicy`](#despawning) | Whether despawn takes the subtree or detaches children |
| [`SceneError`](#errors) | Reparent / (de)serialization failures |
---
## `Scene`
### Building a hierarchy
```rust
use oxide_engine::prelude::*;
let mut scene = Scene::new();
// A root entity (no parent).
let sun = scene.spawn("sun", Transform::IDENTITY);
// Children. `spawn_child` panics if the parent is not a live entity.
let planet = scene.spawn_child(
sun,
"planet",
Transform::from_translation(Vec3::new(10.0, 0.0, 0.0)),
);
let moon = scene.spawn_child(
planet,
"moon",
Transform::from_translation(Vec3::new(0.0, 0.0, 2.0)),
);
```
`spawn`/`spawn_child` take anything that converts into a `Node`, so a bare
`&str` works as a name (`"planet"``Node::new("planet")`); pass a `Node`
directly when you need to set `enabled`.
### Resolving world transforms
```rust
// One entity (walks up the parent chain):
let moon_world = scene.world_transform(moon).unwrap();
// Every entity at once (single top-down pass — prefer this in bulk):
let worlds = scene.world_transforms(); // HashMap<Entity, Transform>
```
`world_transforms()` is the path the renderer will use; it resolves a
10,000-entity, 5-level scene in well under a millisecond (see the
`world_transforms_10k_depth5` benchmark).
### Reparenting
```rust
scene.set_parent(moon, Some(sun))?; // moon now orbits the sun directly
scene.set_parent(moon, None)?; // moon becomes a root
```
Reparenting preserves the **local** transform (it does not compensate to keep
the world transform fixed). Cycles are rejected: parenting an entity to itself
or to one of its descendants returns [`SceneError::WouldCycle`], leaving the
hierarchy untouched.
### Despawning
```rust
use oxide_engine::scene::DespawnPolicy;
// Remove the entity and its entire subtree:
scene.despawn(planet, DespawnPolicy::Recursive);
// Remove only the entity; its children move up to its parent (or become roots
// if it was a root):
scene.despawn(planet, DespawnPolicy::DetachChildren);
```
### Editing nodes
```rust
scene.set_name(planet, "earth");
scene.set_enabled(moon, false); // later systems skip disabled subtrees
scene.set_local_transform(moon, Transform::IDENTITY);
let name = scene.name(planet); // Option<String>
let on = scene.is_enabled(moon); // Option<bool>
let local = scene.local_transform(moon); // Option<Transform>
```
### Querying
```rust
for &root in scene.roots() { /**/ }
for &child in scene.children(planet) { /**/ }
let parent = scene.parent(moon); // Option<Entity>
let n = scene.len();
```
### Extra components (it's a real ECS)
Entities are full `hecs` entities, so later stages attach their own components
(meshes, rigid bodies, …) alongside the `Node`/`Transform`:
```rust
scene.world_mut().insert_one(planet, /* e.g. */ 0u32).unwrap();
let value = scene.get::<u32>(planet); // Option<hecs::Ref<u32>>
```
Use `world()` for read-only queries and `world_mut()` for adding/removing
*non-hierarchy* components. Drive lifecycle and parenting through the `Scene`
methods so the hierarchy bookkeeping stays consistent — spawning or despawning
directly on the world bypasses it.
---
## `Node`
```rust
pub struct Node {
pub name: String, // display name; not required to be unique
pub enabled: bool, // honored by later systems, not by transform resolution
}
```
`Node::new(name)` builds an enabled node. `enabled` is a declaration of intent:
Stage 3 only stores and toggles it; rendering/physics/audio will skip disabled
subtrees in later stages. It deliberately does **not** affect
`world_transform`, which is purely geometric.
---
## Serialization
A scene round-trips through RON. Because `hecs::Entity` handles are not stable
across a save/load, the scene is flattened to an indexed node list in a
deterministic pre-order walk, so serialize → deserialize → serialize is
byte-for-byte stable.
```rust
let ron: String = scene.to_ron()?;
let restored = Scene::from_ron(&ron)?;
assert_eq!(ron, restored.to_ron()?); // identical
```
Corrupt input (out-of-range child indices, a node listed as both root and
child) is rejected with [`SceneError::Deserialize`].
---
## Errors
```rust
pub enum SceneError {
NoSuchEntity, // operation referenced a dead entity
WouldCycle, // reparent would make an entity its own ancestor
Serialize(String), // encoding to RON failed
Deserialize(String), // decoding failed or data was inconsistent
}
```
---
## Example
A complete, runnable tour lives in
[`examples/src/bin/scene_basic.rs`](../examples/src/bin/scene_basic.rs):
```sh
cargo run -p oxide-examples --bin scene_basic
```
It builds a sun/planet/moon hierarchy, prints local vs. world transforms,
reparents a node, round-trips through RON, and despawns with a detach policy.
## In the editor
`oxide-editor` renders a **Scene Hierarchy** panel (left) and an **Inspector**
(right) over the viewport, driving this same API: select a node, rename it,
toggle its `enabled` flag, reparent it via the Inspector's parent dropdown, and
add/delete nodes from the toolbar. The egui integration is editor-only — the
engine exposes a generic post-clear draw hook ([`App::render`]) and keeps egui
out of its own dependency tree.
[`App::render`]: ../engine/src/window/app.rs
## Design notes
- **Why hierarchy outside the ECS?** Storing `Parent`/`Children` as components
is idiomatic but makes ordered iteration and reparenting awkward (archetype
moves, borrow juggling) and gives no ordering guarantee. Keeping the tree in
the `Scene` yields deterministic child order — which serialization and the
editor both rely on — and O(1) link edits. The ECS still owns all entity
*data*.
- **World transforms are resolved, not stored.** There is no dirty-flag cache
yet; `world_transforms()` recomputes in one pass. A cache can be added later
behind the same API without changing callers.
- **Despawn policies** map onto the two things callers actually want: delete a
whole subtree, or remove one node and keep its children.