18 KiB
ROADMAP: GUID Resolution for MonoBehaviour Components
Executive Summary
The #[derive(UnityComponent)] macro system is fully functional for parsing custom Unity components. However, Unity stores custom MonoBehaviour scripts in scene/prefab files using a generic MonoBehaviour class name with a GUID reference to the actual script. To enable automatic discovery and parsing of custom components like PlaySFX from real Unity projects, we need to implement GUID → Class Name resolution.
Current Status ✅
What's Working
-
Procedural Macro System
#[derive(UnityComponent)]generates parsing code automatically- Supports all Unity primitive types (f64, String, bool, Vector3, etc.)
- Generates
UnityComponent::parse()implementation - Generates
EcsInsertable::insert_into_world()implementation
-
Auto-Registration via Inventory
- Components automatically register themselves at compile time
build_world_from_documents()discovers and registers all custom components- No manual modification of builder.rs needed
-
Type Filtering System
TypeFilterallows selective parsing for performanceparse_with_types!macro for ergonomic type selection- Works with both Unity types and custom types
-
ECS Integration
- Custom components insert into Sparsey ECS world
- Components can be queried via
world.borrow::<PlaySFX>() - Full integration with existing Transform, GameObject, etc.
What's Missing
MonoBehaviour GUID Resolution: Unity scene/prefab files store custom scripts like this:
--- !u!114 &1234567
MonoBehaviour: # ⚠️ Generic class name, not "PlaySFX"
m_GameObject: {fileID: 890}
m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3} # 👈 Actual type
volume: 1.0
startTime: 0.0
endTime: 5.0
isLoop: 0
Problem: When parsing, we see class_name = "MonoBehaviour", so we can't match it to the PlaySFX component registration.
Solution: Resolve the m_Script GUID to discover the actual class name "PlaySFX".
The GUID Resolution Feature
How Unity GUID Resolution Works
-
Every asset has a .meta file alongside it with a unique GUID:
Assets/Scripts/PlaySFX.cs # The actual C# script Assets/Scripts/PlaySFX.cs.meta # Contains GUID -
The .meta file contains the GUID:
fileFormatVersion: 2 guid: 091c537484687e9419460cdcd7038234 # 👈 This is the GUID MonoImporter: serializedVersion: 2 defaultReferences: [] executionOrder: 0 -
MonoBehaviour references the GUID:
m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3} -
We need to map:
GUID → Script Path → Class Name091c537484687e9419460cdcd7038234→Assets/Scripts/PlaySFX.cs→"PlaySFX"
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ Unity Project Parsing │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Scan for .meta files (*.cs.meta) │
│ Build GUID → Asset Path mapping │
│ Parse class name from .cs files │
│ Result: GuidResolver { guid → class_name } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Parse Unity files (.unity, .prefab) │
│ When encountering MonoBehaviour: │
│ - Extract m_Script.guid │
│ - Lookup guid in GuidResolver │
│ - Get actual class_name (e.g., "PlaySFX") │
│ - Match with component registry │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Component Registration (via inventory) │
│ Check if class_name matches registered component │
│ If match found: │
│ - Call component.parse_and_insert() │
│ - Insert into ECS world │
└─────────────────────────────────────────────────────────────┘
Implementation Plan
Phase 1: GUID → Asset Path Resolution
Goal: Build a mapping from GUID to file path by scanning .meta files
New Module: src/parser/guid_resolver.rs
/// Resolves Unity GUIDs to asset paths
pub struct GuidResolver {
/// Map from GUID string to asset file path
guid_to_path: HashMap<String, PathBuf>,
}
impl GuidResolver {
/// Scan a Unity project directory for .meta files
pub fn from_project(project_path: &Path) -> Result<Self> {
// 1. Find all *.meta files (recursively)
// 2. Parse each .meta file to extract GUID
// 3. Derive asset path from .meta path (remove .meta extension)
// 4. Build HashMap
}
/// Look up a GUID to get the asset path
pub fn resolve(&self, guid: &str) -> Option<&Path> {
self.guid_to_path.get(guid).map(|p| p.as_path())
}
}
Files to Create/Modify:
- 📄
src/parser/guid_resolver.rs- New file - 📄
src/parser/mod.rs- Export GuidResolver - 📄
src/lib.rs- Re-export GuidResolver
Implementation Details:
- Recursively walk project directory
- Filter for
*.cs.metafiles (MonoBehaviour scripts) - Parse YAML to extract
guid:field - Store
guid → "Assets/Scripts/PlaySFX.cs"mapping
Phase 2: Class Name Extraction from C# Files
Goal: Parse .cs files to extract the class name
Approach: Simple regex/string parsing (no full C# parser needed)
impl GuidResolver {
/// Extract class name from a C# script file
fn extract_class_name(cs_path: &Path) -> Result<String> {
let content = std::fs::read_to_string(cs_path)?;
// Look for: "public class PlaySFX" or "class PlaySFX"
// Regex: r"(?:public\s+)?class\s+(\w+)"
// Return the captured class name
}
/// Resolve GUID to class name
pub fn resolve_class_name(&self, guid: &str) -> Option<String> {
let path = self.resolve(guid)?;
Self::extract_class_name(path).ok()
}
}
Files to Modify:
- 📄
src/parser/guid_resolver.rs- Add class name extraction - 📄
Cargo.toml- Addregexdependency (optional, can use manual parsing)
Phase 3: MonoBehaviour Parser Enhancement
Goal: Extract m_Script GUID when parsing MonoBehaviour components
Current MonoBehaviour Parsing (builder.rs line 206):
_ => {
// Check if this is a registered custom component
for reg in inventory::iter::<crate::types::ComponentRegistration> {
if reg.class_name == doc.class_name.as_str() { // ⚠️ Won't match "MonoBehaviour"
// ...
}
}
}
Enhanced MonoBehaviour Parsing:
"MonoBehaviour" => {
// Extract m_Script GUID
if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") {
let guid = script_ref.guid();
// Resolve GUID to class name (requires access to GuidResolver)
if let Some(class_name) = guid_resolver.resolve_class_name(guid) {
// Now match against registered components
for reg in inventory::iter::<crate::types::ComponentRegistration> {
if reg.class_name == class_name.as_str() {
if (reg.parse_and_insert)(yaml, &ctx, world, entity) {
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
}
return Ok(());
}
}
}
}
// Fall back to generic MonoBehaviour warning
eprintln!("Warning: Skipping unknown MonoBehaviour");
}
Files to Modify:
- 📄
src/ecs/builder.rs- Add MonoBehaviour case, pass GuidResolver - 📄
src/types/references.rs- Ensure ExternalRef hasguid()method - 📄
src/types/component.rs- Addget_external_refto yaml_helpers (if not exists)
Challenge: How to pass GuidResolver to build_world_from_documents()?
Option A: Add parameter (breaking change)
pub fn build_world_from_documents(
documents: Vec<RawDocument>,
guid_resolver: Option<&GuidResolver>, // 👈 New parameter
) -> Result<(World, HashMap<FileID, Entity>)>
Option B: Store in ComponentContext (preferred)
pub struct ComponentContext<'a> {
pub type_id: u32,
pub file_id: FileID,
pub class_name: &'a str,
pub entity: Option<Entity>,
pub linking_ctx: Option<&'a RefCell<LinkingContext>>,
pub yaml: &'a Mapping,
pub guid_resolver: Option<&'a GuidResolver>, // 👈 New field
}
Phase 4: Integration with UnityFile Parsing
Goal: Build GuidResolver when loading a Unity project
Current Flow (UnityFile::from_path):
pub fn from_path(path: &Path) -> Result<UnityFile> {
let content = std::fs::read_to_string(path)?;
let documents = parse_unity_file(&content)?;
match file_type {
Scene => {
let (world, entity_map) = build_world_from_documents(documents)?;
// ...
}
}
}
Enhanced Flow:
pub fn from_path(path: &Path) -> Result<UnityFile> {
// 1. Detect if this is part of a Unity project
let project_root = find_project_root(path)?;
// 2. Build GUID resolver for the project
let guid_resolver = GuidResolver::from_project(&project_root)?;
// 3. Parse file with GUID resolution
let content = std::fs::read_to_string(path)?;
let documents = parse_unity_file(&content)?;
match file_type {
Scene => {
let (world, entity_map) = build_world_from_documents(
documents,
Some(&guid_resolver) // 👈 Pass resolver
)?;
// ...
}
}
}
Files to Modify:
- 📄
src/model.rs- UpdateUnityFile::from_path() - 📄
src/parser/guid_resolver.rs- Addfind_project_root()helper
Optimization: Cache GuidResolver per project (expensive to rebuild)
// Thread-local cache for performance
thread_local! {
static GUID_CACHE: RefCell<HashMap<PathBuf, Arc<GuidResolver>>> = RefCell::new(HashMap::new());
}
Phase 5: Update Examples and Tests
Examples to Update:
- 📄
examples/find_playsfx.rs- Should now find PlaySFX components! - 📄
examples/ecs_integration.rs- Add example with GUID resolution
Tests to Add:
- 📄
tests/guid_resolution_tests.rs- New test file- Test GuidResolver can scan project
- Test GUID → path resolution
- Test class name extraction
- Integration test with VR_Horror project
Integration Test:
#[test]
fn test_guid_resolution_vr_horror() {
let project_path = Path::new("test_data/VR_Horror_YouCantRun");
// Build resolver
let resolver = GuidResolver::from_project(project_path).unwrap();
// Known PlaySFX GUID
let playsfx_guid = "091c537484687e9419460cdcd7038234";
// Should resolve to class name
assert_eq!(
resolver.resolve_class_name(playsfx_guid),
Some("PlaySFX".to_string())
);
// Now parse a scene with PlaySFX
let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity");
let file = UnityFile::from_path(&scene_path).unwrap();
if let UnityFile::Scene(scene) = file {
// Should find PlaySFX components
let playsfx_view = scene.world.borrow::<PlaySFX>();
let mut count = 0;
for entity in scene.entity_map.values() {
if playsfx_view.get(*entity).is_some() {
count += 1;
}
}
assert!(count > 0, "Should find at least one PlaySFX component");
}
}
File Checklist
New Files to Create
src/parser/guid_resolver.rs- GUID resolution logictests/guid_resolution_tests.rs- Unit and integration tests
Existing Files to Modify
src/parser/mod.rs- Export GuidResolversrc/lib.rs- Re-export GuidResolversrc/ecs/builder.rs- Add MonoBehaviour GUID resolutionsrc/model.rs- Update UnityFile::from_path()src/types/component.rs- Add guid_resolver to ComponentContextsrc/types/references.rs- Ensure ExternalRef has guid() methodCargo.toml- Add regex dependency (optional)examples/find_playsfx.rs- Update with notes/verificationexamples/ecs_integration.rs- Add GUID resolution example
Technical Challenges & Solutions
Challenge 1: Performance - Scanning Large Projects
Problem: Scanning thousands of .meta files on every parse is slow
Solutions:
- Lazy Loading: Only scan when needed (first MonoBehaviour encountered)
- Caching: Store GuidResolver per project path in thread-local cache
- Incremental: Only scan Assets/ directory, skip Library/Temp
- Parallel: Use rayon to scan .meta files in parallel
Recommendation: Start with simple implementation, optimize if needed
Challenge 2: Class Name Extraction Complexity
Problem: C# parsing can be complex (namespaces, partial classes, etc.)
Solutions:
- Simple Regex:
r"(?:public\s+)?class\s+(\w+)(?:\s*:\s*MonoBehaviour)?"- Good enough for 95% of cases
- Fast and simple
- Full C# Parser: Use tree-sitter or similar
- Overkill for this use case
- Adds heavy dependency
Recommendation: Start with regex, handle edge cases as discovered
Challenge 3: Breaking API Changes
Problem: Adding GuidResolver parameter changes public API
Solutions:
- Option A: Make it optional with
Option<&GuidResolver>- Backward compatible
- GUIDs won't resolve if not provided
- Option B: Add separate method
from_path_with_resolver()- Keep original API unchanged
- Explicit opt-in to GUID resolution
- Option C: Detect and auto-create GuidResolver
- Best user experience
- Always attempt GUID resolution
- Fall back gracefully if project not detected
Recommendation: Option C - Auto-detect and resolve
Challenge 4: Multiple Scripts Per File
Problem: C# files can have multiple classes
Example:
public class PlaySFX : MonoBehaviour { }
public class SFXHelper { } // Same file!
Solution: The .meta file is associated with the file, not the class. Unity creates separate .meta files for each public MonoBehaviour. Non-MonoBehaviour helper classes in the same file aren't referenced by GUID.
Recommendation: Extract first public class that inherits from MonoBehaviour
Success Criteria
When this feature is complete:
✅ Functionality:
- GuidResolver can scan a Unity project and build GUID mappings
- MonoBehaviour components resolve to their actual class names
- Custom components like PlaySFX are discovered and parsed automatically
examples/find_playsfx.rsfinds PlaySFX components in VR_Horror project
✅ Performance:
- GUID resolution adds < 500ms overhead for typical projects
- Caching prevents redundant scanning
✅ Testing:
- Unit tests for GUID resolution
- Integration test finds PlaySFX in VR_Horror
- Edge cases handled (missing .meta, invalid GUIDs, etc.)
✅ Documentation:
- GUID resolution documented in README
- Examples show usage with real Unity projects
- API docs explain GuidResolver usage
Timeline Estimate
- Phase 1 (GUID → Path): 2-4 hours
- Phase 2 (Class Name Extraction): 1-2 hours
- Phase 3 (MonoBehaviour Parser): 2-3 hours
- Phase 4 (Integration): 2-3 hours
- Phase 5 (Tests & Examples): 2-3 hours
Total: 9-15 hours of development time
Future Enhancements (Out of Scope)
These can be added later if needed:
- Prefab GUID Resolution: Resolve nested prefab references
- AssetDatabase: Full asset path resolution (materials, textures, etc.)
- GUID Cache File: Persist GUID mappings to disk for instant loading
- Watch Mode: Auto-update GUID mappings when .meta files change
- Cross-Platform Paths: Handle Windows/Mac/Linux path differences
Related Documentation
- Unity YAML Format: UnityYAMLParser
- Unity .meta Files: Unity Manual - Meta Files
- GUID Format: RFC 4122 compliant UUIDs without hyphens
Status: 📋 Planning Complete - Ready for Implementation
Last Updated: 2026-01-02