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:
+232
@@ -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.
|
||||
Reference in New Issue
Block a user