using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; namespace SaveDuringPlay { /// A collection of tools for finding objects public 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 (fullName == null || fullName.Length == 0 || 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()) { 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); /// /// Which fields will be scanned /// public BindingFlags bindingFlags = 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 && !type.IsSubclassOf(typeof(Component)) && !type.IsSubclassOf(typeof(GameObject))) { // Is it an array? if (type.IsArray) { isLeaf = false; Array 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 { // Check if it's a complex type FieldInfo[] fields = obj.GetType().GetFields(bindingFlags); 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; } public bool ScanFields(string fullName, MonoBehaviour b) { bool doneSomething = false; FieldInfo[] fields = b.GetType().GetFields(bindingFlags); 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 the 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 && 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.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); } public string ObjetFullPath { get { return mObjectFullPath; } } /// /// 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.OnLeafField = (string fullName, Type type, ref object value) => { // Lookup the value in the dictionary string savedValue; if (mValues.TryGetValue(fullName, out savedValue) && StringFromLeafObject(value) != savedValue) { //Debug.Log(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); return true; }; return scanner.ScanFields(go); } /// Ignore fields marked with the [NoSaveDuringPlay] attribute bool FilterField(string fullName, FieldInfo fieldInfo) { var attrs = fieldInfo.GetCustomAttributes(false); foreach (var attr in attrs) if (attr.GetType().Name.Contains("NoSaveDuringPlay")) return false; return true; } /// /// 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 (type.IsSubclassOf(typeof(Component))) { // Try to find the named game object GameObject go = ObjectTreeUtil.FindObjectFromFullName(value, roots); return (go != null) ? go.GetComponent(type) : null; } if (type.IsSubclassOf(typeof(GameObject))) { // Try to find the named game object return GameObject.Find(value); } return null; } static string StringFromLeafObject(object obj) { if (obj == null) return string.Empty; if (obj.GetType().IsSubclassOf(typeof(Component))) { 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 (obj.GetType().IsSubclassOf(typeof(GameObject))) { GameObject go = (GameObject)obj; if (go == null) // GameObject overrides the == operator, so we have to check return string.Empty; return ObjectTreeUtil.GetFullName(go); } 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 { public static string kEnabledKey = "SaveDuringPlay_Enabled"; public static bool Enabled { get { return 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) { // If exiting playmode, collect the state of all interesting objects if (pmsc == PlayModeStateChange.ExitingPlayMode) SaveAllInterestingStates(); else if (pmsc == PlayModeStateChange.EnteredEditMode && sSavedStates != null) RestoreAllInterestingStates(); } } #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; public delegate void OnHotSaveDelegate(); /// Collect all relevant objects, active or not static Transform[] FindInterestingObjects() { List objects = new List(); MonoBehaviour[] everything = ObjectTreeUtil.FindAllBehavioursInScene(); foreach (var b in everything) { var attrs = b.GetType().GetCustomAttributes(true); foreach (var attr in attrs) { if (attr.GetType().Name.Contains("SaveDuringPlay")) { //Debug.Log("Found " + ObjectTreeUtil.GetFullName(b.gameObject) + " for hot-save"); objects.Add(b.transform); break; } } } return objects.ToArray(); } static List sSavedStates = null; static GameObject sSaveStatesGameObject; static void SaveAllInterestingStates() { //Debug.Log("Exiting play mode: Saving state for all interesting objects"); if (OnHotSave != null) OnHotSave(); sSavedStates = new List(); Transform[] objects = FindInterestingObjects(); foreach (Transform obj in objects) { ObjectStateSaver saver = new ObjectStateSaver(); saver.CollectFieldValues(obj.gameObject); 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; } } }