18 KiB
Prefab Instantiation Deep Dive
This document explains how prefabs and nested prefabs are instantiated in the Unity Parser.
Table of Contents
- Core Concepts
- Prefab Representation
- Simple Prefab Instantiation
- Nested Prefab System
- The 4-Pass ECS Building Process
- FileID Remapping
- 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.prefabmight contain a nestedWeapon.prefab- When you instantiate
Player.prefab, it must also instantiateWeapon.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:
- Efficient Cloning: Prefabs need to be instantiated multiple times with different values
- YAML Overrides: Unity stores modifications as YAML property path overrides (e.g.,
m_LocalPosition.x = 100) - 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 fileclass_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:
- Reads the file content
- Validates the Unity YAML header
- Splits the YAML into separate documents (by
--- !u!N &IDseparators) - Creates
RawDocumentobjects with metadata extracted - 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):
- Applies all stored overrides to the YAML
- Calls
build_world_from_documents_into()to create entities - 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
GameObjectcomponent - Adds to entity_map (FileID → Entity)
Pass 2: Attach components
- Iterates through remaining documents (Transform, RectTransform, MonoBehaviour, etc.)
- Looks up
m_GameObjectreference 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):
- Initialization: Scans Unity project directory for
.prefab.metafiles - GUID Extraction: Parses each
.metafile to get the GUID - 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
PrefabInstanceComponentattached - 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_GameObjectreference 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:
- Only runs if a PrefabGuidResolver is provided
- Finds all entities with
PrefabInstanceComponent - For each one:
- Resolves GUID → loads prefab
- Creates instance with modifications
- Spawns into current world
- Links to parent entity
- 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
- Prefabs stay as YAML until instantiation for efficient cloning and overrides
- FileID remapping prevents collisions when instantiating multiple times
- PrefabGuidResolver maps GUIDs to file paths for automatic loading
- Pass 2.5 in the ECS builder handles automatic prefab instantiation
- Recursive instantiation handles arbitrary nesting depth with circular reference detection
- 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, PrefabResolverunity-parser/src/parser/prefab_guid_resolver.rs- GUID → Path mappingunity-parser/src/ecs/builder.rs- 4-pass ECS building with prefab resolutionunity-parser/src/model/mod.rs- UnityPrefab, UnityScene data structures