├── .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