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;
}
}
}