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
}