Unity3D 如何在退出运行模式后保存修改数据

之前因为策划有在Game运行模式时动态修改脚本参数,在退出Playmode后脚本参数保存的需求,用于在场景中动态地调整参数。研究了一下之前老外实现的一个插件PlayModePersist,也就是一个在运行模式下进行保存数据的插件。
PlayModePersist 这款插件不知道什么原因已经从Appstore下架了。有需要的同学可以自行下载。
通过阅读这款插件的源码,学到了两个比较有趣的实现:
1. 通过反射获取所有应用类,做成编辑器以供筛选。
2. 退出Playmode后,保存修改过的数据。

今天讨论的就是No.2的实现方式


实现的核心还是通过反射。
通过脚本控制添加所需要修改的组 件列表,在 EditorApplication.playmodeStateChanged 类似于OnApplicationQuit 的时候把保存的数据提交修改。

我发现以前写云笔记的时候还是比较轻松,毕竟只是写给自己看,写博客的时候要让读者读懂,说清楚思路还是好难。
不废话了,先上代码
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. using UnityEditor;
  4. using System.Reflection;
  5. using System;
  6. public class PlayModeEditHelper {
  7. private static PlayModeEditHelper _instance;
  8. public static PlayModeEditHelper Instance
  9. {
  10. get
  11. {
  12. if (_instance == null)
  13. _instance = new PlayModeEditHelper();
  14. return _instance;
  15. }
  16. }
  17. //在构造函数中 注册修改事件
  18. public PlayModeEditHelper()
  19. {
  20. EditorApplication.playmodeStateChanged = Application_PlaymodeStateChanged;
  21. SavingDatas = new Dictionary< int, SettingData>();
  22. }
  23. List<Component> SavingGroups = new List<Component>(); //需要保存的组件列表
  24. List< int> SavingIDs = new List< int>(); //需要保存组件的InstanceID列表
  25. Dictionary< int, SettingData> SavingDatas; //缓存数据字典
  26. void Application_PlaymodeStateChanged()
  27. {
  28. if (EditorApplication.isPlaying || EditorApplication.isPaused)
  29. {
  30. //window repaint or do sth...
  31. }
  32. else
  33. {
  34. RestoreAllSavingSetting();
  35. EditorApplication.playmodeStateChanged = null;
  36. }
  37. }
  38. //还原所有修改数据
  39. void RestoreAllSavingSetting()
  40. {
  41. for ( int i = 0; i < SavingIDs.Count; i++)
  42. {
  43. RestoreSetting(SavingIDs[i]);
  44. }
  45. }
  46. //通过实例ID修改组件参数
  47. void RestoreSetting(int id)
  48. {
  49. Component ComponentObject = EditorUtility.InstanceIDToObject(id) as Component;
  50. Dictionary< string, object> values = SavingDatas[id].values;
  51. foreach ( string name in values.Keys)
  52. {
  53. object newValue = values[name];
  54. PropertyInfo property = ComponentObject.GetType().GetProperty(name);
  55. if ( null != property)
  56. {
  57. object currentValue = property.GetValue(ComponentObject, null);
  58. property.SetValue(ComponentObject, newValue, null);
  59. }
  60. else
  61. {
  62. FieldInfo field = ComponentObject.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
  63. object currentValue = field.GetValue(ComponentObject);
  64. field.SetValue(ComponentObject, newValue);
  65. }
  66. }
  67. if (ComponentObject != null)
  68. {
  69. SetPrefabDirty(ComponentObject);
  70. }
  71. }
  72. //如果是预设体,通知引擎修改 不能省略,修正了修改目标是预设体时,每次运行参数重置的bug
  73. void SetPrefabDirty(Component ComponentObject)
  74. {
  75. PrefabType prefabType = PrefabUtility.GetPrefabType(ComponentObject.gameObject);
  76. if (prefabType == PrefabType.DisconnectedPrefabInstance || prefabType == PrefabType.PrefabInstance)
  77. {
  78. EditorUtility.SetDirty(ComponentObject);
  79. }
  80. }
  81. //缓存修改信息 包括属性、字段
  82. public void SaveValues(Component ComponentObject)
  83. {
  84. if (SavingGroups.Contains(ComponentObject))
  85. {
  86. RefreshValues(ComponentObject);
  87. return;
  88. }
  89. Dictionary< string, object> values = new Dictionary< string, object>();
  90. List<PropertyInfo> properties = GetProperties(ComponentObject);
  91. List<FieldInfo> fields = GetFields(ComponentObject);
  92. foreach (PropertyInfo property in properties)
  93. {
  94. values.Add(property.Name, property.GetValue(ComponentObject, null));
  95. }
  96. foreach (FieldInfo field in fields)
  97. {
  98. values.Add(field.Name, field.GetValue(ComponentObject));
  99. }
  100. //添加需要保存的组件
  101. SavingGroups.Add(ComponentObject);
  102. SavingIDs.Add(ComponentObject.GetInstanceID());
  103. SavingDatas.Add(ComponentObject.GetInstanceID(), new SettingData(values));
  104. }
  105. //修正多次点击修改不能更新数据的bug
  106. private void RefreshValues(Component ComponentObject)
  107. {
  108. Dictionary< string, object> values = SavingDatas[ComponentObject.GetInstanceID()].values;
  109. List<PropertyInfo> properties = GetProperties(ComponentObject);
  110. List<FieldInfo> fields = GetFields(ComponentObject);
  111. foreach (PropertyInfo property in properties)
  112. {
  113. values[property.Name] = property.GetValue(ComponentObject, null);
  114. }
  115. foreach (FieldInfo field in fields)
  116. {
  117. values[field.Name] = field.GetValue(ComponentObject);
  118. }
  119. }
  120. //获取私有字段+公有字段列表
  121. private List<FieldInfo> GetFields(Component ComponentObject)
  122. {
  123. List<FieldInfo> fields = new List<FieldInfo>();
  124. //获取字段包括私有字段、共有字段 修正了私有字段不能正常修改的bug
  125. FieldInfo[] infos = ComponentObject.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
  126. //foreach (FieldInfo fieldInfo in ComponentObject.GetType().GetFields())
  127. foreach (FieldInfo fieldInfo in infos)
  128. {
  129. if (!Attribute.IsDefined(fieldInfo, typeof(HideInInspector)))
  130. {
  131. fields.Add(fieldInfo);
  132. }
  133. }
  134. return fields;
  135. }
  136. //获取属性列表
  137. private List<PropertyInfo> GetProperties(Component ComponentObject)
  138. {
  139. List<PropertyInfo> properties = new List<PropertyInfo>();
  140. foreach (PropertyInfo propertyInfo in ComponentObject.GetType().GetProperties())
  141. {
  142. if (!Attribute.IsDefined(propertyInfo, typeof(HideInInspector)))
  143. {
  144. MethodInfo setMethod = propertyInfo.GetSetMethod();
  145. if ( null != setMethod && setMethod.IsPublic)
  146. {
  147. properties.Add(propertyInfo);
  148. }
  149. }
  150. }
  151. return properties;
  152. }
  153. }
  154. //简易的设置数据 自行根据需求扩充
  155. public class SettingData
  156. {
  157. public Dictionary< string, object> values;
  158. public SettingData(Dictionary<string, object> datas)
  159. {
  160. values = datas;
  161. }
  162. public void AddData(string name, object value)
  163. {
  164. if (!values.ContainsKey(name))
  165. {
  166. values.Add(name, value);
  167. }
  168. }
  169. }

其中测试脚本如下:
  1. using UnityEngine;
  2. public class TestEditParam : MonoBehaviour {
  3. public enum ENUM_TEST
  4. {
  5. ENUM1,
  6. ENUM2,
  7. ENUM3
  8. }
  9. public int mIntValue; //测试整形字段
  10. public float mFloatValue; //测试Float型字段
  11. public Vector3 mVector3Value; //测试Vector型字段
  12. public string mStringValue; //测试String型字段
  13. public ENUM_TEST mEnumType; //测试自定义枚举型字段
  14. //测试私有字段
  15. [ SerializeField]
  16. private int mPrivateIntValue;
  17. [ SerializeField]
  18. private float mPrivateFloatValue;
  19. }

测试Editor脚本:置于Editor文件夹下
  1. using UnityEngine;
  2. using UnityEditor;
  3. [ CustomEditor(typeof(TestEditParam), true)]
  4. public class TestEditParamEditor : Editor {
  5. public override void OnInspectorGUI()
  6. {
  7. base.OnInspectorGUI();
  8. GUILayout.Space( 10);
  9. if (GUILayout.Button( "Save"))
  10. {
  11. SaveParam();
  12. }
  13. }
  14. //将TestEditParam 和 Transform 注册到修改队列
  15. void SaveParam()
  16. {
  17. //将target(即Editor脚本的目标组件TestEditParam)注册到修改队列
  18. Component mTarget = target as Component;
  19. PlayModeEditHelper.Instance.SaveValues(mTarget);
  20. //将Transform 注册到修改队列
  21. Transform rectCom = mTarget.transform.GetComponent<Transform>();
  22. PlayModeEditHelper.Instance.SaveValues(rectCom);
  23. //ApplyParam();
  24. }
  25. //提交预设体修改 直接保存prefab的暴力方法 只需更改prefab时直接用此接口即可
  26. void ApplyParam()
  27. {
  28. Object prefabParent = PrefabUtility.GetPrefabParent(target);
  29. TestEditParam mScript = target as TestEditParam;
  30. PrefabUtility.ReplacePrefab(mScript.gameObject, prefabParent);
  31. }
  32. }

使用方法 很简单,在Editor脚本的按钮事件中添加需要修改的组件mTarget 即可
PlayModeEditHelper.Instance.SaveValues(mTarget);
支持添加多个组件,支持多次保存更新

其实prefab在保存时就是一系列序列化信息。可以通过文本工具查看,如:

主要实现的思路就是,通过反射方法,把Component的 属性(PropertyInfo),和字段(FieldInfo)缓存下来。在结束运行模式后,系统首先会把组件数值还原。然后调用Application_PlaymodeStateChanged 方法,在系统默认还原后重新通过InstanceID获取组件,并对其字段属性的数值进行覆盖。
由于这个时序性,具体应用时会直观地看到一个数值跳动的过程。

接下来我们具体看一下 测试脚本中 PropertyInfo 和FieldInfo具体包含的信息

PropertyInfo :

FieldInfo:

我们测试时用到的是字段值,而工作中也经常会用到属性索引器等等。所以都需要处理。

我们可以看到在PropertyInfo 中有些无用的基类的属性比如tag值,hideflag等。
这些在插件源码中单独处理成了一个IgnoreList 忽略列表,但是代码基类千变万化,出于简化代码的目的(不想人力维护这个列表),而且插件中的属性有多余的情况,比如包含了Transform类的基础属性等等,最终没有定义这个筛选列表。如果有需要可以参照插件源码。

这里在获取所有字段中用到了反射的筛选条件,支持了获取私有字段
ComponentObject.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
具体反射类可自行查阅相关文章,这里不做详细说明。

在playmodeStateChanged 状态切换后,检查状态如果是退出playmode时,调用脚本的保存修改方法。
通过property.SetValue,field.SetValue设置参数。

这里需要说明的是,如果修改的目标被设置成prefab(通常都是这样),要通过EditorUtility.SetDirty(ComponentObject)方法在Editor中将其标记为脏(变化过的)才能保存修改。否则会出现退出时虽然属性被修改,但是下一次运行时,系统会自动还原成prefab的参数。

如果大家不明白最好的方法就是断点走一遍。呃。程序猿们最简单的上手方法。

补充:
1. 之前只支持了单组件的保存。后来发现实际过程中可能会想保存Transform的属性等等,所以添加了对多个组件保存的支持。为了不影响之前的代码结构 维护了一个Dictionary<int, SettingData> SavingDatas; //缓存数据字典
临时添加SavingDatas用作处理缓存数据,方法也比较粗暴。大家用到时可以扩充数据类型,优化查找方法。
2. 还是写了这篇文章自己测试后发现,多次Save只能保存第一次的数据。忘了Save也应该有更新数据的功能,所以添加了RefreshValues的方法。也很粗暴,直接进行的所有数据全更新。请同学们自行修改优化。
3. 本文描述时比较啰嗦,主要是想简单记录一下实现时遇到的坑。
4. 在测试Editor脚本中有个方法。其实是脚本做提交预设体修改的方法。即:
  1. Object prefabParent = PrefabUtility.GetPrefabParent(target);
  2. TestEditParam mScript = target as TestEditParam;
  3. PrefabUtility.ReplacePrefab(mScript.gameObject, prefabParent);
如果仅是保存prefab需求直接应用即可



由于这是个辅助脚本,用于配合企划的修改需求。不会在正常的项目中实际运行到。所以代码并不优雅,只是给同学们一个思路,也许有更佳的方法

猜你喜欢

转载自blog.csdn.net/lizhenxiqnmlgb/article/details/80955072