2383 lines
84 KiB
C#
2383 lines
84 KiB
C#
using UnityEngine;
|
|
using UnityEditor;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<LogEntry> logEntries = new List<LogEntry>();
|
|
|
|
// 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<string, string> boneMapping = new Dictionary<string, string>();
|
|
|
|
// 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<string, float> limbRatios = new Dictionary<string, float>();
|
|
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<ComponentInfo> rootComponents = new List<ComponentInfo>();
|
|
public List<ChildObjectInfo> childObjects = new List<ChildObjectInfo>();
|
|
public List<ColliderInfo> colliders = new List<ColliderInfo>();
|
|
public AnimatorInfo animatorInfo;
|
|
public CameraInfo cameraInfo;
|
|
public RigidbodyInfo rigidbodyInfo;
|
|
public List<EffectInfo> effects = new List<EffectInfo>();
|
|
public int totalComponentCount;
|
|
public int totalChildCount;
|
|
}
|
|
|
|
[Serializable]
|
|
public class ComponentInfo
|
|
{
|
|
public string typeName;
|
|
public string fullTypeName;
|
|
public bool isEnabled;
|
|
public List<FieldValueInfo> serializedFields = new List<FieldValueInfo>();
|
|
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<ComponentInfo> components = new List<ComponentInfo>();
|
|
public List<ChildObjectInfo> children = new List<ChildObjectInfo>();
|
|
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<AnimatorParameterInfo> parameters = new List<AnimatorParameterInfo>();
|
|
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<ComponentInfo> additionalComponents = new List<ComponentInfo>();
|
|
}
|
|
|
|
[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<CharacterPrefabTransferTool>("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)
|
|
|
|
/// <summary>
|
|
/// Starts an IEnumerator coroutine that is advanced one step per editor frame.
|
|
/// This prevents editor freezing by yielding control between steps.
|
|
/// </summary>
|
|
private void StartCoroutine(IEnumerator routine)
|
|
{
|
|
StopCoroutine(); // Stop any existing coroutine
|
|
activeCoroutine = routine;
|
|
isProcessing = true;
|
|
wasCancelled = false;
|
|
EditorApplication.update += EditorUpdateTick;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the running coroutine and cleans up.
|
|
/// </summary>
|
|
private void StopCoroutine()
|
|
{
|
|
if (activeCoroutine != null)
|
|
{
|
|
EditorApplication.update -= EditorUpdateTick;
|
|
activeCoroutine = null;
|
|
}
|
|
EditorUtility.ClearProgressBar();
|
|
isProcessing = false;
|
|
progress = 0f;
|
|
progressMessage = "";
|
|
wasCancelled = false;
|
|
Repaint();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called every editor frame. Advances the coroutine by one step.
|
|
/// Between frames, Unity redraws the UI so progress bars actually render.
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Legacy synchronous scan - kept as internal method called by coroutine
|
|
/// </summary>
|
|
private void ScanSourcePrefab()
|
|
{
|
|
StartCoroutine(ScanSourceCoroutine());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Copying - Coroutine Based (Non-Blocking)
|
|
|
|
/// <summary>
|
|
/// Coroutine-based copy operation. Each yield return null gives the editor
|
|
/// a full frame to render the progress bar, preventing freezing.
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Legacy synchronous entry point - now just starts the coroutine
|
|
/// </summary>
|
|
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<Component>();
|
|
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<Component>();
|
|
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<Collider>(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<Animator>();
|
|
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<Camera>(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<Component>())
|
|
{
|
|
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<Rigidbody>();
|
|
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<ParticleSystem>(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<TrailRenderer>(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<string> path = new List<string>();
|
|
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
|
|
|
|
/// <summary>
|
|
/// Maps limb names to start/end HumanBodyBones for length measurement.
|
|
/// </summary>
|
|
private static readonly Dictionary<string, HumanBodyBones[]> LimbBoneChains = new Dictionary<string, HumanBodyBones[]>
|
|
{
|
|
{"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 }},
|
|
};
|
|
|
|
/// <summary>
|
|
/// Maps collider / child-object name keywords to limb names for ratio lookup.
|
|
/// </summary>
|
|
private static readonly Dictionary<string, string> ColliderKeywordToLimb = new Dictionary<string, string>(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"},
|
|
};
|
|
|
|
/// <summary>
|
|
/// Calculate bone-proportional scale ratios between source and target characters.
|
|
/// Uses Animator.GetBoneTransform for humanoid rigs, falls back to overall bounds height.
|
|
/// </summary>
|
|
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>();
|
|
Animator targetAnim = targetInstance.GetComponent<Animator>();
|
|
|
|
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<Renderer>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the scale ratio for a given child-object name by matching keywords to limb ratios.
|
|
/// E.g. "lefthandcollider" → matches "lefthand" → returns leftarm ratio.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the scale ratio based on a parent bone name.
|
|
/// E.g. bone "LeftHand" → matches left arm → returns leftarm ratio.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales a collider's dimensions (center, size, radius, height) by the given ratio.
|
|
/// </summary>
|
|
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<GameObject>(backupPath) != null)
|
|
{
|
|
backupPath = targetPath.Replace(".prefab", $"_backup_{counter}.prefab");
|
|
counter++;
|
|
}
|
|
|
|
AssetDatabase.CopyAsset(targetPath, backupPath);
|
|
Log($"Created backup at: {backupPath}", LogType.Success);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
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<Component>();
|
|
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<Collider>();
|
|
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>();
|
|
Collider[] targetColliders = targetChild.GetComponents<Collider>();
|
|
|
|
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>();
|
|
Animator targetAnimator = targetInstance.GetComponent<Animator>();
|
|
|
|
if (targetAnimator == null)
|
|
{
|
|
targetAnimator = targetInstance.AddComponent<Animator>();
|
|
}
|
|
|
|
// 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<Camera>(true);
|
|
Camera targetCamera = targetInstance.GetComponentInChildren<Camera>(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<Camera>();
|
|
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<Rigidbody>();
|
|
if (targetRb == null)
|
|
{
|
|
targetRb = targetInstance.AddComponent<Rigidbody>();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies a single effect to the target (called per-effect from coroutine)
|
|
/// </summary>
|
|
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
|
|
}
|