project load first
This commit is contained in:
627
PREFAB_SUMMARY.md
Normal file
627
PREFAB_SUMMARY.md
Normal file
@@ -0,0 +1,627 @@
|
||||
# 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
|
||||
@@ -9,12 +9,12 @@
|
||||
mod types;
|
||||
|
||||
use types::InteractableResource;
|
||||
use unity_parser::UnityFile;
|
||||
use unity_parser::UnityProject;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use unity_parser::log::DedupLogger;
|
||||
use log::{info, warn, error, LevelFilter};
|
||||
use log::{info, warn, error, LevelFilter};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
@@ -22,23 +22,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
log::set_boxed_logger(Box::new(logger))
|
||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
||||
.unwrap();
|
||||
log::set_max_level(LevelFilter::Warn);
|
||||
// log::set_max_level(LevelFilter::Warn);
|
||||
|
||||
info!("🎮 Cursebreaker - Resource Parser");
|
||||
|
||||
let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity");
|
||||
// Initialize Unity project once - scans entire project for GUID mappings
|
||||
let project_root = Path::new("/home/connor/repos/CBAssets");
|
||||
info!("📦 Initializing Unity project from: {}", project_root.display());
|
||||
|
||||
// Check if scene exists
|
||||
if !scene_path.exists() {
|
||||
error!("Scene not found at {}", scene_path.display());
|
||||
return Err("Scene file not found".into());
|
||||
}
|
||||
let project = UnityProject::from_path(project_root)?;
|
||||
|
||||
info!("📁 Parsing scene: {}", scene_path.display());
|
||||
// Now parse the scene using the pre-built GUID resolvers
|
||||
let scene_path = "_GameAssets/Scenes/Tiles/10_3.unity";
|
||||
info!("📁 Parsing scene: {}", scene_path);
|
||||
|
||||
// Parse the scene
|
||||
match UnityFile::from_path(&scene_path) {
|
||||
Ok(UnityFile::Scene(scene)) => {
|
||||
// Parse the scene using the project
|
||||
match project.parse_scene(scene_path) {
|
||||
Ok(scene) => {
|
||||
info!("✅ Scene parsed successfully!");
|
||||
info!(" Total entities: {}", scene.entity_map.len());
|
||||
|
||||
@@ -114,10 +114,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
info!("✅ Parsing complete!");
|
||||
}
|
||||
Ok(_) => {
|
||||
error!("File is not a scene");
|
||||
return Err("Not a Unity scene file".into());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Parse error: {}", e);
|
||||
return Err(Box::new(e));
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
Cursebreaker Resources - 10_3.unity Scene
|
||||
======================================================================
|
||||
|
||||
Total resources found: 2
|
||||
Total resources found: 1
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Resource: HarvestableSpawner_11Redberries
|
||||
TypeID: 11
|
||||
MaxHealth: 0
|
||||
Position: (1769.135864, 32.664658, 150.395081)
|
||||
|
||||
Resource: HarvestableSpawner_38Dandelions
|
||||
TypeID: 38
|
||||
MaxHealth: 0
|
||||
|
||||
@@ -38,13 +38,11 @@ pub mod types;
|
||||
|
||||
// Re-exports
|
||||
pub use error::{Error, Result};
|
||||
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene, UnityProject};
|
||||
pub use parser::{
|
||||
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered,
|
||||
GuidResolver, PrefabGuidResolver,
|
||||
};
|
||||
// TODO: Re-enable once project module is updated
|
||||
// pub use project::UnityProject;
|
||||
pub use property::PropertyValue;
|
||||
pub use types::{
|
||||
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable,
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
//! Unity files can be Scenes (.unity), Prefabs (.prefab), or Assets (.asset), each
|
||||
//! with different handling requirements.
|
||||
|
||||
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
||||
use crate::types::FileID;
|
||||
use log::info;
|
||||
use sparsey::{Entity, World};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A parsed Unity file - can be a Scene, Prefab, or Asset
|
||||
#[derive(Debug)]
|
||||
@@ -210,3 +212,132 @@ impl RawDocument {
|
||||
self.yaml.as_mapping()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Unity project with pre-built GUID resolvers for efficient scene parsing
|
||||
///
|
||||
/// This struct holds pre-initialized GUID mappings for both MonoBehaviour scripts
|
||||
/// and prefab files. By building these mappings once at initialization, you can
|
||||
/// parse multiple scenes efficiently without rescanning the project for each file.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::UnityProject;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// // Initialize project once - scans entire project for .meta files
|
||||
/// let project = UnityProject::from_path("/home/user/repos/CBAssets")?;
|
||||
///
|
||||
/// // Parse multiple scenes efficiently using pre-built resolvers
|
||||
/// let scene1 = project.parse_scene("_GameAssets/Scenes/Tiles/10_3.unity")?;
|
||||
/// let scene2 = project.parse_scene("_GameAssets/Scenes/Tiles/11_5.unity")?;
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnityProject {
|
||||
/// Project root path
|
||||
pub root: PathBuf,
|
||||
|
||||
/// Script GUID resolver (MonoBehaviour scripts)
|
||||
pub guid_resolver: GuidResolver,
|
||||
|
||||
/// Prefab GUID resolver (Prefab files)
|
||||
pub prefab_resolver: PrefabGuidResolver,
|
||||
}
|
||||
|
||||
impl UnityProject {
|
||||
/// Create a new UnityProject by scanning the entire project directory
|
||||
///
|
||||
/// This scans ALL subdirectories under the project root for .meta files
|
||||
/// to build comprehensive GUID mappings for both scripts and prefabs.
|
||||
///
|
||||
/// Unlike the individual resolver constructors which only scan Assets/ or
|
||||
/// _GameAssets/, this scans the entire project root to find assets in
|
||||
/// directories like AssetDumpster/, AssetPacks/, etc.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `root` - Path to the Unity project root directory
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::UnityProject;
|
||||
///
|
||||
/// let project = UnityProject::from_path("/home/user/repos/CBAssets")?;
|
||||
/// println!("Found {} script GUIDs", project.guid_resolver.len());
|
||||
/// println!("Found {} prefab GUIDs", project.prefab_resolver.len());
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_path(root: impl Into<PathBuf>) -> crate::Result<Self> {
|
||||
let root = root.into();
|
||||
|
||||
info!("🔍 Initializing Unity project from: {}", root.display());
|
||||
|
||||
// Build script GUID resolver by scanning entire project root
|
||||
let guid_resolver = GuidResolver::from_project_root(&root)?;
|
||||
info!(" ✓ Script GUID resolver built ({} mappings)", guid_resolver.len());
|
||||
|
||||
// Build prefab GUID resolver by scanning entire project root
|
||||
let prefab_resolver = PrefabGuidResolver::from_project_root(&root)?;
|
||||
info!(" ✓ Prefab GUID resolver built ({} mappings)", prefab_resolver.len());
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
guid_resolver,
|
||||
prefab_resolver,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a Unity scene using the pre-built GUID resolvers
|
||||
///
|
||||
/// This is more efficient than `UnityFile::from_path` when parsing multiple
|
||||
/// scenes because the GUID resolvers are already initialized.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the scene file (relative to project root or absolute)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use unity_parser::UnityProject;
|
||||
/// # let project = UnityProject::from_path("/home/user/repos/CBAssets")?;
|
||||
/// let scene = project.parse_scene("_GameAssets/Scenes/Tiles/10_3.unity")?;
|
||||
/// println!("Scene has {} entities", scene.entity_map.len());
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn parse_scene(&self, path: impl AsRef<Path>) -> crate::Result<UnityScene> {
|
||||
let path = self.resolve_path(path.as_ref());
|
||||
crate::parser::parse_scene_with_project(&path, self)
|
||||
}
|
||||
|
||||
/// Parse a Unity prefab using the pre-built GUID resolvers
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the prefab file (relative to project root or absolute)
|
||||
pub fn parse_prefab(&self, path: impl AsRef<Path>) -> crate::Result<UnityPrefab> {
|
||||
let path = self.resolve_path(path.as_ref());
|
||||
crate::parser::parse_prefab_with_project(&path, self)
|
||||
}
|
||||
|
||||
/// Parse a Unity asset file using the pre-built GUID resolvers
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the asset file (relative to project root or absolute)
|
||||
pub fn parse_asset(&self, path: impl AsRef<Path>) -> crate::Result<UnityAsset> {
|
||||
let path = self.resolve_path(path.as_ref());
|
||||
crate::parser::parse_asset_with_project(&path, self)
|
||||
}
|
||||
|
||||
/// Resolve a path (if relative, make it relative to project root)
|
||||
fn resolve_path(&self, path: &Path) -> PathBuf {
|
||||
if path.is_absolute() {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
self.root.join(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ impl GuidResolver {
|
||||
/// parses them to extract GUIDs, then parses the corresponding
|
||||
/// C# files to extract class names.
|
||||
///
|
||||
/// **Note**: This only scans the Assets/ or _GameAssets/ directory.
|
||||
/// For scanning the entire project root, use `from_project_root`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||
@@ -94,10 +97,49 @@ impl GuidResolver {
|
||||
)));
|
||||
};
|
||||
|
||||
Self::scan_directory(&assets_dir)
|
||||
}
|
||||
|
||||
/// Build a GuidResolver by scanning the entire project root
|
||||
///
|
||||
/// Unlike `from_project` which only scans Assets/ or _GameAssets/,
|
||||
/// this scans ALL subdirectories to find .cs.meta files. This is useful
|
||||
/// when assets are spread across multiple directories (e.g., AssetDumpster/,
|
||||
/// AssetPacks/, etc.).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_root` - Path to the Unity project root directory
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::parser::GuidResolver;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// // Scans entire project root including AssetDumpster/, AssetPacks/, etc.
|
||||
/// let resolver = GuidResolver::from_project_root(Path::new("/home/user/repos/CBAssets"))?;
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_project_root(project_root: impl AsRef<Path>) -> Result<Self> {
|
||||
let project_root = project_root.as_ref();
|
||||
|
||||
if !project_root.is_dir() {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"Project root is not a directory: {}",
|
||||
project_root.display()
|
||||
)));
|
||||
}
|
||||
|
||||
Self::scan_directory(project_root)
|
||||
}
|
||||
|
||||
/// Internal method to scan a directory for .cs.meta files
|
||||
fn scan_directory(dir: &Path) -> Result<Self> {
|
||||
let mut resolver = Self::new();
|
||||
|
||||
// Walk the Assets directory looking for .cs.meta files
|
||||
for entry in WalkDir::new(&assets_dir)
|
||||
// Walk the directory looking for .cs.meta files
|
||||
for entry in WalkDir::new(dir)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
|
||||
@@ -14,7 +14,7 @@ pub use yaml::split_yaml_documents;
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene, UnityProject};
|
||||
use crate::types::{FileID, Guid, TypeFilter};
|
||||
use crate::{Error, Result};
|
||||
use regex::Regex;
|
||||
@@ -198,6 +198,83 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) ->
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse a scene file using pre-built GUID resolvers from a UnityProject
|
||||
///
|
||||
/// This is more efficient than `parse_scene` when parsing multiple scenes
|
||||
/// because the GUID resolvers are already initialized.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the scene file
|
||||
/// * `project` - Pre-initialized UnityProject with GUID resolvers
|
||||
pub fn parse_scene_with_project(path: &Path, project: &UnityProject) -> Result<UnityScene> {
|
||||
// Read the file
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
// Validate Unity header
|
||||
validate_unity_header(&content, path)?;
|
||||
|
||||
// Parse raw documents
|
||||
let raw_documents = parse_raw_documents(&content, None)?;
|
||||
|
||||
// Build ECS world from documents using project's resolvers
|
||||
let (world, entity_map) = crate::ecs::build_world_from_documents(
|
||||
raw_documents,
|
||||
Some(&project.guid_resolver),
|
||||
Some(&project.prefab_resolver),
|
||||
)?;
|
||||
|
||||
Ok(UnityScene::new(
|
||||
path.to_path_buf(),
|
||||
world,
|
||||
entity_map,
|
||||
))
|
||||
}
|
||||
|
||||
/// Parse a prefab file using pre-built GUID resolvers from a UnityProject
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the prefab file
|
||||
/// * `project` - Pre-initialized UnityProject with GUID resolvers
|
||||
pub fn parse_prefab_with_project(path: &Path, _project: &UnityProject) -> Result<UnityPrefab> {
|
||||
// Read the file
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
// Validate Unity header
|
||||
validate_unity_header(&content, path)?;
|
||||
|
||||
// Parse raw documents
|
||||
let raw_documents = parse_raw_documents(&content, None)?;
|
||||
|
||||
Ok(UnityPrefab::new(
|
||||
path.to_path_buf(),
|
||||
raw_documents,
|
||||
))
|
||||
}
|
||||
|
||||
/// Parse an asset file using pre-built GUID resolvers from a UnityProject
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path to the asset file
|
||||
/// * `project` - Pre-initialized UnityProject with GUID resolvers
|
||||
pub fn parse_asset_with_project(path: &Path, _project: &UnityProject) -> Result<UnityAsset> {
|
||||
// Read the file
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
|
||||
// Validate Unity header
|
||||
validate_unity_header(&content, path)?;
|
||||
|
||||
// Parse raw documents
|
||||
let raw_documents = parse_raw_documents(&content, None)?;
|
||||
|
||||
Ok(UnityAsset::new(
|
||||
path.to_path_buf(),
|
||||
raw_documents,
|
||||
))
|
||||
}
|
||||
|
||||
/// Parse a prefab file into raw YAML documents
|
||||
fn parse_prefab(path: &Path, content: &str, type_filter: Option<&TypeFilter>) -> Result<UnityFile> {
|
||||
let raw_documents = parse_raw_documents(content, type_filter)?;
|
||||
|
||||
@@ -62,6 +62,9 @@ impl PrefabGuidResolver {
|
||||
/// This scans for all `.prefab.meta` files and extracts their GUIDs
|
||||
/// to build a GUID → Prefab Path mapping.
|
||||
///
|
||||
/// **Note**: This only scans the Assets/ or _GameAssets/ directory.
|
||||
/// For scanning the entire project root, use `from_project_root`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||
@@ -90,10 +93,49 @@ impl PrefabGuidResolver {
|
||||
)));
|
||||
};
|
||||
|
||||
Self::scan_directory(&assets_dir)
|
||||
}
|
||||
|
||||
/// Build a PrefabGuidResolver by scanning the entire project root
|
||||
///
|
||||
/// Unlike `from_project` which only scans Assets/ or _GameAssets/,
|
||||
/// this scans ALL subdirectories to find .prefab.meta files. This is useful
|
||||
/// when prefabs are spread across multiple directories (e.g., AssetDumpster/,
|
||||
/// AssetPacks/, etc.).
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `project_root` - Path to the Unity project root directory
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use unity_parser::parser::PrefabGuidResolver;
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// // Scans entire project root including AssetDumpster/, AssetPacks/, etc.
|
||||
/// let resolver = PrefabGuidResolver::from_project_root(Path::new("/home/user/repos/CBAssets"))?;
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_project_root(project_root: impl AsRef<Path>) -> Result<Self> {
|
||||
let project_root = project_root.as_ref();
|
||||
|
||||
if !project_root.is_dir() {
|
||||
return Err(Error::invalid_format(format!(
|
||||
"Project root is not a directory: {}",
|
||||
project_root.display()
|
||||
)));
|
||||
}
|
||||
|
||||
Self::scan_directory(project_root)
|
||||
}
|
||||
|
||||
/// Internal method to scan a directory for .prefab.meta files
|
||||
fn scan_directory(dir: &Path) -> Result<Self> {
|
||||
let mut resolver = Self::new();
|
||||
|
||||
// Walk the Assets directory looking for .prefab.meta files
|
||||
for entry in WalkDir::new(&assets_dir)
|
||||
// Walk the directory looking for .prefab.meta files
|
||||
for entry in WalkDir::new(dir)
|
||||
.follow_links(false)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
|
||||
Reference in New Issue
Block a user