Skip to content

Instantly share code, notes, and snippets.

@SolidAlloy
Last active July 30, 2023 03:38
Show Gist options
  • Save SolidAlloy/bcea34227edbbbbd5de82bf3f6f9c5f9 to your computer and use it in GitHub Desktop.
Save SolidAlloy/bcea34227edbbbbd5de82bf3f6f9c5f9 to your computer and use it in GitHub Desktop.
Unity utility class for reverting overrides of a field or component in all prefab instances.
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using vietlabs.fr2;
// I was frustrated with the fact that RectTransform fields are often marked as overriden when prefab instances
// of UI elements are used in windows. So if I change, let's say, width of RectTransform of a button which I have
// 100 instances of accross multiple windows and scenes, the change does not apply anywhere, and I have to go
// through all the prefabs and revert the field override individually.
// Thus, an idea of automating the revert of a field came up and I implemented it.
// This utility uses FindReference2 to find all the prefabs and scenes where a prefab is used.
// If you don't want to use FindReference2, you can use this utility as a base, and implement your own way
// to find the assets that use the target prefab.
public static class PrefabPropertyReverter
{
const string REVERT_PATH = "CONTEXT/Component/Revert in all instances";
[MenuItem(REVERT_PATH, priority = 0)]
static void RevertComponent(MenuCommand command)
{
var component = (Component) command.context;
string prefabPath = PrefabStageUtility.GetCurrentPrefabStage().assetPath;
IterateOverridenInstanceComponents(component, prefabPath, ApplyChangeToComponent);
static void ApplyChangeToComponent(Component targetComponent, string assetPath)
{
PrefabUtility.RevertObjectOverride(targetComponent, InteractionMode.UserAction);
LogComponentChange(targetComponent, assetPath, "component");
}
}
[MenuItem(REVERT_PATH, validate = true)]
static bool ValidateRevertComponent(MenuCommand command)
{
return PrefabStageUtility.GetCurrentPrefabStage() != null;
}
[InitializeOnLoadMethod]
static void Initialize()
{
EditorApplication.contextualPropertyMenu += OnPropertyContextMenu;
static void OnPropertyContextMenu(GenericMenu menu, SerializedProperty property)
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage == null) return;
var propertyCopy = property.Copy();
menu.AddItem(new GUIContent("Revert in all instances"), false, () => RevertInAllInstances(propertyCopy, prefabStage.assetPath));
}
}
static void RevertInAllInstances(SerializedProperty property, string prefabPath)
{
var component = (Component)property.serializedObject.targetObject;
string propertyPath = property.propertyPath;
IterateOverridenInstanceComponents(component, prefabPath, ApplyChangeToComponent);
void ApplyChangeToComponent(Component targetComponent, string assetPath)
{
var serializedObject = new SerializedObject(targetComponent);
var targetProperty = serializedObject.FindProperty(propertyPath);
if (!targetProperty.prefabOverride) return;
PrefabUtility.RevertPropertyOverride(targetProperty, InteractionMode.UserAction);
LogComponentChange(targetComponent, assetPath, "property");
}
}
static void IterateOverridenInstanceComponents(Component component, string prefabPath, Action<Component, string> applyChangeToComponent)
{
var componentType = component.GetType();
var bottomUpHierarchy = GetBottomUpHierarchy(component.transform).ToList();
string thisPrefabGUID = AssetDatabase.AssetPathToGUID(prefabPath);
var currentScene = SceneManager.GetActiveScene();
var usedByDict = FR2_Ref.FindUsedBy(new[] { thisPrefabGUID });
foreach (var (usedAssetGUID, usedAssetRef) in usedByDict)
{
if (usedAssetRef.depth == 0) continue;
string assetPath = AssetDatabase.GUIDToAssetPath(usedAssetGUID);
Scene scene = default;
PrefabUtility.EditPrefabContentsScope prefabScope = default;
IEnumerable<GameObject> rootGameObjects;
bool isScene = assetPath.EndsWith(".unity");
if (isScene)
{
scene = currentScene.path == assetPath ? currentScene : EditorSceneManager.OpenScene(assetPath, OpenSceneMode.Additive);
rootGameObjects = scene.GetRootGameObjects();
}
else
{
prefabScope = new PrefabUtility.EditPrefabContentsScope(assetPath);
rootGameObjects = prefabScope.prefabContentsRoot.SingleItemAsEnumerable();
}
try
{
foreach (var rootGameObject in rootGameObjects)
{
var instanceRoots = GetChildrenRecursively(rootGameObject.transform)
.Where(PrefabUtility.IsAnyPrefabInstanceRoot)
.Where(gameObject => prefabPath == PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(gameObject));
foreach (var instanceRoot in instanceRoots)
{
var objectOverrides = PrefabUtility.GetObjectOverrides(instanceRoot, true);
var targetTransform = GetChildByHierarchy(instanceRoot, bottomUpHierarchy);
var targetComponent = targetTransform.GetComponent(componentType);
if (objectOverrides.Any(objOverride => objOverride.instanceObject == targetComponent))
applyChangeToComponent(targetComponent, assetPath);
}
}
}
finally
{
if (isScene)
{
if (scene != currentScene)
{
EditorSceneManager.SaveScene(scene);
EditorSceneManager.CloseScene(scene, true);
}
}
else
{
prefabScope.Dispose();
}
}
}
}
static void LogComponentChange(Component component, string assetPath, string revertedObjectName)
{
Debug.Log($"Reverted {revertedObjectName} in {assetPath}/{string.Join('/', GetBottomUpHierarchyNames(component.transform).Reverse())}");
static IEnumerable<string> GetBottomUpHierarchyNames(Transform transform)
{
while (transform.parent != null)
{
yield return transform.name;
transform = transform.parent;
}
}
}
static Transform GetChildByHierarchy(GameObject root, List<int> bottomUpHierarchy)
{
var obj = root.transform;
// go top down
for (int i = bottomUpHierarchy.Count - 1; i >= 0; i--)
{
obj = obj.GetChild(bottomUpHierarchy[i]);
}
return obj;
}
static IEnumerable<int> GetBottomUpHierarchy(Transform transform)
{
while (transform.parent != null)
{
var parent = transform.parent;
// If this is a UI prefab, the root gameObject will be a helper canvas, so we don't need to include it.
// The same goes for a prefab open in scene context.
if (parent.parent == null && parent.gameObject.hideFlags.HasFlag(HideFlags.NotEditable))
yield break;
yield return transform.GetSiblingIndex();
transform = parent;
}
}
static IEnumerable<GameObject> GetChildrenRecursively(Transform root)
{
foreach (Transform child in root)
{
yield return child.gameObject;
foreach (var childGameObject in GetChildrenRecursively(child))
{
yield return childGameObject;
}
}
}
}
public static class EnumerableExtensions
{
public static IEnumerable<T> SingleItemAsEnumerable<T>(this T item)
{
yield return item;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment