Last active
May 17, 2024 12:26
-
-
Save JLChnToZ/def0f46444b1135695ea02b8268d6c04 to your computer and use it in GitHub Desktop.
Take snapshots of properties of objects/components on play mode and re-apply them when stopped.
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.Linq; | |
using System.Reflection; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.SceneManagement; | |
using UnityEditor; | |
using UnityEditor.SceneManagement; | |
using UnityObject = UnityEngine.Object; | |
[InitializeOnLoad] | |
public static class ObjectSnapshot { | |
const string MENU_NAME = "Take Snapshot on Property Values"; | |
const string GAME_OBJECT_TAKE_SNAPSHOT_MENU = "GameObject/" + MENU_NAME; | |
const string COMPONENT_TAKE_SNAPSHOT_MENU = "CONTEXT/Component/" + MENU_NAME; | |
static readonly PropertyInfo gradientValueProperty; | |
static readonly Dictionary<GlobalObjectReference, Dictionary<string, ObjectSnapshotData>> snapshots; | |
static ObjectSnapshot() { | |
snapshots = new Dictionary<GlobalObjectReference, Dictionary<string, ObjectSnapshotData>>(); | |
gradientValueProperty = typeof(SerializedProperty).GetProperty("gradientValue", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); | |
EditorApplication.playModeStateChanged += OnPlayModeStateChanged; | |
EditorApplication.contextualPropertyMenu += OnPropertyContextMenu; | |
} | |
static void OnPlayModeStateChanged(PlayModeStateChange change) { | |
if (change == PlayModeStateChange.EnteredEditMode && snapshots.Count > 0) { | |
Undo.IncrementCurrentGroup(); | |
int undoGroup = Undo.GetCurrentGroup(); | |
var tempOpenedScenes = new HashSet<Scene>(); | |
try { | |
int i = 0; | |
foreach (var kv in snapshots) { | |
var reference = kv.Key; | |
EditorUtility.DisplayProgressBar("Restore Snapshot", reference.ToString(), (float)i / snapshots.Count); | |
i++; | |
if (kv.Value.Count == 0) continue; | |
if (reference.OpenSceneIfRequired(out var scene)) | |
tempOpenedScenes.Add(scene); | |
if (!reference.TryGet(out var target)) { | |
Debug.LogWarning($"Failed to restore {reference}."); | |
continue; | |
} | |
using (var so = new SerializedObject(target)) { | |
foreach (var state in kv.Value.Values.OrderBy(c => c.path, PathComparer.Default)) { | |
if (state.OpenSceneIfRequired(out scene)) | |
tempOpenedScenes.Add(scene); | |
if (!state.Restore(so)) | |
Debug.LogWarning($"Failed to restore {target.name} {state.path} ({state.type})", target); | |
} | |
so.ApplyModifiedProperties(); | |
} | |
} | |
} finally { | |
EditorUtility.ClearProgressBar(); | |
Undo.SetCurrentGroupName("Restore Snapshot from Play Mode"); | |
Undo.CollapseUndoOperations(undoGroup); | |
snapshots.Clear(); | |
foreach (var scene in tempOpenedScenes) { | |
if (!EditorSceneManager.SaveScene(scene)) | |
Debug.LogWarning($"Failed to save scene {scene.name}"); | |
if (!EditorSceneManager.CloseScene(scene, true)) | |
Debug.LogWarning($"Failed to close scene {scene.name}"); | |
} | |
} | |
} | |
} | |
static void OnPropertyContextMenu(GenericMenu menu, SerializedProperty property) { | |
if (!EditorApplication.isPlaying) return; | |
var targets = property.serializedObject.targetObjects; | |
if (targets.Length == 0) return; | |
menu.AddItem(new GUIContent(MENU_NAME), false, TakeSnapshot, (targets, property.Copy())); | |
} | |
[MenuItem(COMPONENT_TAKE_SNAPSHOT_MENU)] | |
static void TakeSnapshot(MenuCommand command) => TakeSnapshot(command.context); | |
[MenuItem(COMPONENT_TAKE_SNAPSHOT_MENU, true)] | |
static bool TakeSnapshotEnabled(MenuCommand command) => command.context != null && EditorApplication.isPlaying; | |
[MenuItem(GAME_OBJECT_TAKE_SNAPSHOT_MENU, false, 49)] | |
static void TakeSnapshot() { | |
var components = new List<Component>(); | |
foreach (var go in Selection.gameObjects) { | |
if (!IsObjectEditable(go)) continue; | |
TakeSnapshot(go); | |
go.GetComponents(components); | |
foreach (var component in components) | |
if (IsObjectEditable(component)) | |
TakeSnapshot(component); | |
} | |
} | |
[MenuItem(GAME_OBJECT_TAKE_SNAPSHOT_MENU, true, 49)] | |
static bool TakeSnapshotEnabled() => EditorApplication.isPlaying && Selection.gameObjects.Any(IsObjectEditable); | |
public static void TakeSnapshot(UnityObject obj) { | |
if (obj == null) return; | |
Scene scene; | |
if (obj is GameObject gameObject) scene = gameObject.scene; | |
else if (obj is Component component) scene = component.gameObject.scene; | |
else return; | |
if (!scene.IsValid() || scene.buildIndex < 0) return; | |
var key = new GlobalObjectReference(obj); | |
if (!snapshots.TryGetValue(key, out var datas)) | |
snapshots[key] = datas = new Dictionary<string, ObjectSnapshotData>(); | |
using (var so = new SerializedObject(obj)) | |
AddTree(datas, so.GetIterator()); | |
} | |
public static void TakeSnapshot(UnityObject obj, string path) { | |
if (obj == null || string.IsNullOrEmpty(path)) return; | |
using (var so = new SerializedObject(obj)) | |
TakeSnapshot(obj, so.FindProperty(path)); | |
} | |
static void TakeSnapshot(UnityObject obj, SerializedProperty property) { | |
if (obj == null || property == null) return; | |
Scene scene; | |
if (obj is GameObject gameObject) scene = gameObject.scene; | |
else if (obj is Component component) scene = component.gameObject.scene; | |
else return; | |
if (!scene.IsValid() || scene.buildIndex < 0) return; | |
var key = new GlobalObjectReference(obj); | |
if (!snapshots.TryGetValue(key, out var datas)) | |
snapshots[key] = datas = new Dictionary<string, ObjectSnapshotData>(); | |
var data = new ObjectSnapshotData(property); | |
if (!AddTree(datas, property, property.depth)) | |
datas[data.path] = data; | |
} | |
static bool AddTree(Dictionary<string, ObjectSnapshotData> datas, SerializedProperty property, int depth = -1) { | |
if (property == null) return false; | |
bool hasChild = false; | |
while (property.NextVisible(true) && property.depth > depth) { | |
if (!property.hasVisibleChildren) { | |
var data = new ObjectSnapshotData(property); | |
datas[data.path] = data; | |
} | |
hasChild = true; | |
} | |
return hasChild; | |
} | |
static void TakeSnapshot(object entry) { | |
var (objs, property) = ((UnityObject[], SerializedProperty))entry; | |
if (objs == null || property == null) return; | |
foreach (var obj in objs) TakeSnapshot(obj, property); | |
} | |
static bool IsObjectEditable(UnityObject obj) => obj && (obj.hideFlags & (HideFlags.DontSaveInEditor | HideFlags.NotEditable)) != HideFlags.None; | |
static GUID GetContainingSceneGuid(UnityObject obj) { | |
Scene scene; | |
if (obj is GameObject gameObject) scene = gameObject.scene; | |
else if (obj is Component component) scene = component.gameObject.scene; | |
else return default; | |
if (!scene.IsValid() || scene.buildIndex < 0) return default; | |
if (string.IsNullOrEmpty(scene.path)) { | |
scene = SceneManager.GetSceneByBuildIndex(scene.buildIndex); | |
if (!scene.IsValid()) return default; | |
} | |
GUID.TryParse(AssetDatabase.AssetPathToGUID(scene.path), out var guid); | |
return guid; | |
} | |
readonly struct ObjectSnapshotData { | |
public readonly string path; | |
public readonly SerializedPropertyType type; | |
public readonly object value; | |
public ObjectSnapshotData(SerializedProperty property) { | |
path = property.propertyPath; | |
type = property.propertyType; | |
switch (type) { | |
case SerializedPropertyType.Integer: value = property.longValue; break; | |
case SerializedPropertyType.LayerMask: | |
case SerializedPropertyType.ArraySize: | |
case SerializedPropertyType.Character: | |
case SerializedPropertyType.FixedBufferSize: value = property.intValue; break; | |
case SerializedPropertyType.Boolean: value = property.boolValue; break; | |
case SerializedPropertyType.Float: value = property.doubleValue; break; | |
case SerializedPropertyType.String: value = property.stringValue; break; | |
case SerializedPropertyType.Color: value = property.colorValue; break; | |
case SerializedPropertyType.ObjectReference: value = new GlobalObjectReference(property.objectReferenceValue); break; | |
case SerializedPropertyType.Enum: value = property.enumValueIndex; break; | |
case SerializedPropertyType.Vector2: value = property.vector2Value; break; | |
case SerializedPropertyType.Vector3: value = property.vector3Value; break; | |
case SerializedPropertyType.Vector4: value = property.vector4Value; break; | |
case SerializedPropertyType.Rect: value = property.rectValue; break; | |
case SerializedPropertyType.AnimationCurve: value = property.animationCurveValue; break; | |
case SerializedPropertyType.Bounds: value = property.boundsValue; break; | |
case SerializedPropertyType.Gradient: value = gradientValueProperty.GetValue(property); break; | |
case SerializedPropertyType.Quaternion: value = property.quaternionValue; break; | |
case SerializedPropertyType.ExposedReference: value = new GlobalObjectReference(property.exposedReferenceValue); break; | |
case SerializedPropertyType.Vector2Int: value = property.vector2IntValue; break; | |
case SerializedPropertyType.Vector3Int: value = property.vector3IntValue; break; | |
case SerializedPropertyType.RectInt: value = property.rectIntValue; break; | |
case SerializedPropertyType.BoundsInt: value = property.boundsIntValue; break; | |
#if UNITY_2021_1_OR_NEWER | |
case SerializedPropertyType.Hash128: value = property.hash128Value; break; | |
#endif | |
default: value = null; break; | |
} | |
} | |
public bool Restore(SerializedObject so) { | |
var property = so.FindProperty(path); | |
if (property == null || property.propertyType != type) return false; | |
switch (type) { | |
case SerializedPropertyType.Integer: property.longValue = (long)value; break; | |
case SerializedPropertyType.LayerMask: | |
case SerializedPropertyType.ArraySize: | |
case SerializedPropertyType.Character: | |
case SerializedPropertyType.FixedBufferSize: property.intValue = (int)value; break; | |
case SerializedPropertyType.Boolean: property.boolValue = (bool)value; break; | |
case SerializedPropertyType.Float: property.doubleValue = (double)value; break; | |
case SerializedPropertyType.String: property.stringValue = (string)value; break; | |
case SerializedPropertyType.Color: property.colorValue = (Color)value; break; | |
case SerializedPropertyType.ObjectReference: { | |
if (!((GlobalObjectReference)value).TryGet(out var obj)) return false; | |
property.objectReferenceValue = obj; | |
break; | |
} | |
case SerializedPropertyType.Enum: property.enumValueIndex = (int)value; break; | |
case SerializedPropertyType.Vector2: property.vector2Value = (Vector2)value; break; | |
case SerializedPropertyType.Vector3: property.vector3Value = (Vector3)value; break; | |
case SerializedPropertyType.Vector4: property.vector4Value = (Vector4)value; break; | |
case SerializedPropertyType.Rect: property.rectValue = (Rect)value; break; | |
case SerializedPropertyType.AnimationCurve: property.animationCurveValue = (AnimationCurve)value; break; | |
case SerializedPropertyType.Bounds: property.boundsValue = (Bounds)value; break; | |
case SerializedPropertyType.Gradient: gradientValueProperty.SetValue(property, value); break; | |
case SerializedPropertyType.Quaternion: property.quaternionValue = (Quaternion)value; break; | |
case SerializedPropertyType.ExposedReference: { | |
if (!((GlobalObjectReference)value).TryGet(out var obj)) return false; | |
property.exposedReferenceValue = obj; | |
break; | |
} | |
case SerializedPropertyType.Vector2Int: property.vector2IntValue = (Vector2Int)value; break; | |
case SerializedPropertyType.Vector3Int: property.vector3IntValue = (Vector3Int)value; break; | |
case SerializedPropertyType.RectInt: property.rectIntValue = (RectInt)value; break; | |
case SerializedPropertyType.BoundsInt: property.boundsIntValue = (BoundsInt)value; break; | |
#if UNITY_2021_1_OR_NEWER | |
case SerializedPropertyType.Hash128: property.hash128Value = (Hash128)value; break; | |
#endif | |
default: return false; | |
} | |
return true; | |
} | |
public bool OpenSceneIfRequired(out Scene scene) { | |
if (value is GlobalObjectReference reference) return reference.OpenSceneIfRequired(out scene); | |
scene = default; | |
return false; | |
} | |
} | |
readonly struct GlobalObjectReference : IEquatable<GlobalObjectReference> { | |
readonly UnityObject unityObject; | |
readonly int instanceId; | |
readonly GlobalObjectId objectId; | |
readonly GUID sceneGUID; | |
public GlobalObjectReference(UnityObject obj) { | |
unityObject = obj; | |
if (obj) { | |
instanceId = obj.GetInstanceID(); | |
objectId = GlobalObjectId.GetGlobalObjectIdSlow(obj); | |
sceneGUID = GetContainingSceneGuid(obj); | |
} else { | |
instanceId = 0; | |
objectId = default; | |
sceneGUID = default; | |
} | |
} | |
public bool OpenSceneIfRequired(out Scene scene) { | |
if (sceneGUID.Empty()) { | |
scene = default; | |
return false; | |
} | |
var scenePath = AssetDatabase.GUIDToAssetPath(sceneGUID); | |
if (string.IsNullOrEmpty(scenePath)) { | |
scene = default; | |
return false; | |
} | |
scene = SceneManager.GetSceneByPath(scenePath); | |
if (scene.isLoaded) return false; | |
scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); | |
return true; | |
} | |
public bool TryGet(out UnityObject obj) { | |
if (unityObject != null) { | |
obj = unityObject; | |
return true; | |
} | |
obj = EditorUtility.InstanceIDToObject(instanceId); | |
if (obj != null) return true; | |
if (objectId.identifierType == 0) { | |
obj = null; // It is null and we know it | |
return true; | |
} | |
obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(objectId); | |
return obj != null; | |
} | |
public bool Equals(GlobalObjectReference other) => objectId.Equals(other.objectId); | |
public override bool Equals(object obj) => obj is GlobalObjectReference other && Equals(other); | |
public override int GetHashCode() => unchecked( | |
(int)objectId.targetObjectId ^ (int)(objectId.targetObjectId >> 32) ^ | |
(int)objectId.targetPrefabId ^ (int)(objectId.targetPrefabId >> 32) ^ | |
objectId.assetGUID.GetHashCode() ^ objectId.identifierType | |
); | |
public override string ToString() => objectId.ToString(); | |
} | |
class PathComparer : IComparer<string> { | |
public readonly static PathComparer Default = new PathComparer(); | |
public int Compare(string x, string y) { | |
if (string.IsNullOrEmpty(x)) return string.IsNullOrEmpty(y) ? 0 : -1; | |
if (string.IsNullOrEmpty(y)) return 1; | |
int lengthDiff = x.Length - y.Length; | |
if (lengthDiff != 0) return lengthDiff; | |
return string.Compare(x, y, StringComparison.Ordinal); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment