Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scene Hierarchy

A scene hierarchy is a parent-child tree of entities where each child's transform is interpreted relative to its parent. Moving the parent moves every descendant with it. This is how a robot arm holds together as one piece, how a sword stays in a character's hand during an animation, and how an entire scene can be translated by editing a single root entity.

Parent-Child Relationships

Setting a Parent

Setting the parent attaches a child to its parent and marks the local transform dirty so the next propagation pass picks up the change.

#![allow(unused)]
fn main() {
world.core.set_parent(child_entity, Parent(Some(parent_entity)));
world.core.set_local_transform_dirty(child_entity, LocalTransformDirty);
}

After the attach, the child's LocalTransform.translation, rotation, and scale are measured relative to the parent. A child at translation (0, 1, 0) with a parent at translation (5, 0, 0) renders at world-space (5, 1, 0).

Getting Children

The children_cache resource maps each parent entity to a Vec<Entity> of its direct children. The cache is rebuilt lazily when stale, so the lookup is a constant-time HashMap query.

#![allow(unused)]
fn main() {
if let Some(children) = world.resources.children_cache.get(&parent_entity) {
    for child in children {
    }
}
}

Detaching

Setting the parent to None detaches the child. Its local transform is then measured in world space.

#![allow(unused)]
fn main() {
world.core.set_parent(child_entity, Parent(None));
}

Transform Propagation

The engine runs propagate_transforms once per frame as part of the frame schedule. The pass walks every dirty entity, recomputes its global transform, and clears the dirty flag. Doing this manually is unnecessary, the schedule handles it.

#![allow(unused)]
fn main() {
propagate_transforms(world);
}

The propagation pass does five things in order. First, it gathers every entity marked LocalTransformDirty. Second, it walks descendants of each dirty entity and marks them dirty too, because a parent change invalidates the children's world positions. Third, it sorts the dirty set so parents are processed before children. Fourth, it computes each entity's global transform as either parent.GlobalTransform * LocalTransform if the entity has a parent, or LocalTransform.to_matrix() if it does not. Fifth, it clears the dirty flag.

Local vs Global Transform

#![allow(unused)]
fn main() {
pub struct LocalTransform {
    pub translation: Vec3,
    pub rotation: Quat,
    pub scale: Vec3,
}

pub struct GlobalTransform(pub Mat4);
}

LocalTransform is the editable form. Position, rotation, scale, all relative to the parent or to the world if there is no parent. Writing to a LocalTransform requires marking the entity dirty so the next propagation pass picks it up.

GlobalTransform is the read-only output. A single 4x4 matrix that bakes in the entire chain of parents. The renderer reads from GlobalTransform. Physics reads from GlobalTransform. Game logic that needs world-space coordinates reads from GlobalTransform.

Scene Serialization

Scenes can be saved to disk and reloaded. This is the format used by the level editor and the runtime loader.

Scene Structure

#![allow(unused)]
fn main() {
pub struct Scene {
    pub name: String,
    pub entities: Vec<SerializedEntity>,
    pub hierarchy: Vec<HierarchyNode>,
    pub assets: SceneAssets,
}

pub struct SerializedEntity {
    pub id: u64,
    pub name: Option<String>,
    pub components: SerializedComponents,
}

pub struct SceneAssets {
    pub textures: Vec<TextureReference>,
    pub materials: Vec<MaterialReference>,
    pub meshes: Vec<MeshReference>,
}
}

Saving

world_to_scene walks the world and serializes every entity and the hierarchy that connects them. The result is a Scene value that can be written to disk.

#![allow(unused)]
fn main() {
let scene = world_to_scene(world);
save_scene(&scene, "level1.scene")?;
}

Loading

#![allow(unused)]
fn main() {
let scene = load_scene("level1.scene")?;
spawn_scene(world, &scene);
}

spawn_scene allocates fresh entity ids and rebuilds the hierarchy. The ids in the saved file are not preserved across loads, since they have to coexist with whatever entities the live world already contains.

Binary Format

For larger scenes, the binary form is faster to load and smaller on disk.

#![allow(unused)]
fn main() {
let bytes = serialize_scene_binary(&scene)?;
let scene = deserialize_scene_binary(&bytes)?;
}

Recursive Operations

Despawning with Children

A parent and all its descendants can be despawned in a single call. Use the deferred command queue when calling from inside a system, or the immediate helper when the entity must be gone before the next system runs.

#![allow(unused)]
fn main() {
despawn_recursive_immediate(world, parent_entity);
}

Cloning Hierarchy

clone_entity_recursive produces a deep copy of an entity and every descendant. The clones get fresh ids and a fresh hierarchy that mirrors the original.

#![allow(unused)]
fn main() {
let clone = clone_entity_recursive(world, original_entity);
}

Example: A Robot Arm

The robot arm illustrates the hierarchy pattern. Four entities chained by parent pointers. Rotating the lower arm rotates everything above it.

#![allow(unused)]
fn main() {
fn spawn_robot_arm(world: &mut World) -> Entity {
    let base = spawn_cube_at(world, Vec3::zeros());
    world.core.set_name(base, Name("Base".to_string()));

    let lower_arm = spawn_cube_at(world, Vec3::new(0.0, 1.5, 0.0));
    world.core.set_name(lower_arm, Name("Lower Arm".to_string()));
    world.core.set_parent(lower_arm, Parent(Some(base)));

    let upper_arm = spawn_cube_at(world, Vec3::new(0.0, 2.0, 0.0));
    world.core.set_name(upper_arm, Name("Upper Arm".to_string()));
    world.core.set_parent(upper_arm, Parent(Some(lower_arm)));

    let hand = spawn_cube_at(world, Vec3::new(0.0, 1.5, 0.0));
    world.core.set_name(hand, Name("Hand".to_string()));
    world.core.set_parent(hand, Parent(Some(upper_arm)));

    base
}

fn rotate_arm(world: &mut World, lower_arm: Entity, angle: f32) {
    if let Some(transform) = world.core.get_local_transform_mut(lower_arm) {
        transform.rotation = nalgebra_glm::quat_angle_axis(
            angle,
            &Vec3::z(),
        );
    }
    world.core.set_local_transform_dirty(lower_arm, LocalTransformDirty);
}
}

The lower arm's translation (0, 1.5, 0) is relative to the base. The upper arm's translation (0, 2.0, 0) is relative to the lower arm. The hand's translation (0, 1.5, 0) is relative to the upper arm. Rotating the lower arm by setting its local rotation propagates through the chain on the next frame, and the upper arm and hand swing with it.