628 lines
18 KiB
Markdown
628 lines
18 KiB
Markdown
# Prefab Instantiation Deep Dive
|
|
|
|
This document explains how prefabs and nested prefabs are instantiated in the Unity Parser.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Core Concepts](#core-concepts)
|
|
2. [Prefab Representation](#prefab-representation)
|
|
3. [Simple Prefab Instantiation](#simple-prefab-instantiation)
|
|
4. [Nested Prefab System](#nested-prefab-system)
|
|
5. [The 4-Pass ECS Building Process](#the-4-pass-ecs-building-process)
|
|
6. [FileID Remapping](#fileid-remapping)
|
|
7. [Code Examples](#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`:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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:
|
|
|
|
```yaml
|
|
--- !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`:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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`):
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
instance.override_value(original_file_id, "m_Name", "Player1".into())?;
|
|
```
|
|
|
|
The API accepts the **original** FileID for convenience, but internally:
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|