using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.Assertions; namespace SaveDuringPlay { /// A collection of tools for finding objects static class ObjectTreeUtil { /// /// Get the full name of an object, travelling up the transform parents to the root. /// public static string GetFullName(GameObject current) { if (current == null) return ""; if (current.transform.parent == null) return "/" + current.name; return GetFullName(current.transform.parent.gameObject) + "/" + current.name; } /// /// Will find the named object, active or inactive, from the full path. /// public static GameObject FindObjectFromFullName(string fullName, GameObject[] roots) { if (string.IsNullOrEmpty(fullName) || roots == null) return null; string[] path = fullName.Split('/'); if (path.Length < 2) // skip leading '/' return null; Transform root = null; for (int i = 0; root == null && i < roots.Length; ++i) if (roots[i].name == path[1]) root = roots[i].transform; if (root == null) return null; for (int i = 2; i < path.Length; ++i) // skip root { bool found = false; for (int c = 0; c < root.childCount; ++c) { Transform child = root.GetChild(c); if (child.name == path[i]) { found = true; root = child; break; } } if (!found) return null; } return root.gameObject; } /// Finds all the root objects in a scene, active or not public static GameObject[] FindAllRootObjectsInScene() { return UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects(); } /// /// This finds all the behaviours in scene, active or inactive, excluding prefabs /// public static T[] FindAllBehavioursInScene() where T : MonoBehaviour { List objectsInScene = new List(); foreach (T b in Resources.FindObjectsOfTypeAll()) { if (b == null) continue; // object was deleted GameObject go = b.gameObject; if (go.hideFlags == HideFlags.NotEditable || go.hideFlags == HideFlags.HideAndDontSave) continue; if (EditorUtility.IsPersistent(go.transform.root.gameObject)) continue; objectsInScene.Add(b); } return objectsInScene.ToArray(); } } class GameObjectFieldScanner { /// /// Called for each leaf field. Return value should be true if action was taken. /// It will be propagated back to the caller. /// public OnLeafFieldDelegate OnLeafField; public delegate bool OnLeafFieldDelegate(string fullName, Type type, ref object value); /// /// Called for each field node, if and only if OnLeafField() for it or one /// of its leaves returned true. /// public OnFieldValueChangedDelegate OnFieldValueChanged; public delegate bool OnFieldValueChangedDelegate( string fullName, FieldInfo fieldInfo, object fieldOwner, object value); /// /// Called for each field, to test whether to proceed with scanning it. Return true to scan. /// public FilterFieldDelegate FilterField; public delegate bool FilterFieldDelegate(string fullName, FieldInfo fieldInfo); /// /// Called for each behaviour, to test whether to proceed with scanning it. Return true to scan. /// public FilterComponentDelegate FilterComponent; public delegate bool FilterComponentDelegate(MonoBehaviour b); /// /// The leafmost UnityEngine.Object /// public UnityEngine.Object LeafObject { get; private set; } /// /// Which fields will be scanned /// const BindingFlags kBindingFlags = BindingFlags.Public | BindingFlags.Instance; bool ScanFields(string fullName, Type type, ref object obj) { bool doneSomething = false; // Check if it's a complex type bool isLeaf = true; if (obj != null && !typeof(Component).IsAssignableFrom(type) && !typeof(ScriptableObject).IsAssignableFrom(type) && !typeof(GameObject).IsAssignableFrom(type)) { if (type.IsArray) { isLeaf = false; var array = obj as Array; object arrayLength = array.Length; if (OnLeafField != null && OnLeafField( fullName + ".Length", arrayLength.GetType(), ref arrayLength)) { Array newArray = Array.CreateInstance( array.GetType().GetElementType(), Convert.ToInt32(arrayLength)); Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newArray.Length)); array = newArray; doneSomething = true; } for (int i = 0; i < array.Length; ++i) { object element = array.GetValue(i); if (ScanFields(fullName + "[" + i + "]", array.GetType().GetElementType(), ref element)) { array.SetValue(element, i); doneSomething = true; } } if (doneSomething) obj = array; } else if (typeof(IList).IsAssignableFrom(type)) { isLeaf = false; var list = obj as IList; object length = list.Count; // restore list size if (OnLeafField != null && OnLeafField( fullName + ".Length", length.GetType(), ref length)) { var newLength = (int)length; var currentLength = list.Count; for (int i = 0; i < currentLength - newLength; ++i) { list.RemoveAt(currentLength - i - 1); // make list shorter if needed } for (int i = 0; i < newLength - currentLength; ++i) { list.Add(GetValue(type.GetGenericArguments()[0])); // make list longer if needed } doneSomething = true; } // restore values for (int i = 0; i < list.Count; ++i) { var c = list[i]; if (ScanFields(fullName + "[" + i + "]", c.GetType(), ref c)) { list[i] = c; doneSomething = true; } } if (doneSomething) obj = list; } else { // Check if it's a complex type FieldInfo[] fields = obj.GetType().GetFields(kBindingFlags); if (fields.Length > 0) { isLeaf = false; for (int i = 0; i < fields.Length; ++i) { string name = fullName + "." + fields[i].Name; if (FilterField == null || FilterField(name, fields[i])) { object fieldValue = fields[i].GetValue(obj); if (ScanFields(name, fields[i].FieldType, ref fieldValue)) { doneSomething = true; if (OnFieldValueChanged != null) OnFieldValueChanged(name, fields[i], obj, fieldValue); } } } } } } // If it's a leaf field then call the leaf handler if (isLeaf && OnLeafField != null) if (OnLeafField(fullName, type, ref obj)) doneSomething = true; return doneSomething; } static object GetValue(Type type) { Assert.IsNotNull(type); return Activator.CreateInstance(type); } bool ScanFields(string fullName, MonoBehaviour b) { bool doneSomething = false; LeafObject = b; FieldInfo[] fields = b.GetType().GetFields(kBindingFlags); if (fields.Length > 0) { for (int i = 0; i < fields.Length; ++i) { string name = fullName + "." + fields[i].Name; if (FilterField == null || FilterField(name, fields[i])) { object fieldValue = fields[i].GetValue(b); if (ScanFields(name, fields[i].FieldType, ref fieldValue)) doneSomething = true; // If leaf action was taken, propagate it up to the parent node if (doneSomething && OnFieldValueChanged != null) OnFieldValueChanged(fullName, fields[i], b, fieldValue); } } } return doneSomething; } /// /// Recursively scan [SaveDuringPlay] MonoBehaviours of a GameObject and its children. /// For each leaf field found, call the OnFieldValue delegate. /// public bool ScanFields(GameObject go, string prefix = null) { bool doneSomething = false; if (prefix == null) prefix = ""; else if (prefix.Length > 0) prefix += "."; MonoBehaviour[] components = go.GetComponents(); for (int i = 0; i < components.Length; ++i) { MonoBehaviour c = components[i]; if (c == null || (FilterComponent != null && !FilterComponent(c))) continue; if (ScanFields(prefix + c.GetType().FullName + i, c)) doneSomething = true; } return doneSomething; } }; /// /// Using reflection, this class scans a GameObject (and optionally its children) /// and records all the field settings. This only works for "nice" field settings /// within MonoBehaviours. Changes to the behaviour stack made between saving /// and restoring will fool this class. /// class ObjectStateSaver { string mObjectFullPath; Dictionary mValues = new Dictionary(); /// /// Recursively collect all the field values in the MonoBehaviours /// owned by this object and its descendants. The values are stored /// in an internal dictionary. /// public void CollectFieldValues(GameObject go) { mObjectFullPath = ObjectTreeUtil.GetFullName(go); GameObjectFieldScanner scanner = new GameObjectFieldScanner(); scanner.FilterField = FilterField; scanner.FilterComponent = HasSaveDuringPlay; scanner.OnLeafField = (string fullName, Type type, ref object value) => { // Save the value in the dictionary mValues[fullName] = StringFromLeafObject(value); //Debug.Log(mObjectFullPath + "." + fullName + " = " + mValues[fullName]); return false; }; scanner.ScanFields(go); } public GameObject FindSavedGameObject(GameObject[] roots) { return ObjectTreeUtil.FindObjectFromFullName(mObjectFullPath, roots); } /// /// Recursively scan the MonoBehaviours of a GameObject and its children. /// For each field found, look up its value in the internal dictionary. /// If it's present and its value in the dictionary differs from the actual /// value in the game object, Set the GameObject's value using the value /// recorded in the dictionary. /// public bool PutFieldValues(GameObject go, GameObject[] roots) { GameObjectFieldScanner scanner = new GameObjectFieldScanner(); scanner.FilterField = FilterField; scanner.FilterComponent = HasSaveDuringPlay; scanner.OnLeafField = (string fullName, Type type, ref object value) => { // Lookup the value in the dictionary if (mValues.TryGetValue(fullName, out string savedValue) && StringFromLeafObject(value) != savedValue) { //Debug.Log("Put " + mObjectFullPath + "." + fullName + " = " + mValues[fullName]); value = LeafObjectFromString(type, mValues[fullName].Trim(), roots); return true; // changed } return false; }; scanner.OnFieldValueChanged = (fullName, fieldInfo, fieldOwner, value) => { fieldInfo.SetValue(fieldOwner, value); if (PrefabUtility.GetPrefabInstanceStatus(go) != PrefabInstanceStatus.NotAPrefab) PrefabUtility.RecordPrefabInstancePropertyModifications(scanner.LeafObject); return true; }; return scanner.ScanFields(go); } /// Ignore fields marked with the [NoSaveDuringPlay] attribute static bool FilterField(string fullName, FieldInfo fieldInfo) { var attrs = fieldInfo.GetCustomAttributes(false); foreach (var attr in attrs) if (attr.GetType().Name.Equals("NoSaveDuringPlayAttribute")) return false; return true; } /// Only process components with the [SaveDuringPlay] attribute public static bool HasSaveDuringPlay(MonoBehaviour b) { var attrs = b.GetType().GetCustomAttributes(true); foreach (var attr in attrs) if (attr.GetType().Name.Equals("SaveDuringPlayAttribute")) return true; return false; } /// /// Parse a string to generate an object. /// Only very limited primitive object types are supported. /// Enums, Vectors and most other structures are automatically supported, /// because the reflection system breaks them down into their primitive components. /// You can add more support here, as needed. /// static object LeafObjectFromString(Type type, string value, GameObject[] roots) { if (type == typeof(Single)) return float.Parse(value); if (type == typeof(Double)) return double.Parse(value); if (type == typeof(Boolean)) return Boolean.Parse(value); if (type == typeof(string)) return value; if (type == typeof(Int32)) return Int32.Parse(value); if (type == typeof(UInt32)) return UInt32.Parse(value); if (typeof(Component).IsAssignableFrom(type)) { // Try to find the named game object GameObject go = ObjectTreeUtil.FindObjectFromFullName(value, roots); return (go != null) ? go.GetComponent(type) : null; } if (typeof(GameObject).IsAssignableFrom(type)) { // Try to find the named game object return GameObject.Find(value); } if (typeof(ScriptableObject).IsAssignableFrom(type)) { return AssetDatabase.LoadAssetAtPath(value, type); } return null; } static string StringFromLeafObject(object obj) { if (obj == null) return string.Empty; if (typeof(Component).IsAssignableFrom(obj.GetType())) { Component c = (Component)obj; if (c == null) // Component overrides the == operator, so we have to check return string.Empty; return ObjectTreeUtil.GetFullName(c.gameObject); } if (typeof(GameObject).IsAssignableFrom(obj.GetType())) { GameObject go = (GameObject)obj; if (go == null) // GameObject overrides the == operator, so we have to check return string.Empty; return ObjectTreeUtil.GetFullName(go); } if (typeof(ScriptableObject).IsAssignableFrom(obj.GetType())) { return AssetDatabase.GetAssetPath(obj as ScriptableObject); } return obj.ToString(); } }; /// /// For all registered object types, record their state when exiting Play Mode, /// and restore that state to the objects in the scene. This is a very limited /// implementation which has not been rigorously tested with many objects types. /// It's quite possible that not everything will be saved. /// /// This class is expected to become obsolete when Unity implements this functionality /// in a more general way. /// /// To use this class, /// drop this script into your project, and add the [SaveDuringPlay] attribute to your class. /// /// Note: if you want some specific field in your class NOT to be saved during play, /// add a property attribute whose class name contains the string "NoSaveDuringPlay" /// and the field will not be saved. /// [InitializeOnLoad] public class SaveDuringPlay { /// Editor preferences key for SaveDuringPlay enabled public static string kEnabledKey = "SaveDuringPlay_Enabled"; /// Enabled status for SaveDuringPlay. /// This is a global setting, saved in Editor Prefs public static bool Enabled { get => EditorPrefs.GetBool(kEnabledKey, false); set { if (value != Enabled) { EditorPrefs.SetBool(kEnabledKey, value); } } } static SaveDuringPlay() { // Install our callbacks #if UNITY_2017_2_OR_NEWER EditorApplication.playModeStateChanged += OnPlayStateChanged; #else EditorApplication.update += OnEditorUpdate; EditorApplication.playmodeStateChanged += OnPlayStateChanged; #endif } #if UNITY_2017_2_OR_NEWER static void OnPlayStateChanged(PlayModeStateChange pmsc) { if (Enabled) { switch (pmsc) { // If exiting playmode, collect the state of all interesting objects case PlayModeStateChange.ExitingPlayMode: SaveAllInterestingStates(); break; case PlayModeStateChange.EnteredEditMode when sSavedStates != null: RestoreAllInterestingStates(); break; } } } #else static void OnPlayStateChanged() { // If exiting playmode, collect the state of all interesting objects if (Enabled) { if (!EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPlaying) SaveAllInterestingStates(); } } static float sWaitStartTime = 0; static void OnEditorUpdate() { if (Enabled && sSavedStates != null && !Application.isPlaying) { // Wait a bit for things to settle before applying the saved state const float WaitTime = 1f; // GML todo: is there a better way to do this? float time = Time.realtimeSinceStartup; if (sWaitStartTime == 0) sWaitStartTime = time; else if (time - sWaitStartTime > WaitTime) { RestoreAllInterestingStates(); sWaitStartTime = 0; } } } #endif /// /// If you need to get notified before state is collected for hotsave, this is the place /// public static OnHotSaveDelegate OnHotSave; /// Delegate for HotSave notification public delegate void OnHotSaveDelegate(); /// Collect all relevant objects, active or not static HashSet FindInterestingObjects() { var objects = new HashSet(); MonoBehaviour[] everything = ObjectTreeUtil.FindAllBehavioursInScene(); foreach (var b in everything) { if (!objects.Contains(b.gameObject) && ObjectStateSaver.HasSaveDuringPlay(b)) { //Debug.Log("Found " + ObjectTreeUtil.GetFullName(b.gameObject) + " for hot-save"); objects.Add(b.gameObject); } } return objects; } static List sSavedStates = null; static void SaveAllInterestingStates() { //Debug.Log("Exiting play mode: Saving state for all interesting objects"); if (OnHotSave != null) OnHotSave(); sSavedStates = new List(); var objects = FindInterestingObjects(); foreach (var obj in objects) { var saver = new ObjectStateSaver(); saver.CollectFieldValues(obj); sSavedStates.Add(saver); } if (sSavedStates.Count == 0) sSavedStates = null; } static void RestoreAllInterestingStates() { //Debug.Log("Updating state for all interesting objects"); bool dirty = false; GameObject[] roots = ObjectTreeUtil.FindAllRootObjectsInScene(); foreach (ObjectStateSaver saver in sSavedStates) { GameObject go = saver.FindSavedGameObject(roots); if (go != null) { Undo.RegisterFullObjectHierarchyUndo(go, "SaveDuringPlay"); if (saver.PutFieldValues(go, roots)) { //Debug.Log("SaveDuringPlay: updated settings of " + saver.ObjetFullPath); EditorUtility.SetDirty(go); dirty = true; } } } if (dirty) UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); sSavedStates = null; } } }