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