MaximilianPs

MassPrefabRestorer

Aug 23rd, 2025 (edited)
117
0
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 11.54 KB | Gaming | 0 0
  1. using UnityEngine;
  2. using UnityEditor;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using UnityEngine.SceneManagement;
  7. using UnityEditor.SceneManagement;
  8.  
  9. public class MassPrefabRestorer : EditorWindow
  10. {
  11.     // A support class to "snapshot" the state of the original object
  12.     private class ObjectSnapshot
  13.     {
  14.         public Vector3 Position;
  15.         public Quaternion Rotation;
  16.         public Vector3 Scale;
  17.         public Transform Parent;
  18.         public int SiblingIndex;
  19.         public List<Transform> OriginalChildren;
  20.         public Material[] Materials; // Array to support multi-material renderers
  21.     }
  22.  
  23.     private DefaultAsset sourceFolder;
  24.     private string prefixToRemove = "";
  25.     private string suffixDelimiter = " (";
  26.     private bool searchInSubfolders = true;
  27.  
  28.     [MenuItem("Tools/Mass Prefab Restorer")]
  29.     public static void ShowWindow()
  30.     {
  31.         var window = GetWindow<MassPrefabRestorer>(utility: true, title: "Mass Prefab Restorer");
  32.         window.minSize = new Vector2(450, 260);
  33.         window.maxSize = new Vector2(450, 260);
  34.     }
  35.  
  36.     void OnGUI()
  37.     {
  38.         EditorGUILayout.LabelField("Mass Prefab Restorer", EditorStyles.boldLabel);
  39.         EditorGUILayout.HelpBox("Intelligently replaces objects, preserving transforms, unique children, and material overrides.", MessageType.Info);
  40.  
  41.         EditorGUILayout.Space();
  42.  
  43.         EditorGUILayout.LabelField("1. Prefab Folder Settings", EditorStyles.boldLabel);
  44.         sourceFolder = (DefaultAsset)EditorGUILayout.ObjectField("Prefab Source Folder", sourceFolder, typeof(DefaultAsset), false);
  45.         searchInSubfolders = EditorGUILayout.Toggle("Search in subfolders", searchInSubfolders);
  46.  
  47.         EditorGUILayout.Space();
  48.  
  49.         EditorGUILayout.LabelField("2. Name Cleanup Settings", EditorStyles.boldLabel);
  50.         prefixToRemove = EditorGUILayout.TextField("Prefix to remove", prefixToRemove);
  51.         suffixDelimiter = EditorGUILayout.TextField("Suffix Delimiter", suffixDelimiter);
  52.  
  53.         EditorGUILayout.Space();
  54.  
  55.         if (GUILayout.Button("Start Object Replacement", GUILayout.Height(40)))
  56.         {
  57.             ReplaceObjects();
  58.         }
  59.     }
  60.  
  61.     void ReplaceObjects()
  62.     {
  63.         if (sourceFolder == null) { EditorUtility.DisplayDialog("Error", "Please select a source folder.", "OK"); return; }
  64.         string folderPath = AssetDatabase.GetAssetPath(sourceFolder);
  65.         if (!AssetDatabase.IsValidFolder(folderPath)) { EditorUtility.DisplayDialog("Error", "The selected asset is not a valid folder.", "OK"); return; }
  66.         SearchOption searchOption = searchInSubfolders ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
  67.         string[] prefabPaths = Directory.GetFiles(folderPath, "*.prefab", searchOption);
  68.         if (prefabPaths.Length == 0) { EditorUtility.DisplayDialog("No Prefabs Found", "No .prefab files were found in the selected folder.", "OK"); return; }
  69.         var prefabDict = new Dictionary<string, GameObject>();
  70.         foreach (var path in prefabPaths)
  71.         {
  72.             var prefabName = Path.GetFileNameWithoutExtension(path);
  73.             var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
  74.             if (prefab != null && !prefabDict.ContainsKey(prefabName)) { prefabDict.Add(prefabName, prefab); }
  75.         }
  76.  
  77.         Scene targetScene;
  78.         int replacedCount = 0;
  79.         var notFoundLog = new List<string>();
  80.  
  81.         Undo.SetCurrentGroupName("Mass Prefab Restore");
  82.         var group = Undo.GetCurrentGroup();
  83.  
  84.         var currentPrefabStage = PrefabStageUtility.GetCurrentPrefabStage();
  85.         if (currentPrefabStage != null && currentPrefabStage.scene.IsValid())
  86.         {
  87.             targetScene = currentPrefabStage.scene;
  88.             GameObject prefabRoot = currentPrefabStage.prefabContentsRoot;
  89.             Debug.Log("In Prefab Stage: Processing children of the root object only.");
  90.             List<Transform> childrenOfRoot = prefabRoot.transform.Cast<Transform>().ToList();
  91.             foreach (var child in childrenOfRoot)
  92.             {
  93.                 ProcessObjectRecursively(child.gameObject, prefabDict, ref replacedCount, notFoundLog, targetScene);
  94.             }
  95.         }
  96.         else
  97.         {
  98.             targetScene = SceneManager.GetActiveScene();
  99.             if (!targetScene.IsValid()) { EditorUtility.DisplayDialog("Error", "Could not get a valid scene to operate on.", "OK"); return; }
  100.             Debug.Log("In a regular scene: Processing all root objects.");
  101.             var rootObjects = new List<GameObject>();
  102.             targetScene.GetRootGameObjects(rootObjects);
  103.             if (rootObjects.Count == 0) { EditorUtility.DisplayDialog("Empty Scene", "No objects found in the scene hierarchy.", "OK"); return; }
  104.             foreach (var rootObj in rootObjects)
  105.             {
  106.                 ProcessObjectRecursively(rootObj, prefabDict, ref replacedCount, notFoundLog, targetScene);
  107.             }
  108.         }
  109.  
  110.         Undo.CollapseUndoOperations(group);
  111.         var message = $"Operation complete.\n\nReplaced: {replacedCount}\nPrefabs not found: {notFoundLog.Count}";
  112.         EditorUtility.DisplayDialog("Result", message, "OK");
  113.         Debug.Log($"<color=green><b>--- Mass Prefab Restore Complete ---</b></color> (Scene: {targetScene.name})");
  114.     }
  115.  
  116.     void ProcessObjectRecursively(GameObject currentObject, Dictionary<string, GameObject> prefabDict, ref int replacedCount, List<string> notFoundLog, Scene targetScene)
  117.     {
  118.         if (PrefabUtility.GetPrefabInstanceStatus(currentObject) == PrefabInstanceStatus.Connected)
  119.         {
  120.             var children = currentObject.transform.Cast<Transform>().ToList();
  121.             foreach (var child in children) ProcessObjectRecursively(child.gameObject, prefabDict, ref replacedCount, notFoundLog, targetScene);
  122.             return;
  123.         }
  124.  
  125.         var originalName = currentObject.name;
  126.         var cleanedName = CleanObjectName(originalName);
  127.  
  128.         if (!string.IsNullOrEmpty(cleanedName) && prefabDict.TryGetValue(cleanedName, out var prefab))
  129.         {
  130.             ReplaceAndStopRecursion(currentObject, prefab, targetScene);
  131.             replacedCount++;
  132.             Debug.Log($"<color=cyan>Replaced:</color> '{originalName}' -> with prefab '{cleanedName}'");
  133.         }
  134.         else
  135.         {
  136.             if (originalName.Contains(suffixDelimiter)) { notFoundLog.Add($"Prefab '{cleanedName}' not found for object '{originalName}'"); }
  137.             var children = currentObject.transform.Cast<Transform>().ToList();
  138.             foreach (var child in children) ProcessObjectRecursively(child.gameObject, prefabDict, ref replacedCount, notFoundLog, targetScene);
  139.         }
  140.     }
  141.  
  142.     string CleanObjectName(string originalName)
  143.     {
  144.         string cleanedName = originalName;
  145.         int delimiterIndex = cleanedName.IndexOf(suffixDelimiter);
  146.         if (delimiterIndex >= 0) { cleanedName = cleanedName.Substring(0, delimiterIndex); }
  147.         if (!string.IsNullOrEmpty(prefixToRemove) && cleanedName.StartsWith(prefixToRemove)) { cleanedName = cleanedName.Substring(prefixToRemove.Length); }
  148.         return cleanedName.Trim();
  149.     }
  150.  
  151.     void ReplaceAndStopRecursion(GameObject originalObj, GameObject prefab, Scene targetScene)
  152.     {
  153.         // 1. CAPTURE STATE into the snapshot
  154.         var snapshot = new ObjectSnapshot
  155.         {
  156.             Position = originalObj.transform.position,
  157.             Rotation = originalObj.transform.rotation,
  158.             Scale = originalObj.transform.localScale,
  159.             Parent = originalObj.transform.parent,
  160.             SiblingIndex = originalObj.transform.GetSiblingIndex(),
  161.             OriginalChildren = originalObj.transform.Cast<Transform>().ToList()
  162.         };
  163.  
  164.         var originalRenderer = originalObj.GetComponent<MeshRenderer>();
  165.         if (originalRenderer != null)
  166.         {
  167.             snapshot.Materials = originalRenderer.sharedMaterials;
  168.         }
  169.  
  170.         // 2. INSTANTIATE the new prefab
  171.         GameObject newInstance = (GameObject)PrefabUtility.InstantiatePrefab(prefab, targetScene);
  172.         Undo.RegisterCreatedObjectUndo(newInstance, "Create Prefab Instance");
  173.  
  174.         // 3. APPLY STATE from the snapshot
  175.         newInstance.transform.SetParent(snapshot.Parent, worldPositionStays: true);
  176.         newInstance.transform.SetPositionAndRotation(snapshot.Position, snapshot.Rotation);
  177.         newInstance.transform.localScale = snapshot.Scale;
  178.         newInstance.transform.SetSiblingIndex(snapshot.SiblingIndex);
  179.         newInstance.name = prefab.name;
  180.  
  181.         // --- START: FINAL, ROBUST Smart Material Restoration Logic ---
  182.         var newRenderer = newInstance.GetComponent<MeshRenderer>();
  183.         if (newRenderer != null && snapshot.Materials != null) // Check snapshot.Materials != null, but allow length 0
  184.         {
  185.             Undo.RecordObject(newRenderer, "Restore Material Overrides");
  186.  
  187.             // Get the default materials from the newly instantiated prefab.
  188.             var defaultPrefabMaterials = newRenderer.sharedMaterials;
  189.             // Create a brand new array to hold the final material setup.
  190.             var finalMaterials = new Material[defaultPrefabMaterials.Length];
  191.  
  192.             // Determine the number of materials to check from the original object.
  193.             int materialsToCheck = Mathf.Min(snapshot.Materials.Length, finalMaterials.Length);
  194.  
  195.             // 1. Iterate through the materials that can be potentially restored.
  196.             for (int i = 0; i < materialsToCheck; i++)
  197.             {
  198.                 // THE KEY FIX: If the original material is not null, it's a valid override. Use it.
  199.                 if (snapshot.Materials[i] != null)
  200.                 {
  201.                     finalMaterials[i] = snapshot.Materials[i];
  202.                 }
  203.                 // If the original material IS null (purple object), keep the prefab's default material.
  204.                 else
  205.                 {
  206.                     finalMaterials[i] = defaultPrefabMaterials[i];
  207.                 }
  208.             }
  209.  
  210.             // 2. If the new prefab has MORE slots, fill the rest with their defaults.
  211.             for (int i = materialsToCheck; i < finalMaterials.Length; i++)
  212.             {
  213.                 finalMaterials[i] = defaultPrefabMaterials[i];
  214.             }
  215.  
  216.             // Assign the newly constructed, repaired array.
  217.             newRenderer.sharedMaterials = finalMaterials;
  218.  
  219.             // Log appropriate messages.
  220.             if (snapshot.Materials.Length == finalMaterials.Length)
  221.             {
  222.                 Debug.Log($"<color=lime>Materials restored for:</color> '{newInstance.name}'");
  223.             }
  224.             else
  225.             {
  226.                 Debug.LogWarning($"Material slot count mismatch for '{newInstance.name}'. Partially restored materials. (Original had {snapshot.Materials.Length}, New has {finalMaterials.Length}). Please review the object.");
  227.             }
  228.         }
  229.         // --- END: FINAL, ROBUST Smart Material Restoration Logic ---
  230.  
  231.         var newPrefabChildrenNames = new HashSet<string>(newInstance.transform.Cast<Transform>().Select(c => c.gameObject.name));
  232.         foreach (var originalChild in snapshot.OriginalChildren)
  233.         {
  234.             if (!newPrefabChildrenNames.Contains(originalChild.gameObject.name))
  235.             {
  236.                 Debug.Log($"<color=orange>Preserved unique child:</color> '{originalChild.name}' and re-parented under '{newInstance.name}'.");
  237.                 Undo.SetTransformParent(originalChild, newInstance.transform, "Preserve Unique Child");
  238.             }
  239.         }
  240.  
  241.         Undo.DestroyObjectImmediate(originalObj);
  242.     }
  243. }
Comments
  • MaximilianPs
    345 days
    Comment was deleted
  • MaximilianPs
    345 days
    # C# 1.18 KB | 0 0
    1. == Key Improvements ==
    2. * ObjectSnapshot Class: We now have a clean container that makes the code easier to read.
    3.   ReplaceAndStopRecursion first "captures" everything in the snapshot and then "applies" the data.
    4. * Save Materials (Material[]): The snapshot now has a Materials field.
    5.   It is populated only if a MeshRenderer exists on the original object, avoiding errors.
    6.   We use the .materials property, which returns a copy of the array, ensuring the data is safe before the original object is destroyed.
    7.   Safe Material Restore: Before applying the saved materials, the script checks two things:
    8.   That the new prefab actually has a MeshRenderer.
    9.   That the number of material "slots" on the new prefab matches that of the original object. This prevents errors and warns the user in the console if, for example, they are replacing an object with 3 materials with a prefab that only has 1.
    10.  * Undo Integration:
    11.   The material change operation is now correctly recorded with Undo.RecordObject, so you can undo it with Ctrl+Z.
    12.  
    13. -- Last Edit:
    14. The system now is much smarter, avoid potential issue with Materials, and can restore the default materials from prefab if, for some reason they are missing.
Add Comment
Please, Sign In to add comment