├── .gitignore ├── LICENSE ├── UndoPro ├── SerializableAction │ ├── SerializableType.cs │ ├── SerializableAction.cs │ ├── SerializableMethodInfo.cs │ └── SerializableObject.cs ├── UndoProRecords.cs └── UndoProManager.cs ├── Editor ├── UndoProTestWindow.cs └── SerializableActionTestWindow.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.meta -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Levin Gäher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /UndoPro/SerializableAction/SerializableType.cs: -------------------------------------------------------------------------------- 1 | namespace UndoPro.SerializableActionHelper 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.CompilerServices; 8 | using UnityEngine; 9 | 10 | /// 11 | /// Wrapper for System.Type that handles serialization. 12 | /// Serialized Data contains assembly type name and generic arguments (one level) only. 13 | /// 14 | [System.Serializable] 15 | public class SerializableType 16 | { 17 | public Type _type; 18 | public Type type 19 | { 20 | get 21 | { 22 | if (_type == null) 23 | Deserialize(); 24 | return _type; 25 | } 26 | } 27 | 28 | [SerializeField] 29 | private string typeName; 30 | [SerializeField] 31 | private string[] genericTypes; 32 | 33 | public bool isCompilerGenerated { get { return Attribute.GetCustomAttribute (type, typeof(CompilerGeneratedAttribute), false) != null; } } 34 | 35 | public SerializableType (Type Type) 36 | { 37 | _type = Type; 38 | Serialize(); 39 | } 40 | 41 | #region Serialization 42 | 43 | public void Serialize () 44 | { 45 | if (_type == null) 46 | { 47 | typeName = String.Empty; 48 | genericTypes = null; 49 | return; 50 | } 51 | 52 | if (_type.IsGenericType) 53 | { // Generic type 54 | typeName = _type.GetGenericTypeDefinition ().AssemblyQualifiedName; 55 | genericTypes = _type.GetGenericArguments ().Select ((Type t) => t.AssemblyQualifiedName).ToArray (); 56 | } 57 | else 58 | { // Normal type 59 | typeName = _type.AssemblyQualifiedName; 60 | genericTypes = null; 61 | } 62 | } 63 | 64 | public void Deserialize () 65 | { 66 | if (String.IsNullOrEmpty (typeName)) 67 | return; 68 | 69 | _type = Type.GetType (typeName); 70 | if (_type == null) 71 | throw new Exception ("Could not deserialize type '" + typeName + "'!"); 72 | 73 | if (_type.IsGenericTypeDefinition && genericTypes != null && genericTypes.Length > 0) 74 | { // Generic type 75 | Type[] genArgs = new Type[genericTypes.Length]; 76 | for (int i = 0; i < genericTypes.Length; i++) 77 | genArgs[i] = Type.GetType (genericTypes[i]); 78 | 79 | Type genType = _type.MakeGenericType (genArgs); 80 | if (genType != null) 81 | _type = genType; 82 | else 83 | Debug.LogError ("Could not make generic-type definition '" + typeName + "' generic!"); 84 | } 85 | } 86 | 87 | #endregion 88 | } 89 | } -------------------------------------------------------------------------------- /UndoPro/SerializableAction/SerializableAction.cs: -------------------------------------------------------------------------------- 1 | namespace UndoPro.SerializableActionHelper 2 | { 3 | using UnityEngine; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.IO; 8 | using System.Runtime.Serialization.Formatters.Binary; 9 | using System.Reflection; 10 | 11 | /// 12 | /// Wrapper for a single-cast Action without parameters that handles serialization supporting System- and UnityEngine Objects aswell as anonymous methods to some degree 13 | /// 14 | [Serializable] 15 | public class SerializableAction 16 | { 17 | private Action action; 18 | 19 | [SerializeField] 20 | private SerializableObject serializedTarget; 21 | [SerializeField] 22 | private SerializableMethodInfo serializedMethod; 23 | 24 | // Accessor Properties 25 | public Action Action 26 | { 27 | get 28 | { 29 | if (action == null) 30 | action = DeserializeAction (); 31 | return action; 32 | } 33 | } 34 | public object Target { get { return Action.Target; } } 35 | public MethodInfo Method { get { return Action.Method; } } 36 | 37 | /// 38 | /// Create a new SerializableAction from a non-anonymous action (System or Unity) 39 | /// 40 | public SerializableAction (Action srcAction) 41 | { 42 | if (srcAction.GetInvocationList ().Length > 1) 43 | throw new UnityException ("Cannot create SerializableAction from a multi-cast action!"); 44 | 45 | SerializeAction (srcAction); 46 | } 47 | 48 | #region General 49 | 50 | /// 51 | /// Invoke this serialized action 52 | /// 53 | public void Invoke () 54 | { 55 | if (Action != null) 56 | Action.Invoke(); 57 | } 58 | 59 | /// 60 | /// Returns whether this action is valid 61 | /// 62 | public bool IsValid () 63 | { 64 | if (action == null) 65 | { 66 | try 67 | { 68 | action = DeserializeAction (); 69 | } 70 | catch 71 | { 72 | return false; 73 | } 74 | } 75 | return true; 76 | } 77 | 78 | #endregion 79 | 80 | #region Serialization 81 | 82 | /// 83 | /// Serializes the given action depending on the type (System or Unity) and stores it into this SerializableAction 84 | /// 85 | private void SerializeAction (Action srcAction) 86 | { 87 | action = srcAction; 88 | 89 | //Debug.Log ("Serializing action for method '" + srcAction.Method.Name + "'!"); // TODO: DEBUG REMOVE 90 | 91 | serializedMethod = new SerializableMethodInfo (srcAction.Method); 92 | serializedTarget = new SerializableObject (action.Target); 93 | } 94 | 95 | 96 | /// 97 | /// Deserializes the action depending on the type (System or Unity) and returns it 98 | /// 99 | private Action DeserializeAction () 100 | { 101 | // Target 102 | object target = serializedTarget.Object; 103 | 104 | // Method 105 | MethodInfo method = serializedMethod.methodInfo; 106 | if (method == null) 107 | throw new DataMisalignedException ("Could not deserialize action method '" + serializedMethod.SignatureName + "'!"); 108 | 109 | return Delegate.CreateDelegate (typeof(Action), target, method) as Action; 110 | } 111 | 112 | #endregion 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /UndoPro/SerializableAction/SerializableMethodInfo.cs: -------------------------------------------------------------------------------- 1 | namespace UndoPro.SerializableActionHelper 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using UnityEngine; 8 | using System.Runtime.CompilerServices; 9 | 10 | /// 11 | /// Wrapper for MethodInfo that handles serialization. 12 | /// Stores declaringType, methodName, parameters and flags only and supports generic types (one level for class, two levels for method). 13 | /// 14 | [Serializable] 15 | public class SerializableMethodInfo 16 | { 17 | private MethodInfo _methodInfo; 18 | public MethodInfo methodInfo 19 | { 20 | get 21 | { 22 | if (_methodInfo == null) 23 | Deserialize(); 24 | return _methodInfo; 25 | } 26 | } 27 | 28 | [SerializeField] 29 | private SerializableType declaringType; 30 | [SerializeField] 31 | private string methodName; 32 | [SerializeField] 33 | private List parameters = null; 34 | [SerializeField] 35 | private List genericTypes = null; 36 | [SerializeField] 37 | private int flags = 0; 38 | 39 | // Accessors 40 | public string SignatureName { get { return (((BindingFlags)flags&BindingFlags.Public) != 0? "public" : "private") + (((BindingFlags)flags&BindingFlags.Static) != 0? " static" : "") + " " + methodName; } } 41 | public bool IsAnonymous { get { return Attribute.GetCustomAttribute (methodInfo, typeof(CompilerGeneratedAttribute), false) != null || declaringType.isCompilerGenerated; } } 42 | 43 | public SerializableMethodInfo (MethodInfo MethodInfo) 44 | { 45 | _methodInfo = MethodInfo; 46 | Serialize(); 47 | } 48 | 49 | #region Serialization 50 | 51 | public void Serialize() 52 | { 53 | if (_methodInfo == null) 54 | return; 55 | 56 | declaringType = new SerializableType (_methodInfo.DeclaringType); 57 | methodName = _methodInfo.Name; 58 | 59 | // Flags 60 | if (_methodInfo.IsPrivate) 61 | flags |= (int)BindingFlags.NonPublic; 62 | else 63 | flags |= (int)BindingFlags.Public; 64 | if (_methodInfo.IsStatic) 65 | flags |= (int)BindingFlags.Static; 66 | else 67 | flags |= (int)BindingFlags.Instance; 68 | 69 | // Parameter 70 | ParameterInfo[] param = _methodInfo.GetParameters (); 71 | if (param != null && param.Length > 0) 72 | parameters = param.Select ((ParameterInfo p) => new SerializableType (p.ParameterType)).ToList (); 73 | else 74 | parameters = null; 75 | 76 | // Generic types 77 | if (_methodInfo.IsGenericMethod) 78 | { 79 | methodName = _methodInfo.GetGenericMethodDefinition ().Name; 80 | genericTypes = _methodInfo.GetGenericArguments ().Select ((Type genArgT) => new SerializableType (genArgT)).ToList (); 81 | } 82 | else 83 | genericTypes = null; 84 | } 85 | 86 | public void Deserialize() 87 | { 88 | if (declaringType == null || declaringType.type == null || string.IsNullOrEmpty (methodName)) 89 | return; 90 | 91 | // Parameters 92 | Type[] param; 93 | if (parameters != null && parameters.Count > 0) // With parameters 94 | param = parameters.Select ((SerializableType t) => t.type).ToArray (); 95 | else 96 | param = new Type[0]; 97 | 98 | _methodInfo = declaringType.type.GetMethod (methodName, (BindingFlags)flags, null, param, null); 99 | if (_methodInfo == null) 100 | { // Retry with private flags, because in some compiler generated methods flags will be uncertain (?) which then return public but are private 101 | _methodInfo = declaringType.type.GetMethod (methodName, (BindingFlags)flags | BindingFlags.NonPublic, null, param, null); 102 | if (_methodInfo == null) 103 | throw new Exception ("Could not deserialize '" + SignatureName + "' in declaring type '" + declaringType.type.FullName + "'!"); 104 | } 105 | 106 | if (_methodInfo.IsGenericMethodDefinition && genericTypes != null && genericTypes.Count > 0) 107 | { // Generic Method 108 | Type[] genArgs = genericTypes.Select ((SerializableType t) => t.type).ToArray (); 109 | 110 | MethodInfo genMethod = _methodInfo.MakeGenericMethod (genArgs); 111 | if (genMethod != null) 112 | _methodInfo = genMethod; 113 | else 114 | Debug.LogError ("Could not make generic-method definition '" + methodName + "' generic!"); 115 | } 116 | } 117 | 118 | #endregion 119 | } 120 | } -------------------------------------------------------------------------------- /Editor/UndoProTestWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | using UndoPro; 7 | using UndoPro.SerializableActionHelper; 8 | 9 | public class UndoProTestWindow : EditorWindow 10 | { 11 | [MenuItem ("Window/UndoPro Test")] 12 | private static void Open () 13 | { 14 | EditorWindow.GetWindow ("UndoPro Test"); 15 | } 16 | 17 | private static int maxRecordCount = 10; 18 | private static string newRecordName = ""; 19 | 20 | public void OnEnable () 21 | { 22 | /*UndoProManager.OnUndoPerformed += (string[] performedUndos) => 23 | { 24 | string undoList = "Performed " + performedUndos.Length + " undos: "; 25 | foreach (string performedUndo in performedUndos) 26 | undoList += performedUndo + "; "; 27 | Debug.Log (undoList); 28 | }; 29 | 30 | UndoProManager.OnRedoPerformed += (string[] performedRedos) => 31 | { 32 | string redoList = "Performed " + performedRedos.Length + " redos: "; 33 | foreach (string performedRedo in performedRedos) 34 | redoList += performedRedo + "; "; 35 | Debug.Log (redoList); 36 | }; 37 | 38 | UndoProManager.OnAddUndoRecord += (string[] addedUndoRecords, bool significant) => 39 | { 40 | string addedUndosList = "Added " + addedUndoRecords.Length + " " + (significant? "significant" : "unsignificant") + " undo records: "; 41 | foreach (string newRecord in addedUndoRecords) 42 | addedUndosList += newRecord + "; "; 43 | Debug.Log (addedUndosList); 44 | };*/ 45 | } 46 | 47 | public void OnGUI () 48 | { 49 | Repaint (); 50 | 51 | if (!UndoProManager.enabled) 52 | return; 53 | 54 | UndoProRecords records = UndoProManager.records; 55 | UndoState state = records.undoState; 56 | 57 | GUILayout.BeginHorizontal (); 58 | if (GUILayout.Button ("< Undo " + (state.undoRecords.Count > 0? ("'" + state.undoRecords[state.undoRecords.Count-1] + "'") : "not possible"))) 59 | Undo.PerformUndo (); 60 | else if (GUILayout.Button ("Redo " + (state.redoRecords.Count > 0? ("'" + state.redoRecords[state.redoRecords.Count-1] + "'") : "not possible") + " >")) 61 | Undo.PerformRedo (); 62 | else // For some reason it throws an unimportant but distracting error when not put in the else condition 63 | GUILayout.EndHorizontal (); 64 | 65 | 66 | EditorGUILayout.Space (); 67 | 68 | 69 | GUILayout.BeginHorizontal (); 70 | newRecordName = EditorGUILayout.TextField (newRecordName); 71 | if (GUILayout.Button ("Add Record")) 72 | { 73 | string recordName = newRecordName; // Have to store because actions will still reference original variable 74 | UndoProManager.RecordOperationAndPerform (() => Debug.Log ("Performed custom Action: " + recordName), () => Debug.Log ("Undid custom Action: " + recordName), recordName); 75 | //UndoProManager.RecordOperationAndPerform (() => Debug.Log ("Performed custom Action!"), () => Debug.Log ("Undid custom Action!"), recordName); 76 | } 77 | GUILayout.EndHorizontal (); 78 | 79 | 80 | EditorGUILayout.Space (); 81 | EditorGUILayout.Space (); 82 | 83 | 84 | maxRecordCount = EditorGUILayout.IntSlider ("Max shown records", maxRecordCount, 0, 20); 85 | 86 | EditorGUILayout.Space(); 87 | 88 | GUILayout.BeginHorizontal(); 89 | 90 | GUILayout.BeginVertical(GUILayout.Width(EditorGUIUtility.currentViewWidth/2)); 91 | GUILayout.Label("UNDO:"); 92 | for (int cnt = 0; cnt < Math.Min(state.undoRecords.Count, maxRecordCount); cnt++) 93 | { 94 | int index = state.undoRecords.Count - 1 - cnt; 95 | string rec = state.undoRecords[index]; 96 | List proRecords = records.proUndoStack.FindAll(r => r.relativeStackPos == -cnt); 97 | int matchInd = proRecords.FindIndex(r => r.label == rec); 98 | string proRec = ""; 99 | for (int i = 0; i < proRecords.Count; i++) 100 | if (i != matchInd) proRec += " / " + proRecords[i].label; 101 | GUILayout.Label("Undo " + index + ": " + rec + proRec, 102 | matchInd == -1? EditorStyles.label : EditorStyles.boldLabel, 103 | GUILayout.MaxWidth(EditorGUIUtility.currentViewWidth / 2)); 104 | } 105 | GUILayout.EndVertical(); 106 | 107 | GUILayout.BeginVertical(); 108 | GUILayout.Label("REDO:"); 109 | for (int cnt = 0; cnt < Math.Min(state.redoRecords.Count, maxRecordCount); cnt++) 110 | { 111 | int index = state.redoRecords.Count - 1 - cnt; 112 | string rec = state.redoRecords[index]; 113 | List proRecords = records.proRedoStack.FindAll(r => r.relativeStackPos == cnt+1); 114 | int matchInd = proRecords.FindIndex(r => r.label == rec); 115 | string proRec = ""; 116 | for (int i = 0; i < proRecords.Count; i++) 117 | if (i != matchInd) proRec += " / " + proRecords[i].label; 118 | GUILayout.Label("Redo " + index + ": " + rec + proRec, matchInd == -1 ? EditorStyles.label : EditorStyles.boldLabel); 119 | } 120 | GUILayout.EndVertical(); 121 | 122 | GUILayout.EndHorizontal(); 123 | 124 | EditorGUILayout.Space(); 125 | 126 | GUILayout.Label ("Current Group " + Undo.GetCurrentGroupName () + " : " + Undo.GetCurrentGroup () + " ---"); 127 | } 128 | 129 | 130 | } 131 | -------------------------------------------------------------------------------- /UndoPro/UndoProRecords.cs: -------------------------------------------------------------------------------- 1 | namespace UndoPro 2 | { 3 | using UnityEngine; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | using UndoPro.SerializableActionHelper; 9 | 10 | #if UNITY_5_3_OR_NEWER || UNITY_5_3 11 | using UnityEngine.SceneManagement; 12 | #endif 13 | 14 | /// 15 | /// Class storing the complete custom undo record for a scene, including the custom operations 16 | /// 17 | [ExecuteInEditMode] 18 | public class UndoProRecords : MonoBehaviour 19 | { 20 | #if UNITY_EDITOR 21 | 22 | #if UNITY_5_3_OR_NEWER || UNITY_5_3 23 | private UnityEngine.SceneManagement.Scene scene; 24 | #else 25 | private string scene; 26 | #endif 27 | 28 | // relativeIndex: -inf -> 0 = Undo; 1 -> +inf = Redo 29 | public List undoProRecords = new List (); 30 | 31 | public UndoState undoState; 32 | 33 | public List proUndoStack { get { return undoProRecords.Where ((UndoProRecord record) => record.relativeStackPos <= 0).OrderByDescending ((UndoProRecord record) => record.relativeStackPos).ToList (); } } 34 | public List proRedoStack { get { return undoProRecords.Where ((UndoProRecord record) => record.relativeStackPos > 0).OrderByDescending ((UndoProRecord record) => record.relativeStackPos).ToList (); } } 35 | 36 | private void OnEnable () 37 | { 38 | #if UNITY_5_3_OR_NEWER || UNITY_5_3 39 | scene = SceneManager.GetActiveScene (); 40 | #else 41 | scene = Application.loadedLevelName; 42 | #endif 43 | } 44 | 45 | private void Update () 46 | { 47 | #if UNITY_5_3_OR_NEWER || UNITY_5_3 48 | Scene curScene = SceneManager.GetActiveScene (); 49 | #else 50 | string curScene = Application.loadedLevelName; 51 | #endif 52 | if (curScene != scene) 53 | Destroy (gameObject); 54 | } 55 | 56 | /// 57 | /// Clears the UndoProRecords in the redo-stack 58 | /// 59 | public void ClearRedo () 60 | { 61 | for (int recCnt = 0; recCnt < undoProRecords.Count; recCnt++) 62 | { 63 | if (undoProRecords[recCnt].relativeStackPos > 0) 64 | { 65 | undoProRecords.RemoveAt (recCnt); 66 | recCnt--; 67 | } 68 | } 69 | } 70 | 71 | /// 72 | /// Updates the internal record-stack accounting for the given amount of default undo entries 73 | /// 74 | public void UndoRecordsAdded (int addedRecordsCount) 75 | { 76 | if (undoState.redoRecords.Count == 0) 77 | ClearRedo (); 78 | //Debug.Log ("Shifted records by " + addedRecordsCount + " because of new added undo records!"); 79 | for (int recCnt = 0; recCnt < undoProRecords.Count; recCnt++) 80 | { 81 | UndoProRecord record = undoProRecords[recCnt]; 82 | if (record.relativeStackPos <= 0) 83 | record.relativeStackPos -= addedRecordsCount; 84 | } 85 | } 86 | 87 | /// 88 | /// Updates onternal records to represent the undo/redo operation represented by a opShift (negative: Undo; positive: Redo); 89 | /// Returns the records which are affected by this undo/redo operation (switched from Undo-stack to redo or vice versa) 90 | /// 91 | public List PerformOperationInternal (int opShift, int anomalyAddedCount) 92 | { 93 | if (opShift == 0) 94 | return new List (); 95 | 96 | // Anomaly 97 | bool opDir = opShift <= 0; 98 | //if (anomalyAddedCount < 0) 99 | // opDir = !opDir; 100 | int anomalyShift = (opDir? anomalyAddedCount : -anomalyAddedCount); 101 | 102 | #if UNDO_DEBUG 103 | if (anomalyAddedCount != 0) 104 | { 105 | Debug.Log ("Anomaly of " + anomalyAddedCount + "; OpDir: " + opDir + "; Shift: " + anomalyShift); 106 | } 107 | #endif 108 | 109 | List operatedRecords = new List (); 110 | for (int recCnt = 0; recCnt < undoProRecords.Count; recCnt++) 111 | { 112 | UndoProRecord record = undoProRecords[recCnt]; 113 | int prevInd = record.relativeStackPos; 114 | 115 | record.relativeStackPos -= opShift; 116 | if (anomalyAddedCount != 0 && isUndo(prevInd) != opDir) // Anomaly 117 | record.relativeStackPos += anomalyShift; 118 | 119 | if (isUndo(prevInd) != isUndo(record.relativeStackPos)) // affected by this undo/redo operation 120 | operatedRecords.Add (record); 121 | } 122 | return operatedRecords; 123 | } 124 | 125 | private bool isUndo (int index) 126 | { 127 | return index <= 0; 128 | } 129 | 130 | #endif 131 | } 132 | 133 | #if UNITY_EDITOR 134 | 135 | /// 136 | /// Represents a custom operation record in the custom recording 137 | /// 138 | [Serializable] 139 | public class UndoProRecord 140 | { 141 | public string label; 142 | public int relativeStackPos; 143 | public SerializableAction perform; 144 | public SerializableAction undo; 145 | 146 | public UndoProRecord (Action PerformAction, Action UndoAction, string Label, int stackPosition) 147 | { 148 | label = Label; 149 | relativeStackPos = stackPosition; 150 | perform = new SerializableAction (PerformAction); 151 | undo = new SerializableAction (UndoAction); 152 | } 153 | } 154 | 155 | /// 156 | /// Stores the internal undo state during a frame 157 | /// 158 | [Serializable] 159 | public class UndoState 160 | { 161 | public List redoRecords = new List (); 162 | public List undoRecords = new List (); 163 | } 164 | 165 | #endif 166 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UndoPro 2 | UndoPro is a command-based undo system integrated into Unity's default system. This allows devs to use actions for their undo/redo operations without forcing the user into a new undo-workflow! 3 | 4 | [Forum Thread](http://forum.unity3d.com/threads/wip-open-source-undopro-command-pattern-undo-integration.406131) 5 | 6 | # Install 7 | Depending on how you want to use UndoPro / distribute your tool, you have several options. 8 | 1. Unity Package Manager (recommended)
9 | Go to Unity Package Manager, add by git url, and enter: https://github.com/Seneral/UndoPro.git#release-pkg
10 | Or download from release-pkg branch and install from disk on earlier unity versions
11 | If you distribute your tool based on UndoPro your users would have to manually install UndoPro first, then your tool
12 | But this option is the most compatible with other tools using UndoPro and also can automatically update 13 | 2. Install and distribute UndoPro separately
14 | Second best options, don't modify UndoPro and you'll be able to distribute it fine as long as all involved tools keep the GUIDs intact. But you'll run into problems when Unity breaks UndoPro and a hot fix is needed to make it work, but you or other tools still distribute older UndoPro versions alongside your tool 15 | 3. Embed UndoPro into your project
16 | Absolutely make sure to change all GUIDs, and wrap all UndoPro code into separate namespaces, and also modify the assembly definitions, else it will conflict with other tools.
17 | Not recommended, as you will solely be responsible for making sure the UndoPro version you distribute is up to date.
18 | Only do this if you absolutely need a specific version of UndoPro or you want to modify it before using it in your tool. 19 | 20 | ## Features 21 | - Extended Callbacks for Undo: Seperate Undo/Redo with record names, OnAddUndoRecord, ... 22 | - API for creating command-based undo records 23 | - Handles most anonymous actions without problems, even using the context! 24 | 25 | ## How it works 26 | Unity provides only very limited information about the undo system: 27 | - The ID of the current record, but it's not unique, rather than steadily increasing :/ 28 | - The current record/group name (not unique) 29 | - With reflection, the complete Undo/Redo stack only by name (not unique) 30 | - UndoRedoPerformed callback 31 | 32 | Internally, UndoPro creates a dummy record in the default system when the API for adding a command-based record is called. This is then tracked using the available callback and the commands are executed when they switch from redo- undo stack or the other way around respectively. 33 | A big hurden was the behaviour of the default Undo system: it is nearly unpredictable! Records may duplicate in certain conditions when undone/redone, or vanish. It is very hard, but a requirement to make a solid tracking algorithm. Obviously the addition of new records has also to be detected. 34 | 35 | When tracking has been done, a shift value for both undo and redo stack seperately is calculated (remember, records might vanish or duplicate during undo/redo!). The internal records are then updated accordingly and the records to undo/redo are outlined. 36 | 37 | On the way of all this, the additional callbacks OnUndoPerformed/OnRedoPerformed/OnAddUndoRecord are called. 38 | 39 | ## Serialization of Command-based records 40 | Serialization is also a big problem, as actions, and even worse anonymous actions, are hard to serialize. UndoPro maintains a hidden, temporary GameObject in the current scene which holds all custom records that need to be serialized. The serialization is achieved by a few wrapper classes that intelligently handle every combination for Actions, Objects, Methods, etc. 41 | 42 | This system can even be used generally! 43 | -> Supports all serializable objects (of both UnityEngine.Object and System.Object) and unserializable objects partially (one layer serializable member serialization), all other objects get defaulted 44 | -> Supports even most anonymous actions (no unserializable found yet)! You can fully use the context and reference nearly all local variables (conditions outlined above apply)! 45 | 46 | ## Problems 47 | This system does seem reliable but I do not claim it is completely bullet-proof! 48 | The worst case that can happen when it messes up the tracking though is that your records are offset by a small amount (actions are executed one or two records after/before). There is currently no such situation known fortunately but I take no liability for any failures! 49 | If you manage to break it please notify me about it and provide me with information of what you've done (through issues) and I try to fix this:) 50 | 51 | ## Installation 52 | Simply put the UndoPro folder somewhere in your project and you're good to go! Even though it doesn't have to be in the Editor folder it does not mean you can use it at runtime though! Functionality requiring the Editor API are excluded at runtime by preprocessor checks. 53 | In the Editor folder on the other hand you find two useful windows to test the functionality of 1. the Undo system itself and 2. the action serialization system. Along with these windows you can debug the system easily yourself to see how it works by uncommenting #define UNDO_DEBUG in UndoProManager! 54 | In order to just use the action serialization system for your own project just copy the folder UndoPro/SerializableAction along with the license of course! 55 | 56 | ## API 57 | The API for developers is very simple. 58 | Add the UndoPro dependency and then use UndoProManager to interact with the system. Important elements: 59 | - RecordOpterationAndPerform - Accepts the actions for undo/redo along with a label and adds them as a record. Additionally it will perform the redo action. 60 | - RecordOperation (2 overloads) - Accepts the actions for undo/redo along with a label and adds them as a record. Use if you already manually performed the action 61 | - OnUndoPerformed Callback - called when undo was performed. Passes all undone records, which can be multiple if they were grouped. 62 | - OnRedoPerformed Callback - called when redo was performed. Passes all redone records, which can be multiple if they were grouped. 63 | - OnAddUndoRecord Callback - called when a record was added, both from UndoPro and the default system. Passes it's name and whether it is considered 'important' (Some records clear the redo stack when added, some, like the Selection Change, do not). This could also relate to whether the record added is included in the last group of record or if it opened a new group (uncertain). 64 | - EnableUndoPro/DisableUndoPro - toggle the state of the system. Not recommended to disable as it will simply create a hole in the record and mess up previous records! 65 | 66 | Recommendation: If you calculated something or performed any operation with an intermediate result, it is very easy to set this up provided the result is serializable. Instead of recalculating in the undo/redo actions just set the previous serializable result in the undo action and the new one in the redo action. Done! 67 | 68 | ## Author and License 69 | This extension was created by [Seneral](https://www.seneral.dev/) and is published under the MIT license (further specified in LICENSE.md) 70 | -------------------------------------------------------------------------------- /Editor/SerializableActionTestWindow.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | using UndoPro.SerializableActionHelper; 7 | 8 | public class SerializableActionTestWindow : EditorWindow 9 | { 10 | [MenuItem ("Window/SerializableAction Test")] 11 | private static void Open () 12 | { 13 | EditorWindow.GetWindow ("SerializableAction Test"); 14 | } 15 | 16 | #region Test Methods: Unity 17 | 18 | private void UnityTargettedNormal () 19 | { 20 | Debug.Log ("Unity-Targetted Normal executed!"); 21 | } 22 | 23 | private void UnityTargettedGenericMethodNormal () 24 | { 25 | Debug.Log ("Unity-Targetted GenericMethod<" + typeof(T).Name + "> Normal executed!"); 26 | } 27 | 28 | private static void UnityTargettedStatic () 29 | { 30 | Debug.Log ("Unity-Targetted Static executed!"); 31 | } 32 | 33 | private static void UnityTargettedGenericMethodStatic () 34 | { 35 | Debug.Log ("Unity-Targetted GenericMethod<" + typeof(T).Name + "> Static executed!"); 36 | } 37 | 38 | private static Action getUnityTargettedAnonymous () 39 | { 40 | return new Action (() => Debug.Log ("Unity-Targetted Anonymous executed!")); 41 | } 42 | 43 | private static Action getUnityTargettedAnonymous () 44 | { 45 | return new Action (() => Debug.Log ("Unity-Targetted GenericMethod<" + typeof(T).Name + "> Anonymous executed!")); 46 | } 47 | 48 | #endregion 49 | 50 | #region Test Methods: System 51 | 52 | [Serializable] 53 | public class SystemClass : System.Object 54 | { 55 | public void SystemTargettedNormal () 56 | { 57 | Debug.Log ("System-Targetted Normal executed!"); 58 | } 59 | 60 | public void SystemTargettedGenericMethodNormal () 61 | { 62 | Debug.Log ("System-Targetted GenericMethod<" + typeof(T).Name + "> Normal executed!"); 63 | } 64 | 65 | public static void SystemTargettedStatic () 66 | { 67 | Debug.Log ("System-Targetted Static executed!"); 68 | } 69 | 70 | public static void SystemTargettedGenericMethodStatic () 71 | { 72 | Debug.Log ("System-Targetted GenericMethod<" + typeof(T).Name + "> Static executed!"); 73 | } 74 | 75 | public Action getSystemTargettedAnonymous () 76 | { 77 | return new Action (() => Debug.Log ("System-Targetted Anonymous executed!")); 78 | } 79 | 80 | public Action getSystemTargettedGenericAnonymous () 81 | { 82 | return new Action (() => Debug.Log ("System-Targetted GenericMethod<" + typeof(T).Name + "> Anonymous executed!")); 83 | } 84 | } 85 | 86 | #endregion 87 | 88 | #region Test Methods: System (Generic Class) 89 | 90 | [Serializable] 91 | public class SystemGenericTypeClass : System.Object 92 | { 93 | public void SystemTargettedGenericNormal () 94 | { 95 | Debug.Log ("System-Targetted GenericClass<" + typeof(T).Name + "> Normal executed!"); 96 | } 97 | 98 | public static void SystemTargettedGenericStatic () 99 | { 100 | Debug.Log ("System-Targetted GenericClass<" + typeof(T).Name + "> Static executed!"); 101 | } 102 | 103 | public Action getSystemTargettedGenericAnonymous () 104 | { 105 | return new Action (() => Debug.Log ("System-Targetted GenericClass<" + typeof(T).Name + "> Anonymous executed!")); 106 | } 107 | } 108 | 109 | #endregion 110 | 111 | public SystemClass systemClass = new SystemClass (); 112 | public SystemGenericTypeClass systemGenericTypeClass = new SystemGenericTypeClass (); 113 | 114 | public int testInt = 62; 115 | 116 | public SerializableAction unityStaticAction; 117 | public SerializableAction unityGenericMethodStaticAction; 118 | 119 | public SerializableAction systemStaticAction; 120 | public SerializableAction systemGenericStaticAction; 121 | public SerializableAction systemGenericMethodStaticAction; 122 | 123 | public SerializableAction unityNormalAction; 124 | public SerializableAction unityGenericMethodNormalAction; 125 | 126 | public SerializableAction systemNormalAction; 127 | public SerializableAction systemGenericNormalAction; 128 | public SerializableAction systemGenericMethodNormalAction; 129 | 130 | public SerializableAction unityAnonymousAction; 131 | public SerializableAction unityGenericMethodAnonymousAction; 132 | public SerializableAction unityLocalVarAnonymousAction; 133 | public SerializableAction unityClassVarAnonymousAction; 134 | 135 | public SerializableAction systemAnonymousAction; 136 | public SerializableAction systemGenericAnonymousAction; 137 | public SerializableAction systemGenericMethodAnonymousAction; 138 | 139 | 140 | public void OnGUI () 141 | { 142 | // ----- 143 | 144 | EditorGUILayout.Space (); 145 | 146 | GUILayout.Label ("STATIC"); 147 | 148 | EditorGUILayout.Space (); 149 | 150 | ActionGUI ("Unity-Targetted Static", ref unityStaticAction, SerializableActionTestWindow.UnityTargettedStatic); 151 | 152 | EditorGUILayout.Space (); 153 | 154 | ActionGUI ("Unity-Targetted GenericMethod Static", ref unityGenericMethodStaticAction, SerializableActionTestWindow.UnityTargettedGenericMethodStatic); 155 | 156 | EditorGUILayout.Space (); 157 | 158 | ActionGUI ("System-Targetted Static", ref systemStaticAction, SystemClass.SystemTargettedStatic); 159 | 160 | EditorGUILayout.Space (); 161 | 162 | ActionGUI ("System-Targetted GenericClass Static", ref systemGenericStaticAction, SystemGenericTypeClass.SystemTargettedGenericStatic); 163 | 164 | EditorGUILayout.Space (); 165 | 166 | ActionGUI ("System-Targetted GenericMethod Static", ref systemGenericMethodStaticAction, SystemClass.SystemTargettedGenericMethodStatic); 167 | 168 | // ----- 169 | 170 | EditorGUILayout.Space (); 171 | 172 | GUILayout.Label ("NORMAL"); 173 | 174 | EditorGUILayout.Space (); 175 | 176 | ActionGUI ("Unity-Targetted Normal", ref unityNormalAction, this.UnityTargettedNormal); 177 | 178 | EditorGUILayout.Space (); 179 | 180 | ActionGUI ("Unity-Targetted GenericMethod Normal", ref unityGenericMethodNormalAction, this.UnityTargettedGenericMethodNormal); 181 | 182 | EditorGUILayout.Space (); 183 | 184 | ActionGUI ("System-Targetted Normal", ref systemNormalAction, systemClass.SystemTargettedNormal); 185 | 186 | EditorGUILayout.Space (); 187 | 188 | ActionGUI ("System-Targetted GenericClass Normal", ref systemGenericNormalAction, systemGenericTypeClass.SystemTargettedGenericNormal); 189 | 190 | EditorGUILayout.Space (); 191 | 192 | ActionGUI ("System-Targetted GenericMethod Normal", ref systemGenericMethodNormalAction, systemClass.SystemTargettedGenericMethodNormal); 193 | 194 | // ----- 195 | 196 | EditorGUILayout.Space (); 197 | 198 | GUILayout.Label ("ANONYMOUS"); 199 | 200 | EditorGUILayout.Space (); 201 | 202 | ActionGUI ("Unity-Targetted Anyonymous", ref unityAnonymousAction, getUnityTargettedAnonymous ()); 203 | 204 | EditorGUILayout.Space (); 205 | 206 | testInt = EditorGUILayout.IntSlider ("Test Int", testInt, 0, 100); 207 | ActionGUI ("Unity-Targetted ClassVar Anyonymous", ref unityClassVarAnonymousAction, () => Debug.Log ("Unity-Targetted ClassVar Anyonymous executed: " + testInt)); 208 | int localInt = testInt; 209 | ActionGUI ("Unity-Targetted LocalVar Anyonymous", ref unityLocalVarAnonymousAction, () => Debug.Log ("Unity-Targetted LocalVar Anyonymous executed: " + localInt)); 210 | 211 | EditorGUILayout.Space (); 212 | 213 | ActionGUI ("Unity-Targetted GenericMethod Anyonymous", ref unityGenericMethodAnonymousAction, getUnityTargettedAnonymous ()); 214 | 215 | EditorGUILayout.Space (); 216 | 217 | ActionGUI ("System-Targetted Anyonymous", ref systemAnonymousAction, systemClass.getSystemTargettedAnonymous ()); 218 | 219 | EditorGUILayout.Space (); 220 | 221 | ActionGUI ("System-Targetted GenericClass Anyonymous", ref systemGenericAnonymousAction, systemGenericTypeClass.getSystemTargettedGenericAnonymous ()); 222 | 223 | EditorGUILayout.Space (); 224 | 225 | ActionGUI ("System-Targetted GenericMethod Anyonymous", ref systemGenericMethodAnonymousAction, systemClass.getSystemTargettedGenericAnonymous ()); 226 | 227 | // ----- 228 | 229 | Repaint (); 230 | } 231 | 232 | private void ActionGUI (string label, ref SerializableAction serializedAction, Action action) 233 | { 234 | GUILayout.Label (label + " SerializableAction"); 235 | if (serializedAction != null && !serializedAction.IsValid ()) 236 | serializedAction = null; 237 | GUILayout.BeginHorizontal (); 238 | if (GUILayout.Button ("Create (" + (serializedAction != null) + ")")) 239 | { 240 | serializedAction = new SerializableAction (action); 241 | serializedAction.Invoke (); 242 | } 243 | if (GUILayout.Button ("Delete")) 244 | { 245 | serializedAction = null; 246 | } 247 | if (GUILayout.Button ("Invoke")) 248 | { 249 | if (serializedAction != null) 250 | serializedAction.Invoke (); 251 | else 252 | Debug.LogError (label + " Action is null!"); 253 | } 254 | GUILayout.EndHorizontal (); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /UndoPro/SerializableAction/SerializableObject.cs: -------------------------------------------------------------------------------- 1 | namespace UndoPro.SerializableActionHelper 2 | { 3 | using UnityEngine; 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Runtime.Serialization.Formatters.Binary; 9 | using System.Reflection; 10 | 11 | /// 12 | /// Wrapper for an arbitrary object that handles basic serialization, both System.Object, UnityEngine.Object, and even basic unserializable types (the same way, but one-level only, unserializable members will be default or null if previously null) 13 | /// 14 | [Serializable] 15 | public class SerializableObject : SerializableObjectOneLevel 16 | { 17 | [SerializeField] 18 | private List manuallySerializedMembers; 19 | [SerializeField] 20 | protected List collectionObjects; 21 | 22 | /// 23 | /// Create a new SerializableObject from an arbitrary object 24 | /// 25 | public SerializableObject(object srcObject) : base(srcObject) { } 26 | /// 27 | /// Create a new SerializableObject from an arbitrary object with the specified name 28 | /// 29 | public SerializableObject(object srcObject, string name) : base(srcObject, name) { } 30 | 31 | #region Serialization 32 | 33 | /// 34 | /// Serializes the given object and stores it into this SerializableObject 35 | /// 36 | protected override void Serialize() 37 | { 38 | if (isNullObject = _object == null) 39 | return; 40 | 41 | base.Serialize(); // Serialized normally 42 | 43 | if (_object.GetType().IsGenericType && 44 | typeof(ICollection<>).MakeGenericType(_object.GetType().GetGenericArguments()).IsAssignableFrom(_object.GetType())) 45 | { 46 | IEnumerable collection = _object as IEnumerable; 47 | collectionObjects = new List(); 48 | foreach (object obj in collection) 49 | collectionObjects.Add(new SerializableObjectTwoLevel(obj)); 50 | } 51 | else if (typeof(UnityEngine.Object).IsAssignableFrom(_object.GetType())) { } 52 | else if (_object.GetType().IsSerializable) { } 53 | else 54 | { // Object is unserializable so it will later be recreated from the type, now serialize the serializable field values of the object 55 | FieldInfo[] fields = objectType.type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 56 | manuallySerializedMembers = new List(); 57 | foreach (FieldInfo field in fields) 58 | manuallySerializedMembers.Add(new SerializableObjectTwoLevel(field.GetValue(_object), field.Name)); 59 | //manuallySerializedMembers = fields.Select ((FieldInfo field) => new SerializableObjectOneLevel (field.GetValue (_object), field.Name)).ToList (); 60 | } 61 | } 62 | 63 | /// 64 | /// Deserializes this SerializableObject 65 | /// 66 | protected override void Deserialize() 67 | { 68 | if (isNullObject) 69 | return; 70 | 71 | base.Deserialize(); // Deserialize normally 72 | 73 | Type type = objectType.type; 74 | if (type.IsGenericType && 75 | typeof(ICollection<>).MakeGenericType(type.GetGenericArguments()).IsAssignableFrom(type)) 76 | { 77 | if (collectionObjects != null && collectionObjects.Count > 0) 78 | { // Add deserialized objects to collection 79 | MethodInfo add = type.GetMethod("Add"); 80 | foreach (SerializableObjectTwoLevel obj in collectionObjects) 81 | add.Invoke(_object, new object[] { obj.Object }); 82 | } 83 | } 84 | else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) 85 | _object = unityObject; 86 | else if (type.IsSerializable) 87 | _object = DeserializeFromString(serializedSystemObject); 88 | else if (manuallySerializedMembers != null && manuallySerializedMembers.Count > 0) 89 | { // This object is an unserializable type, and previously the object was recreated from that type 90 | // Now, restore the serialized field values of the object 91 | FieldInfo[] fields = objectType.type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 92 | if (fields.Length != manuallySerializedMembers.Count) 93 | Debug.LogError("Field length and serialized member length doesn't match (" + fields.Length + ":" + manuallySerializedMembers.Count + ") for object " + objectType.type.Name + "!"); 94 | foreach (FieldInfo field in fields) 95 | { 96 | SerializableObjectTwoLevel matchObj = manuallySerializedMembers.Find((SerializableObjectTwoLevel obj) => obj.Name == field.Name); 97 | if (matchObj != null) 98 | { 99 | object obj = null; 100 | if (matchObj.Object == null) { } 101 | else if (!field.FieldType.IsAssignableFrom(matchObj.Object.GetType())) 102 | Debug.LogWarning("Deserialized object type " + matchObj.Object.GetType().Name + " is incompatible to field type " + field.FieldType.Name + "!"); 103 | else 104 | obj = matchObj.Object; 105 | field.SetValue(Object, obj); 106 | } 107 | else 108 | Debug.LogWarning("Couldn't find a matching serialized field for '" + (field.IsPublic ? "public" : "private") + (field.IsStatic ? " static" : "") + " " + field.FieldType.FullName + "'!"); 109 | } 110 | } 111 | } 112 | 113 | #endregion 114 | } 115 | 116 | /// 117 | /// Wrapper for an arbitrary object that handles basic serialization, both System.Object, UnityEngine.Object, and even basic unserializable types (the same way, but one-level only, unserializable members will be default or null if previously null) 118 | /// 119 | [Serializable] 120 | public class SerializableObjectTwoLevel : SerializableObjectOneLevel 121 | { 122 | [SerializeField] 123 | private List manuallySerializedMembers; 124 | [SerializeField] 125 | protected List collectionObjects; 126 | 127 | /// 128 | /// Create a new SerializableObject from an arbitrary object 129 | /// 130 | public SerializableObjectTwoLevel (object srcObject) : base (srcObject) { } 131 | /// 132 | /// Create a new SerializableObject from an arbitrary object with the specified name 133 | /// 134 | public SerializableObjectTwoLevel (object srcObject, string name) : base(srcObject, name) { } 135 | 136 | #region Serialization 137 | 138 | /// 139 | /// Serializes the given object and stores it into this SerializableObject 140 | /// 141 | protected override void Serialize () 142 | { 143 | if (isNullObject = _object == null) 144 | return; 145 | 146 | base.Serialize (); // Serialized normally 147 | 148 | if (_object.GetType().IsGenericType && 149 | typeof(ICollection<>).MakeGenericType(_object.GetType().GetGenericArguments()).IsAssignableFrom(_object.GetType())) 150 | { 151 | IEnumerable collection = _object as IEnumerable; 152 | collectionObjects = new List(); 153 | foreach (object obj in collection) 154 | collectionObjects.Add(new SerializableObjectOneLevel(obj)); 155 | } 156 | else if (typeof(UnityEngine.Object).IsAssignableFrom(_object.GetType())) { } 157 | else if (_object.GetType().IsSerializable) { } 158 | else 159 | { // Object is unserializable so it will later be recreated from the type, now serialize the serializable field values of the object 160 | FieldInfo[] fields = objectType.type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 161 | manuallySerializedMembers = new List(); 162 | foreach (FieldInfo field in fields) 163 | manuallySerializedMembers.Add(new SerializableObjectOneLevel(field.GetValue(_object), field.Name)); 164 | //manuallySerializedMembers = fields.Select ((FieldInfo field) => new SerializableObjectOneLevel (field.GetValue (_object), field.Name)).ToList (); 165 | } 166 | } 167 | 168 | /// 169 | /// Deserializes this SerializableObject 170 | /// 171 | protected override void Deserialize () 172 | { 173 | if (isNullObject) 174 | return; 175 | 176 | base.Deserialize (); // Deserialize normally 177 | 178 | Type type = objectType.type; 179 | if (type.IsGenericType && 180 | typeof(ICollection<>).MakeGenericType(type.GetGenericArguments()).IsAssignableFrom(type)) 181 | { 182 | if (collectionObjects != null && collectionObjects.Count > 0) 183 | { // Add deserialized objects to collection 184 | MethodInfo add = type.GetMethod("Add"); 185 | foreach (SerializableObjectOneLevel obj in collectionObjects) 186 | add.Invoke(_object, new object[] { obj.Object }); 187 | } 188 | } 189 | else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) 190 | _object = unityObject; 191 | else if (type.IsSerializable) 192 | _object = DeserializeFromString(serializedSystemObject); 193 | else if (manuallySerializedMembers != null && manuallySerializedMembers.Count > 0) 194 | { // This object is an unserializable type, and previously the object was recreated from that type 195 | // Now, restore the serialized field values of the object 196 | FieldInfo[] fields = objectType.type.GetFields (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); 197 | if (fields.Length != manuallySerializedMembers.Count) 198 | Debug.LogError ("Field length and serialized member length doesn't match (" + fields.Length + ":" + manuallySerializedMembers.Count + ") for object " + objectType.type.Name + "!"); 199 | foreach (FieldInfo field in fields) 200 | { 201 | SerializableObjectOneLevel matchObj = manuallySerializedMembers.Find ((SerializableObjectOneLevel obj) => obj.Name == field.Name); 202 | if (matchObj != null) 203 | { 204 | object obj = null; 205 | if (matchObj.Object == null) { } 206 | else if (!field.FieldType.IsAssignableFrom(matchObj.Object.GetType())) 207 | Debug.LogWarning("Deserialized object type " + matchObj.Object.GetType().Name + " is incompatible to field type " + field.FieldType.Name + "!"); 208 | else 209 | obj = matchObj.Object; 210 | field.SetValue(Object, obj); 211 | } 212 | else 213 | Debug.LogWarning("Couldn't find a matching serialized field for '" + (field.IsPublic ? "public" : "private") + (field.IsStatic ? " static" : "") + " " + field.FieldType.FullName + "'!"); 214 | } 215 | } 216 | } 217 | 218 | #endregion 219 | } 220 | 221 | /// 222 | /// Wrapper for an arbitrary object that handles basic serialization, both System.Object, UnityEngine.Object; unserializable types will be default or null if previously null; 223 | /// NO RECOMMENDED TO USE, it is primarily built to support SerializableObject! 224 | /// 225 | [Serializable] 226 | public class SerializableObjectOneLevel 227 | { 228 | [SerializeField] 229 | public string Name; // Just to identify this object 230 | protected object _object; 231 | public object Object 232 | { 233 | get 234 | { 235 | if (_object == null) 236 | Deserialize(); 237 | return _object; 238 | } 239 | } 240 | 241 | // Serialized Data 242 | [SerializeField] 243 | protected bool isNullObject; 244 | [SerializeField] 245 | protected SerializableType objectType; 246 | [SerializeField] 247 | protected UnityEngine.Object unityObject; 248 | [SerializeField] 249 | protected string serializedSystemObject; 250 | 251 | public SerializableObjectOneLevel (object srcObject) 252 | { 253 | _object = srcObject; 254 | Serialize(); 255 | } 256 | 257 | public SerializableObjectOneLevel(object srcObject, string name) 258 | { 259 | _object = srcObject; 260 | Name = name; 261 | Serialize(); 262 | } 263 | 264 | #region Serialization 265 | 266 | /// 267 | /// Serializes the given object and stores it into this SerializableObject 268 | /// 269 | protected virtual void Serialize () 270 | { 271 | if (isNullObject = _object == null) 272 | return; 273 | 274 | unityObject = null; 275 | serializedSystemObject = String.Empty; 276 | objectType = new SerializableType (_object.GetType ()); 277 | 278 | if (_object.GetType().IsGenericType && 279 | typeof(ICollection<>).MakeGenericType(_object.GetType().GetGenericArguments()).IsAssignableFrom(_object.GetType())) 280 | { // If levels are free to serialize, then they will get serialized, if not, then not 281 | } 282 | else if (typeof(UnityEngine.Object).IsAssignableFrom(_object.GetType())) 283 | { 284 | unityObject = (UnityEngine.Object)_object; 285 | } 286 | else if (_object.GetType().IsSerializable) 287 | { 288 | serializedSystemObject = SerializeToString(_object); 289 | if (serializedSystemObject == null) 290 | Debug.LogWarning("Failed to serialize field name " + Name + "!"); 291 | } 292 | // else default object (and even serializable members) will be restored from the type 293 | } 294 | 295 | /// 296 | /// Deserializes this SerializableObject 297 | /// 298 | protected virtual void Deserialize () 299 | { 300 | _object = null; 301 | if (isNullObject) 302 | return; 303 | if (objectType.type == null) 304 | throw new Exception("Could not deserialize object as it's type could no be deserialized!"); 305 | Type type = objectType.type; 306 | 307 | if (type.IsGenericType && 308 | typeof(ICollection<>).MakeGenericType(type.GetGenericArguments()).IsAssignableFrom(type)) 309 | { // Collection type, if still more levels free, members will be serialized, if not, then not 310 | _object = Activator.CreateInstance(type); 311 | } 312 | else if (typeof(UnityEngine.Object).IsAssignableFrom(type)) 313 | _object = unityObject; 314 | else if (type.IsSerializable) 315 | _object = DeserializeFromString(serializedSystemObject); 316 | else 317 | { // Unserializable type, it will be recreated from the type (and even it's serializable members) 318 | _object = Activator.CreateInstance(type); 319 | } 320 | 321 | // Not always critical. Can happen if GC or Unity deleted those references 322 | // if (_object == null) 323 | // Debug.LogWarning ("Could not deserialize object of type '" + type.Name + "'!"); 324 | } 325 | 326 | #endregion 327 | 328 | #region Embedded Util 329 | 330 | /// 331 | /// Serializes 'value' to a string, using BinaryFormatter 332 | /// 333 | protected static string SerializeToString (T value) 334 | { 335 | if (value == null) 336 | return null; 337 | try 338 | { 339 | using (MemoryStream stream = new MemoryStream()) 340 | { 341 | new BinaryFormatter().Serialize(stream, value); 342 | stream.Flush(); 343 | return Convert.ToBase64String(stream.ToArray()); 344 | } 345 | } 346 | catch (System.Runtime.Serialization.SerializationException) 347 | { 348 | Debug.LogWarning("Failed to serialize " + value.GetType().ToString()); 349 | return null; 350 | } 351 | } 352 | 353 | /// 354 | /// Deserializes an object of type T from the string 'data' 355 | /// 356 | protected static T DeserializeFromString (string data) 357 | { 358 | if (String.IsNullOrEmpty (data)) 359 | return default(T); 360 | byte[] bytes = Convert.FromBase64String (data); 361 | using (MemoryStream stream = new MemoryStream(bytes)) 362 | { 363 | return (T)new BinaryFormatter().Deserialize (stream); 364 | } 365 | } 366 | 367 | #endregion 368 | } 369 | } -------------------------------------------------------------------------------- /UndoPro/UndoProManager.cs: -------------------------------------------------------------------------------- 1 | //#define UNDO_DEBUG 2 | 3 | namespace UndoPro 4 | { 5 | #if UNITY_EDITOR 6 | 7 | using UnityEngine; 8 | using UnityEditor; 9 | using System; 10 | using System.Reflection; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | 14 | /// 15 | /// Manager for UndoPro, enabling an action-based undo workflow integrated into the default Unity Undo system and providing detailed callbacks for the Undo system 16 | /// 17 | [InitializeOnLoad] 18 | public static class UndoProManager 19 | { 20 | public static bool enabled; 21 | 22 | private static UnityEngine.Object dummyObject; 23 | 24 | private static Action getRecordsInternalDelegate; 25 | 26 | public static UndoProRecords records; 27 | 28 | public static Action OnUndoPerformed; 29 | public static Action OnRedoPerformed; 30 | 31 | public static Action OnAddUndoRecord; 32 | 33 | #region General 34 | 35 | private static void ResetUndoPro() 36 | { 37 | CreateRecords(); 38 | } 39 | 40 | private static void ToggleUndoPro () 41 | { 42 | if (!enabled) 43 | { 44 | EnableUndoPro (); 45 | if (SceneView.lastActiveSceneView != null) 46 | { 47 | SceneView.lastActiveSceneView.ShowNotification (new GUIContent ("Undo Pro Enabled!")); 48 | SceneView.lastActiveSceneView.Repaint (); 49 | } 50 | } 51 | else 52 | { 53 | DisableUndoPro (); 54 | if (SceneView.lastActiveSceneView != null) 55 | { 56 | SceneView.lastActiveSceneView.ShowNotification (new GUIContent ("Undo Pro Disabled!")); 57 | SceneView.lastActiveSceneView.Repaint (); 58 | } 59 | } 60 | } 61 | 62 | static UndoProManager () 63 | { 64 | EnableUndoPro (); 65 | } 66 | 67 | public static void EnableUndoPro () 68 | { 69 | enabled = true; 70 | 71 | // Assure it is subscribed to all necessary events for undo/redo recognition 72 | Undo.undoRedoPerformed -= UndoRedoPerformed; 73 | Undo.undoRedoPerformed += UndoRedoPerformed; 74 | EditorApplication.update -= Update; 75 | EditorApplication.update += Update; 76 | #if UNITY_2017_2_OR_NEWER 77 | EditorApplication.playModeStateChanged -= PlayModeStateChanged; 78 | EditorApplication.playModeStateChanged += PlayModeStateChanged; 79 | #else 80 | EditorApplication.playmodeStateChanged -= PlaymodeStateChanged; 81 | EditorApplication.playmodeStateChanged += PlaymodeStateChanged; 82 | #endif 83 | 84 | // Fetch Reflection members for Undo interaction 85 | Assembly UnityEditorAsssembly = Assembly.GetAssembly (typeof(UnityEditor.Editor)); 86 | Type undoType = UnityEditorAsssembly.GetType ("UnityEditor.Undo"); 87 | 88 | #if UNITY_2021_2_OR_NEWER 89 | MethodInfo getRecordsInternal = undoType.GetMethod ("GetTimelineRecordsInternal", BindingFlags.NonPublic | BindingFlags.Static); 90 | #else 91 | MethodInfo getRecordsInternal = undoType.GetMethod ("GetRecordsInternal", BindingFlags.NonPublic | BindingFlags.Static); 92 | #endif 93 | getRecordsInternalDelegate = (Action)Delegate.CreateDelegate (typeof(Action), getRecordsInternal); 94 | 95 | // Create dummy object 96 | if (dummyObject == null) 97 | dummyObject = new Texture2D (8, 8); 98 | 99 | // Setup default undo state and record 100 | AssureRecords (); 101 | } 102 | 103 | private static void AssureRecords () 104 | { 105 | if (records == null) 106 | { 107 | records = GameObject.FindObjectOfType (); 108 | if (records == null) 109 | CreateRecords (); 110 | #if UNDO_DEBUG 111 | else Debug.Log ("Found undo records in scene!"); 112 | #endif 113 | } 114 | if (records.undoState == null) 115 | { 116 | #if UNDO_DEBUG 117 | Debug.Log ("UndoState recreated!"); 118 | #endif 119 | records.undoState = FetchUndoState (); 120 | } 121 | } 122 | 123 | private static void CreateRecords () 124 | { 125 | #if UNDO_DEBUG 126 | Debug.Log ("Creating scene undo records!"); 127 | #endif 128 | 129 | if (records != null) 130 | UnityEngine.Object.DestroyImmediate (records.gameObject); 131 | 132 | GameObject recordsGO = new GameObject ("UndoProRecords"); 133 | #if !UNDO_DEBUG 134 | recordsGO.hideFlags = HideFlags.HideInHierarchy | HideFlags.HideInInspector; 135 | #endif 136 | records = recordsGO.AddComponent (); 137 | } 138 | 139 | public static void DisableUndoPro () 140 | { 141 | enabled = false; 142 | 143 | // Unsubscribe from every event 144 | Undo.undoRedoPerformed -= UndoRedoPerformed; 145 | EditorApplication.update -= Update; 146 | #if UNITY_2017_2_OR_NEWER 147 | EditorApplication.playModeStateChanged -= PlayModeStateChanged; 148 | #else 149 | EditorApplication.playmodeStateChanged -= PlaymodeStateChanged; 150 | #endif 151 | 152 | // Discard now unused objects 153 | dummyObject = null; 154 | getRecordsInternalDelegate = null; 155 | records = null; 156 | } 157 | 158 | #endregion 159 | 160 | #region Custom Undo Recording 161 | 162 | private static bool inRecordStack = false; 163 | private static bool firstInRecordStack = true; 164 | 165 | /// 166 | /// Begin merging multiple singular undo operations into one group that get's treated as one 167 | /// 168 | public static void BeginRecordStack() 169 | { 170 | inRecordStack = true; 171 | firstInRecordStack = true; 172 | Undo.IncrementCurrentGroup(); 173 | } 174 | 175 | /// 176 | /// End merging multiple singular undo operations into one group that get's treated as one 177 | /// 178 | public static void EndRecordStack() 179 | { 180 | Undo.FlushUndoRecordObjects(); 181 | Undo.IncrementCurrentGroup(); 182 | inRecordStack = false; 183 | } 184 | 185 | /// 186 | /// Records a custom operation with given label and actions and executes the operation (perform) 187 | /// 188 | public static void RecordOperationAndPerform (Action perform, Action undo, string label, bool mergeBefore = false, bool mergeAfter = false) 189 | { 190 | RecordOperation (new UndoProRecord (perform, undo, label, 0), mergeBefore, mergeAfter); 191 | if (perform != null) 192 | perform.Invoke (); 193 | } 194 | 195 | /// 196 | /// Records a custom operation with given label and actions 197 | /// 198 | public static void RecordOperation (Action perform, Action undo, string label, bool mergeBefore = false, bool mergeAfter = false) 199 | { 200 | RecordOperation (new UndoProRecord (perform, undo, label, 0), mergeBefore, mergeAfter); 201 | } 202 | 203 | /// 204 | /// Records the given operation 205 | /// 206 | private static void RecordOperation (UndoProRecord operation, bool mergeBefore = false, bool mergeAfter = false) 207 | { 208 | // First, make sure the internal records representation is updated 209 | UpdateUndoRecords (); 210 | 211 | // Make sure this record isn't included in the previous group 212 | if (!mergeBefore && !inRecordStack) 213 | Undo.IncrementCurrentGroup (); 214 | 215 | // Create a dummy record with the given label 216 | if (dummyObject == null) 217 | dummyObject = new Texture2D (8, 8); 218 | Undo.RegisterCompleteObjectUndo (dummyObject, operation.label); 219 | 220 | // Make sure future undo records are not included into this group 221 | if (!mergeAfter && !inRecordStack) 222 | { 223 | Undo.FlushUndoRecordObjects(); 224 | Undo.IncrementCurrentGroup(); 225 | } 226 | 227 | // Now get the new Undo state 228 | records.undoState = FetchUndoState (); 229 | 230 | // Record operation internally 231 | if (!inRecordStack || firstInRecordStack) 232 | records.UndoRecordsAdded (1); 233 | firstInRecordStack = false; 234 | records.undoProRecords.Add (operation); 235 | 236 | if (OnAddUndoRecord != null) 237 | OnAddUndoRecord.Invoke (new string[] { operation.label }, true); 238 | } 239 | 240 | #endregion 241 | 242 | #region Undo/Redo Tracking 243 | 244 | private static bool lastFrameUndoRedoPerformed = false; 245 | 246 | /// 247 | /// Checks if new undo records were added 248 | /// 249 | private static void Update () 250 | { 251 | if (!lastFrameUndoRedoPerformed) 252 | { // Only handle the case of possible undo addition, but not when an undo or redo was performed 253 | UpdateUndoRecords (); 254 | } 255 | lastFrameUndoRedoPerformed = false; 256 | } 257 | 258 | #if UNITY_2017_2_OR_NEWER 259 | private static void PlayModeStateChanged(PlayModeStateChange change) 260 | { 261 | if (change == PlayModeStateChange.EnteredEditMode) 262 | UpdateUndoRecords(); 263 | } 264 | #else 265 | private static void PlaymodeStateChanged() 266 | { 267 | UpdateUndoRecords(); 268 | } 269 | #endif 270 | 271 | /// 272 | /// Check the current undoState for any added undo records and updates the internal records accordingly 273 | /// 274 | private static void UpdateUndoRecords () 275 | { 276 | AssureRecords (); 277 | 278 | // Get new UndoState 279 | UndoState prevState = records.undoState; 280 | records.undoState = FetchUndoState (); 281 | UndoState newState = records.undoState; 282 | 283 | // Detect additions to the record through comparision of the old and the new UndoState 284 | if (prevState.undoRecords.Count == newState.undoRecords.Count) 285 | return; // No undo record was added for sure 286 | 287 | // Fetch new undo records 288 | int addedUndoCount = newState.undoRecords.Count-prevState.undoRecords.Count; 289 | if (addedUndoCount < 0) 290 | { // This happens only when the undo was erased, for example after switching the scene 291 | #if UNDO_DEBUG 292 | string[] undosRemoved = prevState.undoRecords.GetRange(prevState.undoRecords.Count + addedUndoCount, -addedUndoCount).ToArray(); 293 | string undoRemLog = "" + (-addedUndoCount) + " undo records removed: "; 294 | for (int undoCnt = 0; undoCnt < undosRemoved.Length; undoCnt++) 295 | undoRemLog += undosRemoved[undoCnt] + "; "; 296 | Debug.Log(undoRemLog); 297 | #endif 298 | 299 | if (newState.undoRecords.Count != 0) 300 | { // Attempt to salvage the undo records that are left 301 | records.UndoRecordsAdded(addedUndoCount); 302 | records.ClearRedo (); 303 | Debug.LogWarning ("Cleared Redo because some undos were removed!"); 304 | } 305 | else 306 | CreateRecords (); 307 | return; 308 | } 309 | 310 | // Update internals 311 | records.UndoRecordsAdded (addedUndoCount); 312 | 313 | // Callback 314 | string[] undosAdded = newState.undoRecords.GetRange (newState.undoRecords.Count-addedUndoCount, addedUndoCount).ToArray (); 315 | if (OnAddUndoRecord != null) 316 | OnAddUndoRecord.Invoke (undosAdded, newState.redoRecords.Count == 0); 317 | 318 | #if UNDO_DEBUG 319 | // Debug added undo records 320 | string undoLog = undosAdded.Length + " undo records added: "; 321 | for (int undoCnt = 0; undoCnt < undosAdded.Length; undoCnt++) 322 | undoLog += undosAdded[undoCnt] + "; "; 323 | Debug.Log (undoLog); 324 | #endif 325 | } 326 | 327 | private static UndoState FetchUndoState () 328 | { 329 | UndoState newUndoState = new UndoState (); 330 | getRecordsInternalDelegate.Invoke (newUndoState.undoRecords, newUndoState.redoRecords); 331 | return newUndoState; 332 | } 333 | 334 | #endregion 335 | 336 | #region UndoPro Record tracking 337 | 338 | /// 339 | /// Callback recognising the type of record, calling the apropriate callback and handling undo pro records 340 | /// 341 | private static void UndoRedoPerformed () 342 | { 343 | lastFrameUndoRedoPerformed = true; 344 | AssureRecords (); 345 | 346 | // Get new UndoState 347 | UndoState prevState = records.undoState; 348 | UndoState newState = records.undoState = FetchUndoState (); 349 | 350 | // Detect undo/redo 351 | int addedRecordCount; 352 | int change = DetectStateChange (prevState, newState, out addedRecordCount); 353 | if (change == 0) // Nothing happend; Only possible if Undo/Redo stack was empty 354 | return; 355 | 356 | List operatedRecords = records.PerformOperationInternal (change, addedRecordCount); 357 | 358 | if (change < 0) 359 | { // UNDO operation 360 | foreach (UndoProRecord undoRecord in operatedRecords) 361 | { // Invoke undo operations 362 | if (undoRecord.undo != null) 363 | undoRecord.undo.Invoke (); 364 | } 365 | // Callback for whole group 366 | if (OnUndoPerformed != null) 367 | OnUndoPerformed.Invoke (operatedRecords.Select ((UndoProRecord record) => record.label).ToArray ()); 368 | } 369 | else 370 | { // REDO operation 371 | foreach (UndoProRecord redoRecord in operatedRecords) 372 | { // Invoke redo operations 373 | if (redoRecord.perform != null) 374 | redoRecord.perform.Invoke (); 375 | } 376 | // Callback for whole group 377 | if (OnRedoPerformed != null) 378 | OnRedoPerformed.Invoke (operatedRecords.Select ((UndoProRecord record) => record.label).ToArray ()); 379 | } 380 | } 381 | 382 | /// 383 | /// Detects an UndoState change as either an undo or redo operation 384 | /// A positive return value indicates a redo operation, a negative an undo operation; 385 | /// If it is 0, then the adressed stack (undo/redo) was empty 386 | /// The absolute value is the number of records that were adressed, means the size of the group 387 | /// It is possible that the group size changed due to an anomaly, so count of records added by the anomaly is put into addedRecordsCount 388 | /// 389 | private static int DetectStateChange (UndoState prevState, UndoState nextState, out int addedRecordsCount) 390 | { 391 | addedRecordsCount = 0; 392 | 393 | int prevUndoCount = prevState.undoRecords.Count, prevRedoCount = prevState.redoRecords.Count; 394 | int nextUndoCount = nextState.undoRecords.Count, nextRedoCount = nextState.redoRecords.Count; 395 | int undoChange = nextUndoCount-prevUndoCount, redoChange = nextRedoCount-prevRedoCount; 396 | 397 | // Check if the action is undo or redo 398 | bool undoAction = undoChange < 0, redoAction = redoChange < 0; 399 | if ((!redoAction && prevUndoCount == 0) || (!undoAction && prevRedoCount == 0)) // Tried to undo/redo with an empty record stack 400 | return 0; 401 | if (!undoAction && !redoAction) 402 | throw new Exception ("Detected neither redo nor undo operation!"); 403 | int recordChange = undoAction? Math.Abs (undoChange) : Math.Abs (redoChange); 404 | 405 | #if UNDO_DEBUG 406 | Debug.Log ("Detected " + (undoAction? "UNDO" : "REDO") + " of " + recordChange + " initial records!"); 407 | #endif 408 | 409 | if (redoChange != -undoChange) 410 | { // This anomaly happens only for records that trigger other undo/redo operations 411 | // -> only known case: Reparent unselected object in hierarchy, each iteration (undo/redo) of the issued record a 'Parenting' record gets added ontop 412 | addedRecordsCount = undoAction? (Math.Abs (redoChange)-Math.Abs (undoChange)) : (Math.Abs (undoChange)-Math.Abs (redoChange)); 413 | #if UNDO_DEBUG 414 | Debug.LogWarning ("Due to an anomaly a difference of " + addedRecordsCount + " records was created during " + (undoAction? "undo" : "redo") + 415 | " where undo change was " + undoChange + " and redo change " + redoChange); 416 | #endif 417 | } 418 | 419 | return (undoAction? -recordChange : recordChange); // Return the count of initially changed records 420 | } 421 | 422 | #endregion 423 | } 424 | 425 | #endif 426 | } --------------------------------------------------------------------------------