SaveDuringPlay.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Reflection;
  4. using UnityEditor;
  5. using UnityEngine;
  6. namespace SaveDuringPlay
  7. {
  8. /// <summary>A collection of tools for finding objects</summary>
  9. static class ObjectTreeUtil
  10. {
  11. /// <summary>
  12. /// Get the full name of an object, travelling up the transform parents to the root.
  13. /// </summary>
  14. public static string GetFullName(GameObject current)
  15. {
  16. if (current == null)
  17. return "";
  18. if (current.transform.parent == null)
  19. return "/" + current.name;
  20. return GetFullName(current.transform.parent.gameObject) + "/" + current.name;
  21. }
  22. /// <summary>
  23. /// Will find the named object, active or inactive, from the full path.
  24. /// </summary>
  25. public static GameObject FindObjectFromFullName(string fullName, GameObject[] roots)
  26. {
  27. if (string.IsNullOrEmpty(fullName) || roots == null)
  28. return null;
  29. string[] path = fullName.Split('/');
  30. if (path.Length < 2) // skip leading '/'
  31. return null;
  32. Transform root = null;
  33. for (int i = 0; root == null && i < roots.Length; ++i)
  34. if (roots[i].name == path[1])
  35. root = roots[i].transform;
  36. if (root == null)
  37. return null;
  38. for (int i = 2; i < path.Length; ++i) // skip root
  39. {
  40. bool found = false;
  41. for (int c = 0; c < root.childCount; ++c)
  42. {
  43. Transform child = root.GetChild(c);
  44. if (child.name == path[i])
  45. {
  46. found = true;
  47. root = child;
  48. break;
  49. }
  50. }
  51. if (!found)
  52. return null;
  53. }
  54. return root.gameObject;
  55. }
  56. /// <summary>Finds all the root objects in a scene, active or not</summary>
  57. public static GameObject[] FindAllRootObjectsInScene()
  58. {
  59. return UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects();
  60. }
  61. /// <summary>
  62. /// This finds all the behaviours in scene, active or inactive, excluding prefabs
  63. /// </summary>
  64. public static T[] FindAllBehavioursInScene<T>() where T : MonoBehaviour
  65. {
  66. List<T> objectsInScene = new List<T>();
  67. foreach (T b in Resources.FindObjectsOfTypeAll<T>())
  68. {
  69. if (b == null)
  70. continue; // object was deleted
  71. GameObject go = b.gameObject;
  72. if (go.hideFlags == HideFlags.NotEditable || go.hideFlags == HideFlags.HideAndDontSave)
  73. continue;
  74. if (EditorUtility.IsPersistent(go.transform.root.gameObject))
  75. continue;
  76. objectsInScene.Add(b);
  77. }
  78. return objectsInScene.ToArray();
  79. }
  80. }
  81. class GameObjectFieldScanner
  82. {
  83. /// <summary>
  84. /// Called for each leaf field. Return value should be true if action was taken.
  85. /// It will be propagated back to the caller.
  86. /// </summary>
  87. public OnLeafFieldDelegate OnLeafField;
  88. public delegate bool OnLeafFieldDelegate(string fullName, Type type, ref object value);
  89. /// <summary>
  90. /// Called for each field node, if and only if OnLeafField() for it or one
  91. /// of its leaves returned true.
  92. /// </summary>
  93. public OnFieldValueChangedDelegate OnFieldValueChanged;
  94. public delegate bool OnFieldValueChangedDelegate(
  95. string fullName, FieldInfo fieldInfo, object fieldOwner, object value);
  96. /// <summary>
  97. /// Called for each field, to test whether to proceed with scanning it. Return true to scan.
  98. /// </summary>
  99. public FilterFieldDelegate FilterField;
  100. public delegate bool FilterFieldDelegate(string fullName, FieldInfo fieldInfo);
  101. /// <summary>
  102. /// Called for each behaviour, to test whether to proceed with scanning it. Return true to scan.
  103. /// </summary>
  104. public FilterComponentDelegate FilterComponent;
  105. public delegate bool FilterComponentDelegate(MonoBehaviour b);
  106. /// <summary>
  107. /// The leafmost UnityEngine.Object
  108. /// </summary>
  109. public UnityEngine.Object LeafObject { get; private set; }
  110. /// <summary>
  111. /// Which fields will be scanned
  112. /// </summary>
  113. const BindingFlags kBindingFlags = BindingFlags.Public | BindingFlags.Instance;
  114. bool ScanFields(string fullName, Type type, ref object obj)
  115. {
  116. bool doneSomething = false;
  117. // Check if it's a complex type
  118. bool isLeaf = true;
  119. if (obj != null
  120. && !typeof(Component).IsAssignableFrom(type)
  121. && !typeof(ScriptableObject).IsAssignableFrom(type)
  122. && !typeof(GameObject).IsAssignableFrom(type))
  123. {
  124. // Is it an array?
  125. if (type.IsArray)
  126. {
  127. isLeaf = false;
  128. var array = obj as Array;
  129. object arrayLength = array.Length;
  130. if (OnLeafField != null && OnLeafField(
  131. fullName + ".Length", arrayLength.GetType(), ref arrayLength))
  132. {
  133. Array newArray = Array.CreateInstance(
  134. array.GetType().GetElementType(), Convert.ToInt32(arrayLength));
  135. Array.Copy(array, 0, newArray, 0, Math.Min(array.Length, newArray.Length));
  136. array = newArray;
  137. doneSomething = true;
  138. }
  139. for (int i = 0; i < array.Length; ++i)
  140. {
  141. object element = array.GetValue(i);
  142. if (ScanFields(fullName + "[" + i + "]", array.GetType().GetElementType(), ref element))
  143. {
  144. array.SetValue(element, i);
  145. doneSomething = true;
  146. }
  147. }
  148. if (doneSomething)
  149. obj = array;
  150. }
  151. else
  152. {
  153. // Check if it's a complex type
  154. FieldInfo[] fields = obj.GetType().GetFields(kBindingFlags);
  155. if (fields.Length > 0)
  156. {
  157. isLeaf = false;
  158. for (int i = 0; i < fields.Length; ++i)
  159. {
  160. string name = fullName + "." + fields[i].Name;
  161. if (FilterField == null || FilterField(name, fields[i]))
  162. {
  163. object fieldValue = fields[i].GetValue(obj);
  164. if (ScanFields(name, fields[i].FieldType, ref fieldValue))
  165. {
  166. doneSomething = true;
  167. if (OnFieldValueChanged != null)
  168. OnFieldValueChanged(name, fields[i], obj, fieldValue);
  169. }
  170. }
  171. }
  172. }
  173. }
  174. }
  175. // If it's a leaf field then call the leaf handler
  176. if (isLeaf && OnLeafField != null)
  177. if (OnLeafField(fullName, type, ref obj))
  178. doneSomething = true;
  179. return doneSomething;
  180. }
  181. bool ScanFields(string fullName, MonoBehaviour b)
  182. {
  183. bool doneSomething = false;
  184. LeafObject = b;
  185. FieldInfo[] fields = b.GetType().GetFields(kBindingFlags);
  186. if (fields.Length > 0)
  187. {
  188. for (int i = 0; i < fields.Length; ++i)
  189. {
  190. string name = fullName + "." + fields[i].Name;
  191. if (FilterField == null || FilterField(name, fields[i]))
  192. {
  193. object fieldValue = fields[i].GetValue(b);
  194. if (ScanFields(name, fields[i].FieldType, ref fieldValue))
  195. doneSomething = true;
  196. // If leaf action was taken, propagate it up to the parent node
  197. if (doneSomething && OnFieldValueChanged != null)
  198. OnFieldValueChanged(fullName, fields[i], b, fieldValue);
  199. }
  200. }
  201. }
  202. return doneSomething;
  203. }
  204. /// <summary>
  205. /// Recursively scan [SaveDuringPlay] MonoBehaviours of a GameObject and its children.
  206. /// For each leaf field found, call the OnFieldValue delegate.
  207. /// </summary>
  208. public bool ScanFields(GameObject go, string prefix = null)
  209. {
  210. bool doneSomething = false;
  211. if (prefix == null)
  212. prefix = "";
  213. else if (prefix.Length > 0)
  214. prefix += ".";
  215. MonoBehaviour[] components = go.GetComponents<MonoBehaviour>();
  216. for (int i = 0; i < components.Length; ++i)
  217. {
  218. MonoBehaviour c = components[i];
  219. if (c == null || (FilterComponent != null && !FilterComponent(c)))
  220. continue;
  221. if (ScanFields(prefix + c.GetType().FullName + i, c))
  222. doneSomething = true;
  223. }
  224. return doneSomething;
  225. }
  226. };
  227. /// <summary>
  228. /// Using reflection, this class scans a GameObject (and optionally its children)
  229. /// and records all the field settings. This only works for "nice" field settings
  230. /// within MonoBehaviours. Changes to the behaviour stack made between saving
  231. /// and restoring will fool this class.
  232. /// </summary>
  233. class ObjectStateSaver
  234. {
  235. string mObjectFullPath;
  236. Dictionary<string, string> mValues = new Dictionary<string, string>();
  237. /// <summary>
  238. /// Recursively collect all the field values in the MonoBehaviours
  239. /// owned by this object and its descendants. The values are stored
  240. /// in an internal dictionary.
  241. /// </summary>
  242. public void CollectFieldValues(GameObject go)
  243. {
  244. mObjectFullPath = ObjectTreeUtil.GetFullName(go);
  245. GameObjectFieldScanner scanner = new GameObjectFieldScanner();
  246. scanner.FilterField = FilterField;
  247. scanner.FilterComponent = HasSaveDuringPlay;
  248. scanner.OnLeafField = (string fullName, Type type, ref object value) =>
  249. {
  250. // Save the value in the dictionary
  251. mValues[fullName] = StringFromLeafObject(value);
  252. //Debug.Log(mObjectFullPath + "." + fullName + " = " + mValues[fullName]);
  253. return false;
  254. };
  255. scanner.ScanFields(go);
  256. }
  257. public GameObject FindSavedGameObject(GameObject[] roots)
  258. {
  259. return ObjectTreeUtil.FindObjectFromFullName(mObjectFullPath, roots);
  260. }
  261. /// <summary>
  262. /// Recursively scan the MonoBehaviours of a GameObject and its children.
  263. /// For each field found, look up its value in the internal dictionary.
  264. /// If it's present and its value in the dictionary differs from the actual
  265. /// value in the game object, Set the GameObject's value using the value
  266. /// recorded in the dictionary.
  267. /// </summary>
  268. public bool PutFieldValues(GameObject go, GameObject[] roots)
  269. {
  270. GameObjectFieldScanner scanner = new GameObjectFieldScanner();
  271. scanner.FilterField = FilterField;
  272. scanner.FilterComponent = HasSaveDuringPlay;
  273. scanner.OnLeafField = (string fullName, Type type, ref object value) =>
  274. {
  275. // Lookup the value in the dictionary
  276. if (mValues.TryGetValue(fullName, out string savedValue)
  277. && StringFromLeafObject(value) != savedValue)
  278. {
  279. //Debug.Log("Put " + mObjectFullPath + "." + fullName + " = " + mValues[fullName]);
  280. value = LeafObjectFromString(type, mValues[fullName].Trim(), roots);
  281. return true; // changed
  282. }
  283. return false;
  284. };
  285. scanner.OnFieldValueChanged = (fullName, fieldInfo, fieldOwner, value) =>
  286. {
  287. fieldInfo.SetValue(fieldOwner, value);
  288. if (PrefabUtility.GetPrefabInstanceStatus(go) != PrefabInstanceStatus.NotAPrefab)
  289. PrefabUtility.RecordPrefabInstancePropertyModifications(scanner.LeafObject);
  290. return true;
  291. };
  292. return scanner.ScanFields(go);
  293. }
  294. /// Ignore fields marked with the [NoSaveDuringPlay] attribute
  295. static bool FilterField(string fullName, FieldInfo fieldInfo)
  296. {
  297. var attrs = fieldInfo.GetCustomAttributes(false);
  298. foreach (var attr in attrs)
  299. if (attr.GetType().Name.Equals("NoSaveDuringPlayAttribute"))
  300. return false;
  301. return true;
  302. }
  303. /// Only process components with the [SaveDuringPlay] attribute
  304. public static bool HasSaveDuringPlay(MonoBehaviour b)
  305. {
  306. var attrs = b.GetType().GetCustomAttributes(true);
  307. foreach (var attr in attrs)
  308. if (attr.GetType().Name.Equals("SaveDuringPlayAttribute"))
  309. return true;
  310. return false;
  311. }
  312. /// <summary>
  313. /// Parse a string to generate an object.
  314. /// Only very limited primitive object types are supported.
  315. /// Enums, Vectors and most other structures are automatically supported,
  316. /// because the reflection system breaks them down into their primitive components.
  317. /// You can add more support here, as needed.
  318. /// </summary>
  319. static object LeafObjectFromString(Type type, string value, GameObject[] roots)
  320. {
  321. if (type == typeof(Single))
  322. return float.Parse(value);
  323. if (type == typeof(Double))
  324. return double.Parse(value);
  325. if (type == typeof(Boolean))
  326. return Boolean.Parse(value);
  327. if (type == typeof(string))
  328. return value;
  329. if (type == typeof(Int32))
  330. return Int32.Parse(value);
  331. if (type == typeof(UInt32))
  332. return UInt32.Parse(value);
  333. if (typeof(Component).IsAssignableFrom(type))
  334. {
  335. // Try to find the named game object
  336. GameObject go = ObjectTreeUtil.FindObjectFromFullName(value, roots);
  337. return (go != null) ? go.GetComponent(type) : null;
  338. }
  339. if (typeof(GameObject).IsAssignableFrom(type))
  340. {
  341. // Try to find the named game object
  342. return GameObject.Find(value);
  343. }
  344. if (typeof(ScriptableObject).IsAssignableFrom(type))
  345. {
  346. return AssetDatabase.LoadAssetAtPath(value, type);
  347. }
  348. return null;
  349. }
  350. static string StringFromLeafObject(object obj)
  351. {
  352. if (obj == null)
  353. return string.Empty;
  354. if (typeof(Component).IsAssignableFrom(obj.GetType()))
  355. {
  356. Component c = (Component)obj;
  357. if (c == null) // Component overrides the == operator, so we have to check
  358. return string.Empty;
  359. return ObjectTreeUtil.GetFullName(c.gameObject);
  360. }
  361. if (typeof(GameObject).IsAssignableFrom(obj.GetType()))
  362. {
  363. GameObject go = (GameObject)obj;
  364. if (go == null) // GameObject overrides the == operator, so we have to check
  365. return string.Empty;
  366. return ObjectTreeUtil.GetFullName(go);
  367. }
  368. if (typeof(ScriptableObject).IsAssignableFrom(obj.GetType()))
  369. {
  370. return AssetDatabase.GetAssetPath(obj as ScriptableObject);
  371. }
  372. return obj.ToString();
  373. }
  374. };
  375. /// <summary>
  376. /// For all registered object types, record their state when exiting Play Mode,
  377. /// and restore that state to the objects in the scene. This is a very limited
  378. /// implementation which has not been rigorously tested with many objects types.
  379. /// It's quite possible that not everything will be saved.
  380. ///
  381. /// This class is expected to become obsolete when Unity implements this functionality
  382. /// in a more general way.
  383. ///
  384. /// To use this class,
  385. /// drop this script into your project, and add the [SaveDuringPlay] attribute to your class.
  386. ///
  387. /// Note: if you want some specific field in your class NOT to be saved during play,
  388. /// add a property attribute whose class name contains the string "NoSaveDuringPlay"
  389. /// and the field will not be saved.
  390. /// </summary>
  391. [InitializeOnLoad]
  392. public class SaveDuringPlay
  393. {
  394. /// <summary>Editor preferences key for SaveDuringPlay enabled</summary>
  395. public static string kEnabledKey = "SaveDuringPlay_Enabled";
  396. /// <summary>Enabled status for SaveDuringPlay.
  397. /// This is a global setting, saved in Editor Prefs</summary>
  398. public static bool Enabled
  399. {
  400. get => EditorPrefs.GetBool(kEnabledKey, false);
  401. set
  402. {
  403. if (value != Enabled)
  404. {
  405. EditorPrefs.SetBool(kEnabledKey, value);
  406. }
  407. }
  408. }
  409. static SaveDuringPlay()
  410. {
  411. // Install our callbacks
  412. #if UNITY_2017_2_OR_NEWER
  413. EditorApplication.playModeStateChanged += OnPlayStateChanged;
  414. #else
  415. EditorApplication.update += OnEditorUpdate;
  416. EditorApplication.playmodeStateChanged += OnPlayStateChanged;
  417. #endif
  418. }
  419. #if UNITY_2017_2_OR_NEWER
  420. static void OnPlayStateChanged(PlayModeStateChange pmsc)
  421. {
  422. if (Enabled)
  423. {
  424. switch (pmsc)
  425. {
  426. // If exiting playmode, collect the state of all interesting objects
  427. case PlayModeStateChange.ExitingPlayMode:
  428. SaveAllInterestingStates();
  429. break;
  430. case PlayModeStateChange.EnteredEditMode when sSavedStates != null:
  431. RestoreAllInterestingStates();
  432. break;
  433. }
  434. }
  435. }
  436. #else
  437. static void OnPlayStateChanged()
  438. {
  439. // If exiting playmode, collect the state of all interesting objects
  440. if (Enabled)
  441. {
  442. if (!EditorApplication.isPlayingOrWillChangePlaymode && EditorApplication.isPlaying)
  443. SaveAllInterestingStates();
  444. }
  445. }
  446. static float sWaitStartTime = 0;
  447. static void OnEditorUpdate()
  448. {
  449. if (Enabled && sSavedStates != null && !Application.isPlaying)
  450. {
  451. // Wait a bit for things to settle before applying the saved state
  452. const float WaitTime = 1f; // GML todo: is there a better way to do this?
  453. float time = Time.realtimeSinceStartup;
  454. if (sWaitStartTime == 0)
  455. sWaitStartTime = time;
  456. else if (time - sWaitStartTime > WaitTime)
  457. {
  458. RestoreAllInterestingStates();
  459. sWaitStartTime = 0;
  460. }
  461. }
  462. }
  463. #endif
  464. /// <summary>
  465. /// If you need to get notified before state is collected for hotsave, this is the place
  466. /// </summary>
  467. public static OnHotSaveDelegate OnHotSave;
  468. /// <summary>Delegate for HotSave notification</summary>
  469. public delegate void OnHotSaveDelegate();
  470. /// Collect all relevant objects, active or not
  471. static HashSet<GameObject> FindInterestingObjects()
  472. {
  473. var objects = new HashSet<GameObject>();
  474. MonoBehaviour[] everything = ObjectTreeUtil.FindAllBehavioursInScene<MonoBehaviour>();
  475. foreach (var b in everything)
  476. {
  477. if (!objects.Contains(b.gameObject) && ObjectStateSaver.HasSaveDuringPlay(b))
  478. {
  479. //Debug.Log("Found " + ObjectTreeUtil.GetFullName(b.gameObject) + " for hot-save");
  480. objects.Add(b.gameObject);
  481. }
  482. }
  483. return objects;
  484. }
  485. static List<ObjectStateSaver> sSavedStates = null;
  486. static void SaveAllInterestingStates()
  487. {
  488. //Debug.Log("Exiting play mode: Saving state for all interesting objects");
  489. if (OnHotSave != null)
  490. OnHotSave();
  491. sSavedStates = new List<ObjectStateSaver>();
  492. var objects = FindInterestingObjects();
  493. foreach (var obj in objects)
  494. {
  495. var saver = new ObjectStateSaver();
  496. saver.CollectFieldValues(obj);
  497. sSavedStates.Add(saver);
  498. }
  499. if (sSavedStates.Count == 0)
  500. sSavedStates = null;
  501. }
  502. static void RestoreAllInterestingStates()
  503. {
  504. //Debug.Log("Updating state for all interesting objects");
  505. bool dirty = false;
  506. GameObject[] roots = ObjectTreeUtil.FindAllRootObjectsInScene();
  507. foreach (ObjectStateSaver saver in sSavedStates)
  508. {
  509. GameObject go = saver.FindSavedGameObject(roots);
  510. if (go != null)
  511. {
  512. Undo.RegisterFullObjectHierarchyUndo(go, "SaveDuringPlay");
  513. if (saver.PutFieldValues(go, roots))
  514. {
  515. //Debug.Log("SaveDuringPlay: updated settings of " + saver.ObjetFullPath);
  516. EditorUtility.SetDirty(go);
  517. dirty = true;
  518. }
  519. }
  520. }
  521. if (dirty)
  522. UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
  523. sSavedStates = null;
  524. }
  525. }
  526. }