Files
cursebreaker-parser-rust/PREFAB_SUMMARY.md
2026-01-05 08:12:55 +00:00

18 KiB

Prefab Instantiation Deep Dive

This document explains how prefabs and nested prefabs are instantiated in the Unity Parser.

Table of Contents

  1. Core Concepts
  2. Prefab Representation
  3. Simple Prefab Instantiation
  4. Nested Prefab System
  5. The 4-Pass ECS Building Process
  6. FileID Remapping
  7. Code Examples

Core Concepts

What is a Prefab?

In Unity, a prefab is a reusable template that can be instantiated multiple times in scenes. Think of it like a blueprint:

  • A prefab file (.prefab) contains GameObjects and Components as YAML
  • When placed in a scene, Unity creates an instance of that prefab
  • Each instance can have modifications (overrides) applied to it

What is a Nested Prefab?

A nested prefab is a prefab that contains instances of other prefabs within it. For example:

  • Player.prefab might contain a nested Weapon.prefab
  • When you instantiate Player.prefab, it must also instantiate Weapon.prefab
  • This can go multiple levels deep

Key Design Decision: Why Raw YAML?

Prefabs are stored as raw YAML documents (UnityPrefab) rather than fully-parsed ECS worlds (UnityScene). This is because:

  1. Efficient Cloning: Prefabs need to be instantiated multiple times with different values
  2. YAML Overrides: Unity stores modifications as YAML property path overrides (e.g., m_LocalPosition.x = 100)
  3. FileID Remapping: Each instance needs unique FileIDs to avoid collisions

Scenes, on the other hand, are parsed directly into Sparsey ECS worlds since they don't need cloning.


Prefab Representation

UnityPrefab Structure

From unity-parser/src/model/mod.rs:93-146:

pub struct UnityPrefab {
    /// Path to the prefab file
    pub path: PathBuf,

    /// Raw YAML documents that make up this prefab
    pub documents: Vec<RawDocument>,
}

Each RawDocument contains:

  • type_id: Unity type ID (e.g., 1 = GameObject, 4 = Transform)
  • file_id: Unique identifier within the file
  • class_name: The Unity class (e.g., "GameObject", "Transform", "MonoBehaviour")
  • yaml: The raw YAML content as serde_yaml::Value

Loading a Prefab

When you call UnityFile::from_path("Player.prefab"), the parser:

  1. Reads the file content
  2. Validates the Unity YAML header
  3. Splits the YAML into separate documents (by --- !u!N &ID separators)
  4. Creates RawDocument objects with metadata extracted
  5. Returns UnityFile::Prefab(UnityPrefab { path, documents })

Important: At this stage, NO ECS world is created. The prefab stays as raw YAML.


Simple Prefab Instantiation

Step-by-Step Process

Let's walk through instantiating a simple prefab (no nesting):

1. Create a PrefabInstance

From unity-parser/src/types/unity_types/prefab_instance.rs:49-70:

let prefab = UnityFile::from_path("Player.prefab")?;
let mut instance = prefab.instantiate();

instantiate() calls PrefabInstance::new() which:

  • Clones all documents from the source prefab
  • Initializes FileID remapping (creates new unique IDs)
  • Remaps all FileID references in the YAML

2. Apply Overrides (Optional)

You can modify the prefab before spawning:

instance.override_value(file_id, "m_Name", "Player1".into())?;
instance.override_value(file_id, "m_LocalPosition.x", 100.0.into())?;

This stores overrides in a HashMap that will be applied before spawning.

3. Spawn into ECS World

let entities = instance.spawn_into(&mut world, &mut entity_map, guid_resolver, prefab_resolver)?;

spawn_into() (prefab_instance.rs:291-309):

  1. Applies all stored overrides to the YAML
  2. Calls build_world_from_documents_into() to create entities
  3. Returns a Vec of spawned entities

The Spawning Process

build_world_from_documents_into() from unity-parser/src/ecs/builder.rs:160-265:

Pass 1: Create entities for GameObjects

  • Iterates through documents with type_id == 1 (GameObject)
  • Spawns ECS entities with GameObject component
  • Adds to entity_map (FileID → Entity)

Pass 2: Attach components

  • Iterates through remaining documents (Transform, RectTransform, MonoBehaviour, etc.)
  • Looks up m_GameObject reference to find owner entity
  • Parses and attaches component to entity

Pass 3: Execute deferred linking

  • Resolves Transform parent/child relationships
  • Converts FileID references to Entity handles

Nested Prefab System

How Unity Represents Nested Prefabs

When you place a prefab inside another prefab in Unity, it creates a PrefabInstance document:

--- !u!1001 &1234567890
PrefabInstance:
  m_SourcePrefab: {fileID: 0, guid: "091c537484687e9419460cdcd7038234", type: 3}
  m_Modification:
    - target: {fileID: 5678}
      propertyPath: m_Name
      value: "ModifiedName"
    - target: {fileID: 5679}
      propertyPath: m_LocalPosition.x
      value: 10.5

PrefabInstanceComponent

From unity-parser/src/types/unity_types/prefab_instance.rs:322-366:

pub struct PrefabInstanceComponent {
    /// External reference to the source prefab (by GUID)
    pub prefab_ref: ExternalRef,  // Contains GUID string

    /// Modifications applied to the nested prefab
    pub modifications: Vec<PrefabModification>,
}

pub struct PrefabModification {
    pub target_file_id: FileID,      // Which object to modify
    pub property_path: String,        // Dot notation: "m_Name", "m_LocalPosition.x"
    pub value: Value,                 // The new value
}

GUID Resolution

Before we can instantiate a nested prefab, we need to resolve its GUID to a file path.

PrefabGuidResolver (unity-parser/src/parser/prefab_guid_resolver.rs):

  1. Initialization: Scans Unity project directory for .prefab.meta files
  2. GUID Extraction: Parses each .meta file to get the GUID
  3. Mapping: Builds a HashMap: Guid → PathBuf

Example:

Assets/Prefabs/Player.prefab.meta contains:
guid: 091c537484687e9419460cdcd7038234

→ Maps: 0x091c537484687e9419460cdcd7038234 → "Assets/Prefabs/Player.prefab"

PrefabResolver

PrefabResolver (prefab_instance.rs:430-706) handles loading and recursive instantiation:

pub struct PrefabResolver<'a> {
    /// Cache of loaded prefabs (GUID → Prefab)
    prefab_cache: HashMap<String, Arc<UnityPrefab>>,

    /// Mapping from GUID to file path
    guid_to_path: HashMap<String, PathBuf>,

    /// Stack for circular reference detection
    instantiation_stack: Vec<String>,

    /// GUID resolver for MonoBehaviour scripts
    guid_resolver: Option<&'a GuidResolver>,

    /// Prefab GUID resolver
    prefab_guid_resolver: Option<&'a PrefabGuidResolver>,
}

Nested Prefab Instantiation Flow

From prefab_instance.rs:496-572 - instantiate_from_component():

PrefabInstanceComponent found in scene
         ↓
1. Extract GUID from component.prefab_ref
         ↓
2. Load prefab via GUID resolver: GUID → Path → UnityPrefab
         ↓
3. Create PrefabInstance (clone + remap FileIDs)
         ↓
4. Apply modifications from component.modifications
         ↓
5. Spawn prefab into world (creates entities)
         ↓
6. Link spawned root to parent entity (if provided)
         ↓
Returns: Vec<Entity> of spawned entities

Recursive Nested Prefabs

For deeply nested prefabs (prefabs containing prefabs containing prefabs...):

instantiate_recursive() (prefab_instance.rs:574-643):

Start with root prefab
         ↓
1. Check for circular references (using instantiation_stack)
         ↓
2. Push prefab ID to stack
         ↓
3. Create PrefabInstance
         ↓
4. Scan documents for nested PrefabInstance components
         ↓
5. For each nested prefab:
   - Load the referenced prefab by GUID
   - Apply its modifications
   - Recursively call instantiate_recursive()
         ↓
6. Spawn this prefab's entities
         ↓
7. Pop from stack

This handles arbitrary nesting depth while preventing infinite loops from circular references.


The 4-Pass ECS Building Process

When parsing a Unity scene, the ECS builder uses a multi-pass approach to handle prefabs.

From unity-parser/src/ecs/builder.rs:31-138:

Pass 1: Create GameObject Entities

for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
    let entity = spawn_game_object(&mut world, doc)?;
    entity_map.insert(doc.file_id, entity);
}

// Also create entities for PrefabInstances
for doc in documents.iter().filter(|d| d.type_id == 1001 || d.class_name == "PrefabInstance") {
    let entity = world.create(());
    entity_map.insert(doc.file_id, entity);

    // Parse and attach PrefabInstanceComponent
    if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
        world.insert(entity, (prefab_comp,));
    }
}

At this stage:

  • All GameObjects → Entities
  • All PrefabInstances → Entities with PrefabInstanceComponent attached
  • entity_map tracks FileID → Entity

Pass 2: Attach Components

for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
    attach_component(&mut world, doc, &linking_ctx, &type_filter, guid_resolver)?;
}
  • Parses Transform, RectTransform, MonoBehaviour, etc.
  • Looks up m_GameObject reference to find owner entity
  • Attaches parsed component to entity

Pass 2.5: Resolve Prefab Instances (NEW!)

This is where the magic happens for nested prefabs (builder.rs:92-132):

if let Some(prefab_resolver_ref) = prefab_guid_resolver {
    let mut prefab_resolver = PrefabResolver::from_resolvers(guid_resolver, prefab_resolver_ref);

    // Query for entities with PrefabInstanceComponent
    let prefab_entities: Vec<_> = world.query::<&PrefabInstanceComponent>().collect();

    for (entity, component) in prefab_entities {
        // Instantiate the referenced prefab
        match prefab_resolver.instantiate_from_component(
            &component,
            Some(entity),  // Parent entity
            &mut world,
            &mut entity_map,
        ) {
            Ok(spawned) => {
                info!("Spawned {} entities from prefab", spawned.len());
            }
            Err(e) => {
                warn!("Failed to instantiate prefab: {}", e);
            }
        }

        // Remove PrefabInstanceComponent after resolution
        world.remove::<(PrefabInstanceComponent,)>(entity);
    }
}

Key Points:

  1. Only runs if a PrefabGuidResolver is provided
  2. Finds all entities with PrefabInstanceComponent
  3. For each one:
    • Resolves GUID → loads prefab
    • Creates instance with modifications
    • Spawns into current world
    • Links to parent entity
  4. Removes PrefabInstanceComponent (no longer needed)

Pass 3: Execute Deferred Linking

let entity_map = linking_ctx.execute_callbacks(&mut world);
  • Resolves Transform parent/child relationships
  • Converts FileID references to actual Entity handles
  • This happens AFTER prefab instantiation so that prefab entities are in the map

FileID Remapping

Why Remap FileIDs?

Unity FileIDs are unique within a single file, but when instantiating multiple prefab instances, we need to ensure no collisions:

Scene.unity:
  GameObject &100  ← FileID = 100
  Transform &101   ← FileID = 101

Player.prefab (first instance):
  GameObject &100  ← COLLISION!
  Transform &200

Player.prefab (second instance):
  GameObject &100  ← COLLISION!
  Transform &200

The Solution

From prefab_instance.rs:72-114:

Step 1: Generate unique IDs for each document

fn generate_file_id(&mut self) -> FileID {
    let id = self.next_file_id;  // Starts at i64::MAX
    self.next_file_id -= 1;      // Decrement
    FileID::from_i64(id)
}
  • Uses i64::MAX and decrements: 9223372036854775807, 9223372036854775806, ...
  • Scene FileIDs are typically small positive numbers (1, 100, 1000)
  • This avoids collisions

Step 2: Build mapping table

fn initialize_file_id_mapping(&mut self) {
    for original_id in original_ids {
        let new_id = self.generate_file_id();
        self.file_id_map.insert(original_id, new_id);
    }
}

Example mapping:

Original → New
100      → 9223372036854775807
200      → 9223372036854775806

Step 3: Remap all references

fn remap_yaml_file_refs(&mut self) {
    // Update document's own FileID
    for doc in &mut self.documents {
        doc.file_id = self.file_id_map[&doc.file_id];
    }

    // Update all FileRef references in YAML: {fileID: N}
    for doc in &mut self.documents {
        Self::remap_value(&mut doc.yaml, &file_id_map);
    }
}

This recursively walks the YAML tree and replaces all {fileID: 100} with {fileID: 9223372036854775807}.

Handling Overrides

When applying overrides before spawning:

instance.override_value(original_file_id, "m_Name", "Player1".into())?;

The API accepts the original FileID for convenience, but internally:

fn apply_overrides(&mut self) -> Result<()> {
    for ((file_id, path), value) in &self.overrides {
        // Map original FileID → remapped FileID
        let remapped_id = self.file_id_map.get(file_id)?;

        // Find document with remapped ID
        let doc = self.documents.iter_mut()
            .find(|d| d.file_id == *remapped_id)?;

        // Apply value change
        set_yaml_value(&mut doc.yaml, path, value)?;
    }
}

Code Examples

Example 1: Manual Prefab Instantiation

use unity_parser::{UnityFile, UnityPrefab};
use sparsey::World;
use std::collections::HashMap;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load prefab
    let file = UnityFile::from_path("Assets/Prefabs/Player.prefab")?;
    let prefab = match file {
        UnityFile::Prefab(p) => p,
        _ => panic!("Expected prefab"),
    };

    // Create instance with modifications
    let mut instance = prefab.instantiate();
    instance.override_value(file_id, "m_Name", "Player1".into())?;
    instance.override_value(file_id, "m_LocalPosition.x", 100.0.into())?;

    // Spawn into world
    let mut world = World::new();
    let mut entity_map = HashMap::new();
    let entities = instance.spawn_into(&mut world, &mut entity_map, None, None)?;

    println!("Spawned {} entities", entities.len());
    Ok(())
}

Example 2: Automatic Scene Parsing with Nested Prefabs

use unity_parser::UnityProject;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize project (builds GUID resolvers)
    let project = UnityProject::from_path("/home/user/UnityProject")?;

    // Parse scene - automatically resolves and instantiates prefabs
    let scene = project.parse_scene("Assets/Scenes/Level1.unity")?;

    println!("Scene has {} entities", scene.entity_map.len());

    // Query entities
    let game_objects = scene.world.borrow::<unity_parser::GameObject>();
    let transforms = scene.world.borrow::<unity_parser::Transform>();

    for (file_id, entity) in &scene.entity_map {
        if let Some(go) = game_objects.get(*entity) {
            if let Some(transform) = transforms.get(*entity) {
                println!("GameObject '{}' at {:?}",
                    go.name(), transform.local_position());
            }
        }
    }

    Ok(())
}

Example 3: Recursive Prefab Loading

use unity_parser::{UnityFile, PrefabResolver, PrefabGuidResolver};
use sparsey::World;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build prefab GUID resolver
    let prefab_guid_resolver = PrefabGuidResolver::from_project("UnityProject")?;

    // Create prefab resolver
    let mut prefab_resolver = PrefabResolver::from_resolvers(None, &prefab_guid_resolver);

    // Load a prefab with nested prefabs
    let file = UnityFile::from_path("Assets/Prefabs/ComplexPrefab.prefab")?;
    let prefab = match file {
        UnityFile::Prefab(p) => p,
        _ => panic!("Expected prefab"),
    };

    // Recursively instantiate (handles nested prefabs automatically)
    let mut world = World::new();
    let mut entity_map = HashMap::new();
    let entities = prefab_resolver.instantiate_recursive(
        &prefab,
        &mut world,
        &mut entity_map,
    )?;

    println!("Recursively spawned {} entities", entities.len());
    Ok(())
}

Summary

Prefab Instantiation Flow

UnityPrefab (raw YAML)
         ↓
   instantiate()
         ↓
PrefabInstance (cloned YAML with remapped FileIDs)
         ↓
override_value() (optional)
         ↓
   spawn_into()
         ↓
ECS World (Sparsey entities with components)

Nested Prefab Resolution Flow

Scene contains PrefabInstance document
         ↓
Pass 1: Create entity with PrefabInstanceComponent
         ↓
Pass 2.5: Find all PrefabInstanceComponent entities
         ↓
For each: GUID → Path → Load Prefab
         ↓
Create instance + apply modifications
         ↓
Recursively check for nested PrefabInstances
         ↓
Spawn all entities into world
         ↓
Link to parent entity

Key Takeaways

  1. Prefabs stay as YAML until instantiation for efficient cloning and overrides
  2. FileID remapping prevents collisions when instantiating multiple times
  3. PrefabGuidResolver maps GUIDs to file paths for automatic loading
  4. Pass 2.5 in the ECS builder handles automatic prefab instantiation
  5. Recursive instantiation handles arbitrary nesting depth with circular reference detection
  6. Modifications are stored as property path + value pairs and applied before spawning

Files to Explore

  • unity-parser/src/types/unity_types/prefab_instance.rs - PrefabInstance, PrefabResolver
  • unity-parser/src/parser/prefab_guid_resolver.rs - GUID → Path mapping
  • unity-parser/src/ecs/builder.rs - 4-pass ECS building with prefab resolution
  • unity-parser/src/model/mod.rs - UnityPrefab, UnityScene data structures