Last active
July 30, 2023 03:38
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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