using UnityEngine; using UnityEditor; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; /// /// Unity Editor Tool for transferring components, settings, and child GameObjects /// from one character prefab to another for combat/fighting games. /// Uses EditorApplication.update coroutine pattern to prevent editor freezing. /// public class CharacterPrefabTransferTool : EditorWindow { #region Fields // Prefab references private GameObject sourcePrefab; private GameObject targetPrefab; // Scan results private PrefabScanResult scanResult; private bool hasScanned = false; // UI State private Vector2 scrollPosition; private Vector2 logScrollPosition; private List logEntries = new List(); // Foldout states private bool showRootComponents = true; private bool showChildObjects = true; private bool showColliders = true; private bool showAnimator = true; private bool showCamera = true; private bool showEffects = true; private bool showRigidbody = true; private bool showLogs = true; // Copy options (checkboxes) private bool copyRootComponents = true; private bool copyChildObjects = true; private bool copyColliders = true; private bool copyAnimator = true; private bool copyCamera = true; private bool copyEffects = true; private bool copyRigidbody = true; private bool copyMaterials = true; private bool copyLayersAndTags = true; // Bone matching private bool matchBonesByName = true; private Dictionary boneMapping = new Dictionary(); // Progress - using EditorApplication.update coroutine pattern private float progress = 0f; private string progressMessage = ""; private bool isProcessing = false; private IEnumerator activeCoroutine = null; private bool wasCancelled = false; // Bone ratio data for smart proportional scaling private Dictionary limbRatios = new Dictionary(); private float overallScaleRatio = 1f; private bool boneRatiosCalculated = false; // Styles private GUIStyle headerStyle; private GUIStyle successStyle; private GUIStyle warningStyle; private GUIStyle errorStyle; private GUIStyle boxStyle; private bool stylesInitialized = false; #endregion #region Data Classes [Serializable] public class PrefabScanResult { public List rootComponents = new List(); public List childObjects = new List(); public List colliders = new List(); public AnimatorInfo animatorInfo; public CameraInfo cameraInfo; public RigidbodyInfo rigidbodyInfo; public List effects = new List(); public int totalComponentCount; public int totalChildCount; } [Serializable] public class ComponentInfo { public string typeName; public string fullTypeName; public bool isEnabled; public List serializedFields = new List(); public bool shouldCopy = true; } [Serializable] public class FieldValueInfo { public string fieldName; public string fieldType; public string valuePreview; public bool isReference; } [Serializable] public class ChildObjectInfo { public string name; public string path; public string tag; public int layer; public Vector3 localPosition; public Quaternion localRotation; public Vector3 localScale; public List components = new List(); public List children = new List(); public bool shouldCopy = true; public string parentBoneName; } [Serializable] public class ColliderInfo { public string gameObjectName; public string gameObjectPath; public string colliderType; public bool isTrigger; public Vector3 center; public Vector3 size; // For BoxCollider public float radius; // For Sphere/Capsule public float height; // For Capsule public int direction; // For Capsule public string physicsMaterial; public bool shouldCopy = true; } [Serializable] public class AnimatorInfo { public string controllerName; public string controllerPath; public string avatarName; public bool applyRootMotion; public AnimatorUpdateMode updateMode; public AnimatorCullingMode cullingMode; public List parameters = new List(); public bool hasAnimator = false; } [Serializable] public class AnimatorParameterInfo { public string name; public AnimatorControllerParameterType type; public float defaultFloat; public int defaultInt; public bool defaultBool; } [Serializable] public class CameraInfo { public bool hasCamera = false; public string gameObjectName; public Vector3 localPosition; public Quaternion localRotation; public float fieldOfView; public float nearClipPlane; public float farClipPlane; public bool orthographic; public float orthographicSize; public int depth; public CameraClearFlags clearFlags; public Color backgroundColor; public List additionalComponents = new List(); } [Serializable] public class RigidbodyInfo { public bool hasRigidbody = false; public float mass; public float drag; public float angularDrag; public bool useGravity; public bool isKinematic; public RigidbodyInterpolation interpolation; public CollisionDetectionMode collisionDetection; public RigidbodyConstraints constraints; } [Serializable] public class EffectInfo { public string name; public string path; public string effectType; // ParticleSystem, TrailRenderer, etc. public bool isActive; public bool shouldCopy = true; } public enum LogType { Info, Success, Warning, Error } public class LogEntry { public string message; public LogType type; public DateTime timestamp; public LogEntry(string msg, LogType logType) { message = msg; type = logType; timestamp = DateTime.Now; } } #endregion #region Menu and Window [MenuItem("Tools/Character Prefab Transfer Tool")] public static void ShowWindow() { var window = GetWindow("Character Prefab Transfer"); window.minSize = new Vector2(500, 600); window.Show(); } private void InitStyles() { if (stylesInitialized) return; headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14, alignment = TextAnchor.MiddleCenter }; successStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.2f, 0.8f, 0.2f) } }; warningStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.9f, 0.7f, 0.1f) } }; errorStyle = new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(0.9f, 0.2f, 0.2f) } }; boxStyle = new GUIStyle("box") { padding = new RectOffset(10, 10, 10, 10) }; stylesInitialized = true; } #endregion #region Main GUI private void OnGUI() { InitStyles(); scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); DrawHeader(); DrawPrefabSelection(); DrawActionButtons(); if (hasScanned && scanResult != null) { DrawScanResults(); DrawCopyOptions(); } DrawLogSection(); if (isProcessing) { DrawProgressBar(); } EditorGUILayout.EndScrollView(); } private void DrawHeader() { EditorGUILayout.Space(10); EditorGUILayout.LabelField("Character Prefab Transfer Tool", headerStyle); EditorGUILayout.Space(5); EditorGUILayout.HelpBox( "Transfer components, colliders, and child objects from an old character prefab to a new one.\n" + "1. Assign your source (old) and target (new) prefabs\n" + "2. Click 'Scan Source' to analyze the source prefab\n" + "3. Review and select what to copy\n" + "4. Click 'Copy All to Target' to transfer", MessageType.Info); EditorGUILayout.Space(10); } private void DrawPrefabSelection() { EditorGUILayout.BeginVertical(boxStyle); EditorGUILayout.LabelField("Prefab Selection", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUI.BeginChangeCheck(); sourcePrefab = (GameObject)EditorGUILayout.ObjectField( "Source Character Prefab", sourcePrefab, typeof(GameObject), false); if (EditorGUI.EndChangeCheck()) { hasScanned = false; scanResult = null; } targetPrefab = (GameObject)EditorGUILayout.ObjectField( "Target Character Prefab", targetPrefab, typeof(GameObject), false); // Validation messages if (sourcePrefab != null && !PrefabUtility.IsPartOfPrefabAsset(sourcePrefab)) { EditorGUILayout.HelpBox("Source must be a prefab asset, not a scene object.", MessageType.Warning); } if (targetPrefab != null && !PrefabUtility.IsPartOfPrefabAsset(targetPrefab)) { EditorGUILayout.HelpBox("Target must be a prefab asset, not a scene object.", MessageType.Warning); } EditorGUILayout.EndVertical(); } private void DrawActionButtons() { EditorGUILayout.Space(10); EditorGUILayout.BeginHorizontal(); GUI.enabled = sourcePrefab != null && !isProcessing; if (GUILayout.Button("Scan Source", GUILayout.Height(30))) { StartCoroutine(ScanSourceCoroutine()); } GUI.enabled = hasScanned && targetPrefab != null && !isProcessing; if (GUILayout.Button("Copy All to Target", GUILayout.Height(30))) { if (EditorUtility.DisplayDialog("Confirm Copy", "This will modify the target prefab. A backup will be created.\n\nContinue?", "Yes, Copy", "Cancel")) { StartCoroutine(CopyToTargetCoroutine()); } } GUI.enabled = true; EditorGUILayout.EndHorizontal(); // Additional options EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Clear Log", GUILayout.Height(25))) { logEntries.Clear(); } if (GUILayout.Button("Reset All", GUILayout.Height(25))) { ResetTool(); } EditorGUILayout.EndHorizontal(); } private void DrawProgressBar() { EditorGUILayout.Space(10); // Status indicator EditorGUILayout.BeginHorizontal(); // Animated spinner effect using time string[] spinChars = { "◐", "◓", "◑", "◒" }; int spinIndex = (int)(EditorApplication.timeSinceStartup * 4) % spinChars.Length; GUIStyle spinStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleCenter }; EditorGUILayout.LabelField(spinChars[spinIndex], spinStyle, GUILayout.Width(25)); EditorGUILayout.LabelField("Processing...", EditorStyles.boldLabel); EditorGUILayout.EndHorizontal(); // Progress bar Rect rect = EditorGUILayout.GetControlRect(false, 25); EditorGUI.ProgressBar(rect, progress, $"{(progress * 100):F0}% - {progressMessage}"); EditorGUILayout.Space(5); } private void DrawLogSection() { EditorGUILayout.Space(10); showLogs = EditorGUILayout.Foldout(showLogs, $"Log ({logEntries.Count} entries)", true); if (showLogs && logEntries.Count > 0) { EditorGUILayout.BeginVertical(boxStyle); logScrollPosition = EditorGUILayout.BeginScrollView(logScrollPosition, GUILayout.MaxHeight(200)); for (int i = logEntries.Count - 1; i >= 0; i--) { var entry = logEntries[i]; GUIStyle style = entry.type switch { LogType.Success => successStyle, LogType.Warning => warningStyle, LogType.Error => errorStyle, _ => EditorStyles.label }; string icon = entry.type switch { LogType.Success => "✓", LogType.Warning => "⚠", LogType.Error => "✗", _ => "•" }; EditorGUILayout.LabelField($"{icon} [{entry.timestamp:HH:mm:ss}] {entry.message}", style); } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } } #endregion #region Scan Results UI private void DrawScanResults() { EditorGUILayout.Space(10); EditorGUILayout.LabelField("Scan Results", EditorStyles.boldLabel); EditorGUILayout.BeginVertical(boxStyle); EditorGUILayout.LabelField($"Total Components: {scanResult.totalComponentCount}"); EditorGUILayout.LabelField($"Total Child Objects: {scanResult.totalChildCount}"); EditorGUILayout.EndVertical(); // Root Components DrawRootComponentsSection(); // Child Objects DrawChildObjectsSection(); // Colliders DrawCollidersSection(); // Animator DrawAnimatorSection(); // Camera DrawCameraSection(); // Rigidbody DrawRigidbodySection(); // Effects DrawEffectsSection(); } private void DrawRootComponentsSection() { showRootComponents = EditorGUILayout.Foldout(showRootComponents, $"Root Components ({scanResult.rootComponents.Count})", true); if (showRootComponents) { EditorGUI.indentLevel++; foreach (var comp in scanResult.rootComponents) { EditorGUILayout.BeginHorizontal(); comp.shouldCopy = EditorGUILayout.Toggle(comp.shouldCopy, GUILayout.Width(20)); string enabledStatus = comp.isEnabled ? "" : " [Disabled]"; EditorGUILayout.LabelField($"{comp.typeName}{enabledStatus}"); EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel--; } } private void DrawChildObjectsSection() { showChildObjects = EditorGUILayout.Foldout(showChildObjects, $"Child Objects ({scanResult.childObjects.Count})", true); if (showChildObjects) { EditorGUI.indentLevel++; foreach (var child in scanResult.childObjects) { DrawChildObjectRecursive(child, 0); } EditorGUI.indentLevel--; } } private void DrawChildObjectRecursive(ChildObjectInfo child, int depth) { EditorGUILayout.BeginHorizontal(); GUILayout.Space(depth * 20); child.shouldCopy = EditorGUILayout.Toggle(child.shouldCopy, GUILayout.Width(20)); string info = $"{child.name}"; if (!string.IsNullOrEmpty(child.tag) && child.tag != "Untagged") info += $" [{child.tag}]"; if (child.components.Count > 0) info += $" ({child.components.Count} components)"; EditorGUILayout.LabelField(info); EditorGUILayout.EndHorizontal(); foreach (var subChild in child.children) { DrawChildObjectRecursive(subChild, depth + 1); } } private void DrawCollidersSection() { showColliders = EditorGUILayout.Foldout(showColliders, $"Colliders ({scanResult.colliders.Count})", true); if (showColliders) { EditorGUI.indentLevel++; foreach (var col in scanResult.colliders) { EditorGUILayout.BeginHorizontal(); col.shouldCopy = EditorGUILayout.Toggle(col.shouldCopy, GUILayout.Width(20)); string trigger = col.isTrigger ? " (Trigger)" : ""; EditorGUILayout.LabelField($"{col.gameObjectName}: {col.colliderType}{trigger}"); EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel--; } } private void DrawAnimatorSection() { if (!scanResult.animatorInfo.hasAnimator) return; showAnimator = EditorGUILayout.Foldout(showAnimator, "Animator", true); if (showAnimator) { EditorGUI.indentLevel++; EditorGUILayout.LabelField($"Controller: {scanResult.animatorInfo.controllerName}"); EditorGUILayout.LabelField($"Avatar: {scanResult.animatorInfo.avatarName}"); EditorGUILayout.LabelField($"Root Motion: {scanResult.animatorInfo.applyRootMotion}"); EditorGUILayout.LabelField($"Parameters: {scanResult.animatorInfo.parameters.Count}"); EditorGUI.indentLevel--; } } private void DrawCameraSection() { if (!scanResult.cameraInfo.hasCamera) return; showCamera = EditorGUILayout.Foldout(showCamera, "Camera", true); if (showCamera) { EditorGUI.indentLevel++; EditorGUILayout.LabelField($"GameObject: {scanResult.cameraInfo.gameObjectName}"); EditorGUILayout.LabelField($"FOV: {scanResult.cameraInfo.fieldOfView}"); EditorGUILayout.LabelField($"Position: {scanResult.cameraInfo.localPosition}"); EditorGUILayout.LabelField($"Additional Components: {scanResult.cameraInfo.additionalComponents.Count}"); EditorGUI.indentLevel--; } } private void DrawRigidbodySection() { if (!scanResult.rigidbodyInfo.hasRigidbody) return; showRigidbody = EditorGUILayout.Foldout(showRigidbody, "Rigidbody", true); if (showRigidbody) { EditorGUI.indentLevel++; EditorGUILayout.LabelField($"Mass: {scanResult.rigidbodyInfo.mass}"); EditorGUILayout.LabelField($"Drag: {scanResult.rigidbodyInfo.drag}"); EditorGUILayout.LabelField($"Kinematic: {scanResult.rigidbodyInfo.isKinematic}"); EditorGUILayout.LabelField($"Use Gravity: {scanResult.rigidbodyInfo.useGravity}"); EditorGUI.indentLevel--; } } private void DrawEffectsSection() { if (scanResult.effects.Count == 0) return; showEffects = EditorGUILayout.Foldout(showEffects, $"Effects ({scanResult.effects.Count})", true); if (showEffects) { EditorGUI.indentLevel++; foreach (var effect in scanResult.effects) { EditorGUILayout.BeginHorizontal(); effect.shouldCopy = EditorGUILayout.Toggle(effect.shouldCopy, GUILayout.Width(20)); string active = effect.isActive ? "" : " [Inactive]"; EditorGUILayout.LabelField($"{effect.name}: {effect.effectType}{active}"); EditorGUILayout.EndHorizontal(); } EditorGUI.indentLevel--; } } private void DrawCopyOptions() { EditorGUILayout.Space(10); EditorGUILayout.LabelField("Copy Options", EditorStyles.boldLabel); EditorGUILayout.BeginVertical(boxStyle); copyRootComponents = EditorGUILayout.Toggle("Copy Root Components", copyRootComponents); copyChildObjects = EditorGUILayout.Toggle("Copy Child Objects", copyChildObjects); copyColliders = EditorGUILayout.Toggle("Copy Colliders", copyColliders); copyAnimator = EditorGUILayout.Toggle("Copy Animator Settings", copyAnimator); copyCamera = EditorGUILayout.Toggle("Copy Camera", copyCamera); copyRigidbody = EditorGUILayout.Toggle("Copy Rigidbody", copyRigidbody); copyEffects = EditorGUILayout.Toggle("Copy Effects (Particles)", copyEffects); copyMaterials = EditorGUILayout.Toggle("Copy Materials", copyMaterials); copyLayersAndTags = EditorGUILayout.Toggle("Copy Layers & Tags", copyLayersAndTags); EditorGUILayout.Space(5); matchBonesByName = EditorGUILayout.Toggle("Match Bones by Name", matchBonesByName); EditorGUILayout.EndVertical(); } #endregion #region Helper Methods private void Log(string message, LogType type = LogType.Info) { logEntries.Add(new LogEntry(message, type)); switch (type) { case LogType.Error: Debug.LogError($"[CharacterTransfer] {message}"); break; case LogType.Warning: Debug.LogWarning($"[CharacterTransfer] {message}"); break; default: Debug.Log($"[CharacterTransfer] {message}"); break; } Repaint(); } private void ResetTool() { StopCoroutine(); sourcePrefab = null; targetPrefab = null; scanResult = null; hasScanned = false; logEntries.Clear(); progress = 0f; isProcessing = false; } private void OnDestroy() { StopCoroutine(); EditorUtility.ClearProgressBar(); } #endregion #region Coroutine System (EditorApplication.update driven) /// /// Starts an IEnumerator coroutine that is advanced one step per editor frame. /// This prevents editor freezing by yielding control between steps. /// private void StartCoroutine(IEnumerator routine) { StopCoroutine(); // Stop any existing coroutine activeCoroutine = routine; isProcessing = true; wasCancelled = false; EditorApplication.update += EditorUpdateTick; } /// /// Stops the running coroutine and cleans up. /// private void StopCoroutine() { if (activeCoroutine != null) { EditorApplication.update -= EditorUpdateTick; activeCoroutine = null; } EditorUtility.ClearProgressBar(); isProcessing = false; progress = 0f; progressMessage = ""; wasCancelled = false; Repaint(); } /// /// Called every editor frame. Advances the coroutine by one step. /// Between frames, Unity redraws the UI so progress bars actually render. /// private void EditorUpdateTick() { if (activeCoroutine == null) { EditorApplication.update -= EditorUpdateTick; return; } try { // Check for cancellation via the progress bar bool cancelled = EditorUtility.DisplayCancelableProgressBar( "Character Prefab Transfer", $"{progressMessage} ({(progress * 100):F0}%)", progress); if (cancelled) { wasCancelled = true; Log("Operation cancelled by user.", LogType.Warning); StopCoroutine(); EditorUtility.DisplayDialog("Cancelled", "The transfer was cancelled.\nNo changes were saved to the target prefab.", "OK"); return; } // Advance the coroutine by one step bool hasMore = activeCoroutine.MoveNext(); if (!hasMore) { // Coroutine finished naturally StopCoroutine(); } else { // Repaint to show updated progress Repaint(); } } catch (Exception ex) { Log($"COROUTINE ERROR: {ex.Message}", LogType.Error); Log($"Stack trace: {ex.StackTrace}", LogType.Error); StopCoroutine(); EditorUtility.DisplayDialog("Transfer Failed", $"An error occurred:\n\n{ex.Message}\n\nCheck the console and log for details.", "OK"); } } /// /// Sets the progress values that will be shown on the next frame's progress bar. /// Does NOT block - the actual bar rendering happens in EditorUpdateTick. /// private void SetProgress(float value, string message) { progress = value; progressMessage = message; Debug.Log($"[CharacterTransfer] {(value * 100):F0}% - {message}"); } #endregion #region Scanning - Coroutine Based private IEnumerator ScanSourceCoroutine() { Log("Starting scan of source prefab...", LogType.Info); Log($"Source prefab: {sourcePrefab.name}", LogType.Info); SetProgress(0.01f, "Initializing scan..."); yield return null; // Let UI render scanResult = new PrefabScanResult(); // Scan root components SetProgress(0.1f, "Scanning root components..."); yield return null; ScanRootComponents(); Log($"Found {scanResult.rootComponents.Count} root components", LogType.Info); // Scan child objects SetProgress(0.25f, "Scanning child objects..."); yield return null; ScanChildObjects(); // Scan colliders SetProgress(0.4f, "Scanning colliders..."); yield return null; ScanColliders(); // Scan animator SetProgress(0.55f, "Scanning animator..."); yield return null; ScanAnimator(); // Scan camera SetProgress(0.7f, "Scanning camera..."); yield return null; ScanCamera(); // Scan rigidbody SetProgress(0.8f, "Scanning rigidbody..."); yield return null; ScanRigidbody(); // Scan effects SetProgress(0.9f, "Scanning effects..."); yield return null; ScanEffects(); SetProgress(1f, "Scan complete!"); hasScanned = true; Log("=== SCAN COMPLETE ===", LogType.Success); Log($"Total Components: {scanResult.totalComponentCount}", LogType.Success); Log($"Total Child Objects: {scanResult.totalChildCount}", LogType.Success); Log($"Colliders: {scanResult.colliders.Count}", LogType.Info); Log($"Effects: {scanResult.effects.Count}", LogType.Info); yield return null; // Let final progress render EditorUtility.DisplayDialog("Scan Complete", $"Scan of '{sourcePrefab.name}' complete!\n\n" + $"• Components: {scanResult.totalComponentCount}\n" + $"• Child Objects: {scanResult.totalChildCount}\n" + $"• Colliders: {scanResult.colliders.Count}\n" + $"• Effects: {scanResult.effects.Count}\n\n" + "Review the results below and click 'Copy All to Target' when ready.", "OK"); } /// /// Legacy synchronous scan - kept as internal method called by coroutine /// private void ScanSourcePrefab() { StartCoroutine(ScanSourceCoroutine()); } #endregion #region Copying - Coroutine Based (Non-Blocking) /// /// Coroutine-based copy operation. Each yield return null gives the editor /// a full frame to render the progress bar, preventing freezing. /// private IEnumerator CopyToTargetCoroutine() { Log("Starting copy to target prefab...", LogType.Info); Log($"Source: {sourcePrefab.name}, Target: {targetPrefab.name}", LogType.Info); GameObject targetInstance = null; string targetPath = null; int currentStep = 0; int totalSteps = 0; // ---- STEP: Create backup ---- SetProgress(0.02f, "Creating backup of target prefab..."); yield return null; // FRAME BREAK - lets progress bar render try { CreateBackup(); Log("Backup created successfully.", LogType.Success); } catch (Exception ex) { Log($"Backup failed: {ex.Message}", LogType.Error); EditorUtility.DisplayDialog("Backup Failed", ex.Message, "OK"); yield break; // Stop the coroutine } // ---- STEP: Calculate bone ratios for proportional scaling ---- SetProgress(0.04f, "Calculating bone proportions..."); yield return null; try { CalculateBoneRatios(); } catch (Exception ex) { Log($"Warning: Could not calculate bone ratios: {ex.Message}. Using 1:1 scaling.", LogType.Warning); overallScaleRatio = 1f; boneRatiosCalculated = false; } // ---- STEP: Load prefab ---- SetProgress(0.06f, "Loading target prefab for editing..."); yield return null; try { targetPath = AssetDatabase.GetAssetPath(targetPrefab); Log($"Loading prefab from: {targetPath}", LogType.Info); targetInstance = PrefabUtility.LoadPrefabContents(targetPath); if (targetInstance == null) { throw new Exception("Failed to load target prefab contents. The prefab may be corrupted."); } Log("Target prefab loaded successfully.", LogType.Success); } catch (Exception ex) { Log($"Failed to load prefab: {ex.Message}", LogType.Error); EditorUtility.DisplayDialog("Load Failed", ex.Message, "OK"); yield break; } // ---- Calculate steps ---- if (copyRootComponents) totalSteps++; if (copyChildObjects) totalSteps++; if (copyAnimator) totalSteps++; if (copyCamera) totalSteps++; if (copyRigidbody) totalSteps++; if (copyEffects) totalSteps++; if (copyLayersAndTags) totalSteps++; if (totalSteps == 0) { Log("No copy options selected!", LogType.Warning); PrefabUtility.UnloadPrefabContents(targetInstance); yield break; } float baseProgress = 0.10f; float stepSize = 0.75f / Mathf.Max(1, totalSteps); // ---- STEP: Copy Root Components ---- if (copyRootComponents) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying root components... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK // Copy each component with a yield between heavy ones foreach (var compInfo in scanResult.rootComponents) { if (!compInfo.shouldCopy) continue; try { CopySingleRootComponent(targetInstance, compInfo); } catch (Exception ex) { Log($"Error copying {compInfo.typeName}: {ex.Message}", LogType.Error); } // Update sub-progress SetProgress(stepProgress + stepSize * 0.5f, $"Copying component: {compInfo.typeName}"); yield return null; // FRAME BREAK per component } currentStep++; Log($"Root components copied. ({currentStep}/{totalSteps})", LogType.Success); } // ---- STEP: Copy Child Objects ---- if (copyChildObjects) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying child objects... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK int childIdx = 0; foreach (var childInfo in scanResult.childObjects) { if (!childInfo.shouldCopy) continue; SetProgress(stepProgress + (stepSize * childIdx / Mathf.Max(1, scanResult.childObjects.Count)), $"Copying child: {childInfo.name}"); yield return null; // FRAME BREAK per child try { CopyChildObjectRecursive(childInfo, targetInstance.transform, sourcePrefab.transform); } catch (Exception ex) { Log($"Error copying child {childInfo.name}: {ex.Message}", LogType.Error); } childIdx++; } currentStep++; Log($"Child objects copied. ({currentStep}/{totalSteps})", LogType.Success); } // ---- STEP: Copy Animator ---- if (copyAnimator) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying animator settings... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK try { CopyAnimatorSettings(targetInstance); currentStep++; Log($"Animator settings copied. ({currentStep}/{totalSteps})", LogType.Success); } catch (Exception ex) { Log($"Error copying animator: {ex.Message}", LogType.Error); currentStep++; } } // ---- STEP: Copy Camera ---- if (copyCamera) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying camera settings... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK try { CopyCameraSettings(targetInstance); currentStep++; Log($"Camera settings copied. ({currentStep}/{totalSteps})", LogType.Success); } catch (Exception ex) { Log($"Error copying camera: {ex.Message}", LogType.Error); currentStep++; } } // ---- STEP: Copy Rigidbody ---- if (copyRigidbody) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying rigidbody settings... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK try { CopyRigidbodySettings(targetInstance); currentStep++; Log($"Rigidbody settings copied. ({currentStep}/{totalSteps})", LogType.Success); } catch (Exception ex) { Log($"Error copying rigidbody: {ex.Message}", LogType.Error); currentStep++; } } // ---- STEP: Copy Effects ---- if (copyEffects) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying effects... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK int effectIdx = 0; foreach (var effect in scanResult.effects) { if (!effect.shouldCopy) continue; SetProgress(stepProgress + (stepSize * effectIdx / Mathf.Max(1, scanResult.effects.Count)), $"Copying effect: {effect.name}"); yield return null; // FRAME BREAK per effect try { CopySingleEffect(targetInstance, effect); } catch (Exception ex) { Log($"Error copying effect {effect.name}: {ex.Message}", LogType.Error); } effectIdx++; } currentStep++; Log($"Effects copied. ({currentStep}/{totalSteps})", LogType.Success); } // ---- STEP: Copy Layers and Tags ---- if (copyLayersAndTags) { float stepProgress = baseProgress + (currentStep * stepSize); SetProgress(stepProgress, $"Copying layers and tags... (Step {currentStep + 1}/{totalSteps})"); yield return null; // FRAME BREAK try { CopyLayersAndTags(targetInstance); currentStep++; Log($"Layers and tags copied. ({currentStep}/{totalSteps})", LogType.Success); } catch (Exception ex) { Log($"Error copying layers/tags: {ex.Message}", LogType.Error); currentStep++; } } // ---- STEP: Save prefab ---- SetProgress(0.90f, "Saving modified prefab..."); yield return null; // FRAME BREAK try { Log("Saving prefab...", LogType.Info); PrefabUtility.SaveAsPrefabAsset(targetInstance, targetPath); Log("Prefab saved successfully!", LogType.Success); } catch (Exception ex) { Log($"Failed to save prefab: {ex.Message}", LogType.Error); PrefabUtility.UnloadPrefabContents(targetInstance); EditorUtility.DisplayDialog("Save Failed", ex.Message, "OK"); yield break; } // ---- STEP: Unload and refresh ---- SetProgress(0.94f, "Unloading prefab contents..."); yield return null; try { PrefabUtility.UnloadPrefabContents(targetInstance); targetInstance = null; } catch (Exception ex) { Log($"Warning: Could not unload prefab contents: {ex.Message}", LogType.Warning); } SetProgress(0.97f, "Refreshing asset database..."); yield return null; AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); SetProgress(1f, "Transfer complete!"); Log("=== TRANSFER COMPLETE ===", LogType.Success); yield return null; // Let the final 100% render // Show completion dialog (this is the last thing) EditorUtility.DisplayDialog("Transfer Complete", $"Successfully transferred components from:\n'{sourcePrefab.name}'\nto:\n'{targetPrefab.name}'\n\n" + $"Completed {currentStep} operations.\nCheck the log for details.", "OK"); } /// /// Legacy synchronous entry point - now just starts the coroutine /// private void CopyToTarget() { StartCoroutine(CopyToTargetCoroutine()); } #endregion // Part 2 and 3 methods will be added below #region Scanning Implementation private void ScanRootComponents() { Component[] components = sourcePrefab.GetComponents(); foreach (var comp in components) { if (comp == null) continue; if (comp is Transform) continue; // Skip transform var info = new ComponentInfo { typeName = comp.GetType().Name, fullTypeName = comp.GetType().FullName, isEnabled = IsComponentEnabled(comp) }; // Get serialized fields var serializedObj = new SerializedObject(comp); var prop = serializedObj.GetIterator(); while (prop.NextVisible(true)) { if (prop.name == "m_Script") continue; info.serializedFields.Add(new FieldValueInfo { fieldName = prop.name, fieldType = prop.type, valuePreview = GetPropertyValuePreview(prop), isReference = prop.propertyType == SerializedPropertyType.ObjectReference }); } scanResult.rootComponents.Add(info); } scanResult.totalComponentCount = scanResult.rootComponents.Count; Log($"Found {scanResult.rootComponents.Count} root components", LogType.Info); } private void ScanChildObjects() { int childCount = 0; foreach (Transform child in sourcePrefab.transform) { var childInfo = ScanChildRecursive(child, ""); scanResult.childObjects.Add(childInfo); childCount += CountChildren(childInfo); } scanResult.totalChildCount = childCount; Log($"Found {childCount} child objects", LogType.Info); } private ChildObjectInfo ScanChildRecursive(Transform obj, string parentPath) { string path = string.IsNullOrEmpty(parentPath) ? obj.name : $"{parentPath}/{obj.name}"; var info = new ChildObjectInfo { name = obj.name, path = path, tag = obj.tag, layer = obj.gameObject.layer, localPosition = obj.localPosition, localRotation = obj.localRotation, localScale = obj.localScale, parentBoneName = obj.parent != null ? obj.parent.name : "" }; // Scan components on this child Component[] components = obj.GetComponents(); foreach (var comp in components) { if (comp == null || comp is Transform) continue; var compInfo = new ComponentInfo { typeName = comp.GetType().Name, fullTypeName = comp.GetType().FullName, isEnabled = IsComponentEnabled(comp) }; info.components.Add(compInfo); } // Scan children recursively foreach (Transform child in obj) { info.children.Add(ScanChildRecursive(child, path)); } return info; } private int CountChildren(ChildObjectInfo info) { int count = 1; foreach (var child in info.children) { count += CountChildren(child); } return count; } private void ScanColliders() { Collider[] colliders = sourcePrefab.GetComponentsInChildren(true); foreach (var col in colliders) { var info = new ColliderInfo { gameObjectName = col.gameObject.name, gameObjectPath = GetGameObjectPath(col.transform, sourcePrefab.transform), colliderType = col.GetType().Name, isTrigger = col.isTrigger, physicsMaterial = col.sharedMaterial != null ? col.sharedMaterial.name : "None" }; switch (col) { case BoxCollider box: info.center = box.center; info.size = box.size; break; case SphereCollider sphere: info.center = sphere.center; info.radius = sphere.radius; break; case CapsuleCollider capsule: info.center = capsule.center; info.radius = capsule.radius; info.height = capsule.height; info.direction = capsule.direction; break; } scanResult.colliders.Add(info); } Log($"Found {scanResult.colliders.Count} colliders", LogType.Info); } private void ScanAnimator() { Animator animator = sourcePrefab.GetComponent(); if (animator == null) { scanResult.animatorInfo = new AnimatorInfo { hasAnimator = false }; return; } scanResult.animatorInfo = new AnimatorInfo { hasAnimator = true, controllerName = animator.runtimeAnimatorController != null ? animator.runtimeAnimatorController.name : "None", controllerPath = animator.runtimeAnimatorController != null ? AssetDatabase.GetAssetPath(animator.runtimeAnimatorController) : "", avatarName = animator.avatar != null ? animator.avatar.name : "None", applyRootMotion = animator.applyRootMotion, updateMode = animator.updateMode, cullingMode = animator.cullingMode }; // Get parameters if controller exists if (animator.runtimeAnimatorController != null) { foreach (var param in animator.parameters) { scanResult.animatorInfo.parameters.Add(new AnimatorParameterInfo { name = param.name, type = param.type, defaultFloat = param.defaultFloat, defaultInt = param.defaultInt, defaultBool = param.defaultBool }); } } Log($"Found animator with controller: {scanResult.animatorInfo.controllerName}", LogType.Info); } private void ScanCamera() { Camera cam = sourcePrefab.GetComponentInChildren(true); if (cam == null) { scanResult.cameraInfo = new CameraInfo { hasCamera = false }; return; } scanResult.cameraInfo = new CameraInfo { hasCamera = true, gameObjectName = cam.gameObject.name, localPosition = cam.transform.localPosition, localRotation = cam.transform.localRotation, fieldOfView = cam.fieldOfView, nearClipPlane = cam.nearClipPlane, farClipPlane = cam.farClipPlane, orthographic = cam.orthographic, orthographicSize = cam.orthographicSize, depth = (int)cam.depth, clearFlags = cam.clearFlags, backgroundColor = cam.backgroundColor }; // Get additional components on camera object foreach (var comp in cam.GetComponents()) { if (comp is Transform || comp is Camera) continue; scanResult.cameraInfo.additionalComponents.Add(new ComponentInfo { typeName = comp.GetType().Name, fullTypeName = comp.GetType().FullName, isEnabled = IsComponentEnabled(comp) }); } Log($"Found camera: {scanResult.cameraInfo.gameObjectName}", LogType.Info); } private void ScanRigidbody() { Rigidbody rb = sourcePrefab.GetComponent(); if (rb == null) { scanResult.rigidbodyInfo = new RigidbodyInfo { hasRigidbody = false }; return; } scanResult.rigidbodyInfo = new RigidbodyInfo { hasRigidbody = true, mass = rb.mass, drag = rb.linearDamping, angularDrag = rb.angularDamping, useGravity = rb.useGravity, isKinematic = rb.isKinematic, interpolation = rb.interpolation, collisionDetection = rb.collisionDetectionMode, constraints = rb.constraints }; Log("Found rigidbody", LogType.Info); } private void ScanEffects() { // Scan particle systems ParticleSystem[] particles = sourcePrefab.GetComponentsInChildren(true); foreach (var ps in particles) { scanResult.effects.Add(new EffectInfo { name = ps.gameObject.name, path = GetGameObjectPath(ps.transform, sourcePrefab.transform), effectType = "ParticleSystem", isActive = ps.gameObject.activeSelf }); } // Scan trail renderers TrailRenderer[] trails = sourcePrefab.GetComponentsInChildren(true); foreach (var trail in trails) { // Avoid duplicates if same object has particle system bool alreadyAdded = scanResult.effects.Any(e => e.path == GetGameObjectPath(trail.transform, sourcePrefab.transform)); if (!alreadyAdded) { scanResult.effects.Add(new EffectInfo { name = trail.gameObject.name, path = GetGameObjectPath(trail.transform, sourcePrefab.transform), effectType = "TrailRenderer", isActive = trail.gameObject.activeSelf }); } } Log($"Found {scanResult.effects.Count} effects", LogType.Info); } #endregion #region Helper Methods for Scanning private bool IsComponentEnabled(Component comp) { if (comp is Behaviour behaviour) return behaviour.enabled; if (comp is Renderer renderer) return renderer.enabled; if (comp is Collider collider) return collider.enabled; return true; } private string GetPropertyValuePreview(SerializedProperty prop) { return prop.propertyType switch { SerializedPropertyType.Integer => prop.intValue.ToString(), SerializedPropertyType.Float => prop.floatValue.ToString("F2"), SerializedPropertyType.Boolean => prop.boolValue.ToString(), SerializedPropertyType.String => prop.stringValue, SerializedPropertyType.Vector3 => prop.vector3Value.ToString(), SerializedPropertyType.ObjectReference => prop.objectReferenceValue != null ? prop.objectReferenceValue.name : "null", _ => prop.type }; } private string GetGameObjectPath(Transform obj, Transform root) { List path = new List(); Transform current = obj; while (current != null && current != root) { path.Insert(0, current.name); current = current.parent; } return string.Join("/", path); } #endregion #region Bone Ratio System - Smart Proportional Scaling /// /// Maps limb names to start/end HumanBodyBones for length measurement. /// private static readonly Dictionary LimbBoneChains = new Dictionary { {"leftarm", new[] { HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftHand }}, {"rightarm", new[] { HumanBodyBones.RightUpperArm, HumanBodyBones.RightHand }}, {"leftleg", new[] { HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftFoot }}, {"rightleg", new[] { HumanBodyBones.RightUpperLeg, HumanBodyBones.RightFoot }}, {"torso", new[] { HumanBodyBones.Hips, HumanBodyBones.Head }}, }; /// /// Maps collider / child-object name keywords to limb names for ratio lookup. /// private static readonly Dictionary ColliderKeywordToLimb = new Dictionary(StringComparer.OrdinalIgnoreCase) { {"lefthand", "leftarm"}, {"righthand", "rightarm"}, {"leftfist", "leftarm"}, {"rightfist", "rightarm"}, {"leftfoot", "leftleg"}, {"rightfoot", "rightleg"}, {"leftleg", "leftleg"}, {"rightleg", "rightleg"}, {"body", "torso"}, {"chest", "torso"}, {"torso", "torso"}, {"head", "torso"}, {"spine", "torso"}, {"hips", "torso"}, }; /// /// Calculate bone-proportional scale ratios between source and target characters. /// Uses Animator.GetBoneTransform for humanoid rigs, falls back to overall bounds height. /// private void CalculateBoneRatios() { limbRatios.Clear(); overallScaleRatio = 1f; boneRatiosCalculated = false; // Temporarily instantiate both prefabs so Animator is initialized and GetBoneTransform works GameObject sourceInstance = null; GameObject targetInstance = null; try { sourceInstance = PrefabUtility.InstantiatePrefab(sourcePrefab) as GameObject; targetInstance = PrefabUtility.InstantiatePrefab(targetPrefab) as GameObject; if (sourceInstance == null || targetInstance == null) { Log("Could not instantiate prefabs for bone measurement. Using 1:1 ratio.", LogType.Warning); return; } Animator sourceAnim = sourceInstance.GetComponent(); Animator targetAnim = targetInstance.GetComponent(); if (sourceAnim != null && targetAnim != null && sourceAnim.avatar != null && targetAnim.avatar != null && sourceAnim.avatar.isHuman && targetAnim.avatar.isHuman) { // Humanoid rig - use GetBoneTransform for precise per-limb ratios CalculateHumanoidRatios(sourceAnim, targetAnim); Log("Using Humanoid bone ratios for proportional scaling.", LogType.Success); } else { // Non-humanoid or missing avatar - use overall renderer bounds ratio CalculateFallbackRatio(sourceInstance, targetInstance); } boneRatiosCalculated = true; Log($"Bone ratios calculated. Overall ratio: {overallScaleRatio:F3}", LogType.Success); foreach (var kvp in limbRatios) { Log($" Limb '{kvp.Key}' ratio: {kvp.Value:F3}", LogType.Info); } } catch (Exception ex) { Log($"Error calculating bone ratios: {ex.Message}. Using 1:1 ratio.", LogType.Warning); overallScaleRatio = 1f; } finally { if (sourceInstance != null) DestroyImmediate(sourceInstance); if (targetInstance != null) DestroyImmediate(targetInstance); } } private void CalculateHumanoidRatios(Animator sourceAnim, Animator targetAnim) { // Overall height ratio (Hips to Head) float sourceHeight = GetLimbLength(sourceAnim, HumanBodyBones.Hips, HumanBodyBones.Head); float targetHeight = GetLimbLength(targetAnim, HumanBodyBones.Hips, HumanBodyBones.Head); overallScaleRatio = (sourceHeight > 0.001f) ? (targetHeight / sourceHeight) : 1f; // Per-limb ratios foreach (var kvp in LimbBoneChains) { float sourceLen = GetLimbLength(sourceAnim, kvp.Value[0], kvp.Value[1]); float targetLen = GetLimbLength(targetAnim, kvp.Value[0], kvp.Value[1]); float ratio = (sourceLen > 0.001f) ? (targetLen / sourceLen) : overallScaleRatio; limbRatios[kvp.Key] = ratio; } } private float GetLimbLength(Animator animator, HumanBodyBones startBone, HumanBodyBones endBone) { Transform start = animator.GetBoneTransform(startBone); Transform end = animator.GetBoneTransform(endBone); if (start == null || end == null) return 0f; return Vector3.Distance(start.position, end.position); } private void CalculateFallbackRatio(GameObject sourceInstance, GameObject targetInstance) { Bounds sourceBounds = CalculateTotalBounds(sourceInstance); Bounds targetBounds = CalculateTotalBounds(targetInstance); float sourceHeight = sourceBounds.size.y; float targetHeight = targetBounds.size.y; overallScaleRatio = (sourceHeight > 0.001f) ? (targetHeight / sourceHeight) : 1f; // Use overall ratio for all limbs foreach (var kvp in LimbBoneChains) { limbRatios[kvp.Key] = overallScaleRatio; } Log("Using overall bounds ratio (non-humanoid fallback).", LogType.Info); } private Bounds CalculateTotalBounds(GameObject obj) { Renderer[] renderers = obj.GetComponentsInChildren(true); if (renderers.Length == 0) return new Bounds(obj.transform.position, Vector3.one); Bounds bounds = renderers[0].bounds; for (int i = 1; i < renderers.Length; i++) { bounds.Encapsulate(renderers[i].bounds); } return bounds; } /// /// Gets the scale ratio for a given child-object name by matching keywords to limb ratios. /// E.g. "lefthandcollider" → matches "lefthand" → returns leftarm ratio. /// private float GetScaleRatioForObject(string objectName) { if (!boneRatiosCalculated) return 1f; string nameLower = objectName.ToLowerInvariant().Replace(" ", "").Replace("_", ""); foreach (var kvp in ColliderKeywordToLimb) { if (nameLower.Contains(kvp.Key.ToLowerInvariant())) { if (limbRatios.TryGetValue(kvp.Value, out float ratio)) { return ratio; } } } return overallScaleRatio; } /// /// Gets the scale ratio based on a parent bone name. /// E.g. bone "LeftHand" → matches left arm → returns leftarm ratio. /// private float GetScaleRatioForBone(string boneName) { if (!boneRatiosCalculated || string.IsNullOrEmpty(boneName)) return overallScaleRatio; string nameLower = boneName.ToLowerInvariant().Replace(" ", "").Replace("_", ""); if (nameLower.Contains("lefthand") || nameLower.Contains("leftlowerarm") || nameLower.Contains("leftupperarm") || nameLower.Contains("leftshoulder") || nameLower.Contains("l_hand") || nameLower.Contains("l_arm") || nameLower.Contains("lhand") || nameLower.Contains("larm")) { return limbRatios.TryGetValue("leftarm", out float r) ? r : overallScaleRatio; } if (nameLower.Contains("righthand") || nameLower.Contains("rightlowerarm") || nameLower.Contains("rightupperarm") || nameLower.Contains("rightshoulder") || nameLower.Contains("r_hand") || nameLower.Contains("r_arm") || nameLower.Contains("rhand") || nameLower.Contains("rarm")) { return limbRatios.TryGetValue("rightarm", out float r) ? r : overallScaleRatio; } if (nameLower.Contains("leftfoot") || nameLower.Contains("leftlowerleg") || nameLower.Contains("leftupperleg") || nameLower.Contains("lefttoe") || nameLower.Contains("l_foot") || nameLower.Contains("l_leg") || nameLower.Contains("lfoot") || nameLower.Contains("lleg")) { return limbRatios.TryGetValue("leftleg", out float r) ? r : overallScaleRatio; } if (nameLower.Contains("rightfoot") || nameLower.Contains("rightlowerleg") || nameLower.Contains("rightupperleg") || nameLower.Contains("righttoe") || nameLower.Contains("r_foot") || nameLower.Contains("r_leg") || nameLower.Contains("rfoot") || nameLower.Contains("rleg")) { return limbRatios.TryGetValue("rightleg", out float r) ? r : overallScaleRatio; } if (nameLower.Contains("hips") || nameLower.Contains("spine") || nameLower.Contains("chest") || nameLower.Contains("head") || nameLower.Contains("neck") || nameLower.Contains("torso")) { return limbRatios.TryGetValue("torso", out float r) ? r : overallScaleRatio; } return overallScaleRatio; } /// /// Scales a collider's dimensions (center, size, radius, height) by the given ratio. /// private void ScaleCollider(Collider collider, float ratio) { if (collider == null || Mathf.Approximately(ratio, 1f)) return; switch (collider) { case BoxCollider box: box.center *= ratio; box.size *= ratio; break; case SphereCollider sphere: sphere.center *= ratio; sphere.radius *= ratio; break; case CapsuleCollider capsule: capsule.center *= ratio; capsule.radius *= ratio; capsule.height *= ratio; break; case MeshCollider mesh: Log($"MeshCollider on '{mesh.gameObject.name}' cannot be auto-scaled - manual adjustment needed.", LogType.Warning); break; } } #endregion #region Copying Implementation private void CreateBackup() { string targetPath = AssetDatabase.GetAssetPath(targetPrefab); string backupPath = targetPath.Replace(".prefab", "_backup.prefab"); // Check if backup already exists int counter = 1; while (AssetDatabase.LoadAssetAtPath(backupPath) != null) { backupPath = targetPath.Replace(".prefab", $"_backup_{counter}.prefab"); counter++; } AssetDatabase.CopyAsset(targetPath, backupPath); Log($"Created backup at: {backupPath}", LogType.Success); } /// /// Copies a single root component to the target (called per-component from coroutine). /// CRITICAL: Preserves target's root Transform - never lets source overwrite scale/position/rotation. /// private void CopySingleRootComponent(GameObject targetInstance, ComponentInfo compInfo) { try { Type compType = GetTypeByName(compInfo.fullTypeName); if (compType == null) { Log($"Could not find type: {compInfo.typeName}", LogType.Warning); return; } // NEVER copy Transform - target must keep its own transform values if (typeof(Transform).IsAssignableFrom(compType)) return; // Skip Animator - handled separately by CopyAnimatorSettings to preserve target's Avatar if (typeof(Animator).IsAssignableFrom(compType)) { Log($"Skipping Animator in root copy (handled separately to preserve target's Avatar)", LogType.Info); return; } Component sourceComp = sourcePrefab.GetComponent(compType); if (sourceComp == null) return; Component targetComp = targetInstance.GetComponent(compType); if (targetComp == null) { targetComp = targetInstance.AddComponent(compType); if (targetComp == null) { Log($"Failed to add component: {compInfo.typeName}", LogType.Error); return; } } // CRITICAL: Save target's root Transform before CopySerialized // EditorUtility.CopySerialized can indirectly affect transform values Vector3 savedPosition = targetInstance.transform.localPosition; Quaternion savedRotation = targetInstance.transform.localRotation; Vector3 savedScale = targetInstance.transform.localScale; CopyComponentValues(sourceComp, targetComp); // CRITICAL: Restore target's root Transform - NEVER let source overwrite it targetInstance.transform.localPosition = savedPosition; targetInstance.transform.localRotation = savedRotation; targetInstance.transform.localScale = savedScale; SetComponentEnabled(targetComp, compInfo.isEnabled); Log($"Copied component: {compInfo.typeName}", LogType.Success); } catch (Exception ex) { Log($"Failed to copy {compInfo.typeName}: {ex.Message}", LogType.Error); } } private void CopyRootComponents(GameObject targetInstance) { foreach (var compInfo in scanResult.rootComponents) { if (!compInfo.shouldCopy) continue; CopySingleRootComponent(targetInstance, compInfo); } } private void CopyChildObjects(GameObject targetInstance) { foreach (var childInfo in scanResult.childObjects) { if (!childInfo.shouldCopy) continue; try { CopyChildObjectRecursive(childInfo, targetInstance.transform, sourcePrefab.transform); } catch (Exception ex) { Log($"Failed to copy child {childInfo.name}: {ex.Message}", LogType.Error); } } } /// /// Smart bone-proportional child object copy. /// - Finds the matching bone on the target skeleton (by name) /// - Creates FRESH GameObjects (not Object.Instantiate which carries source scale) /// - Copies components individually and scales colliders by the limb ratio /// - Only applies scaled transforms to NEW objects; existing skeleton bones are untouched /// private void CopyChildObjectRecursive(ChildObjectInfo childInfo, Transform targetParent, Transform sourceRoot) { // Find source object Transform sourceChild = sourceRoot.Find(childInfo.name); if (sourceChild == null) { sourceChild = FindByPath(sourceRoot, childInfo.path); } // Determine target parent bone Transform targetBone = targetParent; if (matchBonesByName && !string.IsNullOrEmpty(childInfo.parentBoneName)) { Transform matchingBone = FindBoneByName(targetParent.root, childInfo.parentBoneName); if (matchingBone != null) { targetBone = matchingBone; } } // Check if target already has this object (e.g. existing skeleton bone) Transform existingChild = targetBone.Find(childInfo.name); if (existingChild == null) { existingChild = targetParent.Find(childInfo.name); } Transform targetChild; bool isNewObject = false; if (existingChild != null) { targetChild = existingChild; Log($"Found existing child: {childInfo.name} (will update components only)", LogType.Info); } else { // Create a FRESH empty GameObject — do NOT use Object.Instantiate from source // because Instantiate carries the source's baked transform/scale data GameObject newChild = new GameObject(childInfo.name); newChild.transform.SetParent(targetBone); targetChild = newChild.transform; isNewObject = true; if (targetBone != targetParent) { Log($"Created '{childInfo.name}' under bone: {targetBone.name}", LogType.Success); } else { Log($"Created child: {childInfo.name}", LogType.Success); } } // Calculate the proportional scale ratio for this object // First try matching by object name (e.g. "lefthandcollider" → leftarm ratio) float ratio = GetScaleRatioForObject(childInfo.name); // Refine with parent bone name if object name didn't give a specific match if (Mathf.Approximately(ratio, overallScaleRatio)) { float boneRatio = GetScaleRatioForBone(childInfo.parentBoneName); if (!Mathf.Approximately(boneRatio, overallScaleRatio)) { ratio = boneRatio; } } // Apply transform ONLY to NEW objects — never overwrite existing skeleton bone transforms if (isNewObject) { // Position is scaled by ratio so collider offsets match target proportions targetChild.localPosition = childInfo.localPosition * ratio; targetChild.localRotation = childInfo.localRotation; targetChild.localScale = childInfo.localScale; // Keep original local scale } // Copy components from source to target child if (sourceChild != null && isNewObject) { Component[] sourceComponents = sourceChild.GetComponents(); foreach (var sourceComp in sourceComponents) { if (sourceComp == null || sourceComp is Transform) continue; Type compType = sourceComp.GetType(); Component targetComp = targetChild.GetComponent(compType); if (targetComp == null) { try { targetComp = targetChild.gameObject.AddComponent(compType); } catch (Exception ex) { Log($"Could not add {compType.Name} to {childInfo.name}: {ex.Message}", LogType.Warning); continue; } } if (targetComp != null) { // Save child's transform before CopySerialized Vector3 savedPos = targetChild.localPosition; Quaternion savedRot = targetChild.localRotation; Vector3 savedScale = targetChild.localScale; CopyComponentValues(sourceComp, targetComp); // Restore transform (CopySerialized can affect it indirectly) targetChild.localPosition = savedPos; targetChild.localRotation = savedRot; targetChild.localScale = savedScale; } } // Scale all colliders on this object proportionally to target's bone dimensions Collider[] colliders = targetChild.GetComponents(); foreach (var col in colliders) { ScaleCollider(col, ratio); Log($"Auto-scaled {col.GetType().Name} on '{childInfo.name}' by ratio {ratio:F3}", LogType.Info); } } else if (sourceChild != null && !isNewObject) { // For existing objects, update collider values from source then scale Collider[] sourceColliders = sourceChild.GetComponents(); Collider[] targetColliders = targetChild.GetComponents(); for (int i = 0; i < Mathf.Min(sourceColliders.Length, targetColliders.Length); i++) { Vector3 savedPos = targetChild.localPosition; Quaternion savedRot = targetChild.localRotation; Vector3 savedScale = targetChild.localScale; CopyComponentValues(sourceColliders[i], targetColliders[i]); targetChild.localPosition = savedPos; targetChild.localRotation = savedRot; targetChild.localScale = savedScale; ScaleCollider(targetColliders[i], ratio); } } // Apply layer and tag if (copyLayersAndTags) { targetChild.gameObject.layer = childInfo.layer; try { targetChild.gameObject.tag = childInfo.tag; } catch { /* Tag might not exist */ } } // Recurse into sub-children (since we don't use Instantiate, we must build the full tree) foreach (var subChildInfo in childInfo.children) { if (!subChildInfo.shouldCopy) continue; CopyChildObjectRecursive(subChildInfo, targetChild, sourceChild != null ? sourceChild : sourceRoot); } } private void CopyAnimatorSettings(GameObject targetInstance) { if (!scanResult.animatorInfo.hasAnimator) return; Animator sourceAnimator = sourcePrefab.GetComponent(); Animator targetAnimator = targetInstance.GetComponent(); if (targetAnimator == null) { targetAnimator = targetInstance.AddComponent(); } // Copy controller targetAnimator.runtimeAnimatorController = sourceAnimator.runtimeAnimatorController; // Copy settings targetAnimator.applyRootMotion = sourceAnimator.applyRootMotion; targetAnimator.updateMode = sourceAnimator.updateMode; targetAnimator.cullingMode = sourceAnimator.cullingMode; // Note: Avatar should typically be the target's own avatar for humanoid rigs // but we can copy it for generic rigs or if explicitly requested if (targetAnimator.avatar == null) { Log("Target has no avatar - you may need to configure the avatar separately", LogType.Warning); } Log("Copied animator settings", LogType.Success); } private void CopyCameraSettings(GameObject targetInstance) { if (!scanResult.cameraInfo.hasCamera) return; // Find or create camera object Camera sourceCamera = sourcePrefab.GetComponentInChildren(true); Camera targetCamera = targetInstance.GetComponentInChildren(true); if (sourceCamera == null) return; if (targetCamera == null) { // Clone the entire camera object GameObject cameraClone = UnityEngine.Object.Instantiate( sourceCamera.gameObject, targetInstance.transform); cameraClone.name = sourceCamera.gameObject.name; targetCamera = cameraClone.GetComponent(); Log("Created new camera object", LogType.Success); } else { // Update existing camera targetCamera.fieldOfView = sourceCamera.fieldOfView; targetCamera.nearClipPlane = sourceCamera.nearClipPlane; targetCamera.farClipPlane = sourceCamera.farClipPlane; targetCamera.orthographic = sourceCamera.orthographic; targetCamera.orthographicSize = sourceCamera.orthographicSize; targetCamera.depth = sourceCamera.depth; targetCamera.clearFlags = sourceCamera.clearFlags; targetCamera.backgroundColor = sourceCamera.backgroundColor; // Copy position relative to parent targetCamera.transform.localPosition = scanResult.cameraInfo.localPosition; targetCamera.transform.localRotation = scanResult.cameraInfo.localRotation; Log("Updated camera settings", LogType.Success); } // Copy additional camera components foreach (var compInfo in scanResult.cameraInfo.additionalComponents) { Type compType = GetTypeByName(compInfo.fullTypeName); if (compType == null) continue; Component sourceComp = sourceCamera.GetComponent(compType); Component targetComp = targetCamera.GetComponent(compType); if (targetComp == null && sourceComp != null) { targetComp = targetCamera.gameObject.AddComponent(compType); } if (sourceComp != null && targetComp != null) { CopyComponentValues(sourceComp, targetComp); } } } private void CopyRigidbodySettings(GameObject targetInstance) { if (!scanResult.rigidbodyInfo.hasRigidbody) return; Rigidbody targetRb = targetInstance.GetComponent(); if (targetRb == null) { targetRb = targetInstance.AddComponent(); } targetRb.mass = scanResult.rigidbodyInfo.mass; targetRb.linearDamping = scanResult.rigidbodyInfo.drag; targetRb.angularDamping = scanResult.rigidbodyInfo.angularDrag; targetRb.useGravity = scanResult.rigidbodyInfo.useGravity; targetRb.isKinematic = scanResult.rigidbodyInfo.isKinematic; targetRb.interpolation = scanResult.rigidbodyInfo.interpolation; targetRb.collisionDetectionMode = scanResult.rigidbodyInfo.collisionDetection; targetRb.constraints = scanResult.rigidbodyInfo.constraints; Log("Copied rigidbody settings", LogType.Success); } /// /// Copies a single effect to the target (called per-effect from coroutine) /// private void CopySingleEffect(GameObject targetInstance, EffectInfo effect) { try { Transform sourceEffect = FindByPath(sourcePrefab.transform, effect.path); if (sourceEffect == null) { sourceEffect = sourcePrefab.transform.Find(effect.name); } if (sourceEffect == null) { Log($"Could not find source effect: {effect.name}", LogType.Warning); return; } Transform targetParent = targetInstance.transform; if (sourceEffect.parent != null && sourceEffect.parent != sourcePrefab.transform) { string parentName = sourceEffect.parent.name; Transform matchingParent = FindBoneByName(targetInstance.transform, parentName); if (matchingParent != null) { targetParent = matchingParent; } } Transform existingEffect = FindByPath(targetInstance.transform, effect.path); if (existingEffect == null) { existingEffect = targetParent.Find(effect.name); } if (existingEffect != null) { CopyComponentValues( sourceEffect.GetComponent(effect.effectType == "ParticleSystem" ? typeof(ParticleSystem) : typeof(TrailRenderer)), existingEffect.GetComponent(effect.effectType == "ParticleSystem" ? typeof(ParticleSystem) : typeof(TrailRenderer)) ); Log($"Updated effect: {effect.name}", LogType.Success); } else { GameObject cloned = UnityEngine.Object.Instantiate(sourceEffect.gameObject, targetParent); cloned.name = effect.name; cloned.SetActive(effect.isActive); // Scale position proportionally to target's skeleton float ratio = GetScaleRatioForBone(targetParent.name); cloned.transform.localPosition = sourceEffect.localPosition * ratio; cloned.transform.localRotation = sourceEffect.localRotation; Log($"Copied effect: {effect.name} (position scaled by {ratio:F3})", LogType.Success); } } catch (Exception ex) { Log($"Failed to copy effect {effect.name}: {ex.Message}", LogType.Warning); } } private void CopyEffects(GameObject targetInstance) { foreach (var effect in scanResult.effects) { if (!effect.shouldCopy) continue; CopySingleEffect(targetInstance, effect); } } private void CopyLayersAndTags(GameObject targetInstance) { // Copy root object layer and tag targetInstance.layer = sourcePrefab.layer; try { targetInstance.tag = sourcePrefab.tag; } catch { Log("Could not copy root tag - tag may not exist in project", LogType.Warning); } Log("Copied layers and tags", LogType.Success); } #endregion #region Helper Methods for Copying private Type GetTypeByName(string typeName) { // Try to find the type in all loaded assemblies foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { var type = assembly.GetType(typeName); if (type != null) return type; } // Try simple name search foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { foreach (var type in assembly.GetTypes()) { if (type.Name == typeName || type.FullName == typeName) return type; } } return null; } private void CopyComponentValues(Component source, Component target) { if (source == null || target == null) return; if (source.GetType() != target.GetType()) return; try { // Use EditorUtility.CopySerialized for best results EditorUtility.CopySerialized(source, target); } catch { // Fallback to SerializedObject approach try { var sourceObj = new SerializedObject(source); var targetObj = new SerializedObject(target); var prop = sourceObj.GetIterator(); while (prop.NextVisible(true)) { if (prop.name == "m_Script") continue; targetObj.CopyFromSerializedProperty(prop); } targetObj.ApplyModifiedPropertiesWithoutUndo(); } catch (Exception ex) { Log($"Could not copy all values for {source.GetType().Name}: {ex.Message}", LogType.Warning); } } } private void SetComponentEnabled(Component comp, bool enabled) { if (comp is Behaviour behaviour) behaviour.enabled = enabled; else if (comp is Renderer renderer) renderer.enabled = enabled; else if (comp is Collider collider) collider.enabled = enabled; } private Transform FindByPath(Transform root, string path) { if (string.IsNullOrEmpty(path)) return null; string[] parts = path.Split('/'); Transform current = root; foreach (var part in parts) { if (string.IsNullOrEmpty(part)) continue; Transform child = current.Find(part); if (child == null) return null; current = child; } return current == root ? null : current; } private Transform FindBoneByName(Transform root, string boneName) { if (string.IsNullOrEmpty(boneName)) return null; // Direct child check Transform direct = root.Find(boneName); if (direct != null) return direct; // Recursive search foreach (Transform child in root) { if (child.name == boneName) return child; Transform found = FindBoneByName(child, boneName); if (found != null) return found; } return null; } #endregion }