SaveDuringPlay.cs 25 KB

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