Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active August 11, 2023 04:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/def0f46444b1135695ea02b8268d6c04 to your computer and use it in GitHub Desktop.
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.
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