Unity3D 如何在退出运行模式后保存修改数据
来源:互联网 发布:成年后不喜欢读书 知乎 编辑:程序博客网 时间:2024/06/10 09:10
本文介绍了Unity如何在退出运行模式后,保存对组件参数的数据修改。
之前因为策划有在Game运行模式时动态修改脚本参数,在退出Playmode后脚本参数保存的需求,用于在场景中动态地调整参数。研究了一下之前老外实现的一个插件PlayModePersist,也就是一个在运行模式下进行保存数据的插件。
PlayModePersist 这款插件不知道什么原因已经从Appstore下架了。有需要的同学可以自行下载。
通过阅读这款插件的源码,学到了两个比较有趣的实现:
1. 通过反射获取所有应用类,做成编辑器以供筛选。
请参阅UnityEditor:通过反射实现的Class过滤器编辑器
2. 退出Playmode后,保存修改过的数据。
今天讨论的就是No.2的实现方式
实现的核心还是通过反射。
通过脚本控制添加所需要修改的组件列表,在 EditorApplication.playmodeStateChanged 类似于OnApplicationQuit 的时候把保存的数据提交修改。
我发现以前写云笔记的时候还是比较轻松,毕竟只是写给自己看,写博客的时候要让读者读懂,说清楚思路还是好难。
不废话了,先上代码
using System.Collections.Generic;using UnityEngine;using UnityEditor;using System.Reflection;using System;public class PlayModeEditHelper { private static PlayModeEditHelper _instance; public static PlayModeEditHelper Instance { get { if (_instance == null) _instance = new PlayModeEditHelper(); return _instance; } } //在构造函数中 注册修改事件 public PlayModeEditHelper() { EditorApplication.playmodeStateChanged = Application_PlaymodeStateChanged; SavingDatas = new Dictionary<int, SettingData>(); } List<Component> SavingGroups = new List<Component>(); //需要保存的组件列表 List<int> SavingIDs = new List<int>(); //需要保存组件的InstanceID列表 Dictionary<int, SettingData> SavingDatas; //缓存数据字典 void Application_PlaymodeStateChanged() { if (EditorApplication.isPlaying || EditorApplication.isPaused) { //window repaint or do sth... } else { RestoreAllSavingSetting(); EditorApplication.playmodeStateChanged = null; } } //还原所有修改数据 void RestoreAllSavingSetting() { for (int i = 0; i < SavingIDs.Count; i++) { RestoreSetting(SavingIDs[i]); } } //通过实例ID修改组件参数 void RestoreSetting(int id) { Component ComponentObject = EditorUtility.InstanceIDToObject(id) as Component; Dictionary<string, object> values = SavingDatas[id].values; foreach (string name in values.Keys) { object newValue = values[name]; PropertyInfo property = ComponentObject.GetType().GetProperty(name); if (null != property) { object currentValue = property.GetValue(ComponentObject, null); property.SetValue(ComponentObject, newValue, null); } else { FieldInfo field = ComponentObject.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); object currentValue = field.GetValue(ComponentObject); field.SetValue(ComponentObject, newValue); } } if (ComponentObject != null) { SetPrefabDirty(ComponentObject); } } //如果是预设体,通知引擎修改 不能省略,修正了修改目标是预设体时,每次运行参数重置的bug void SetPrefabDirty(Component ComponentObject) { PrefabType prefabType = PrefabUtility.GetPrefabType(ComponentObject.gameObject); if (prefabType == PrefabType.DisconnectedPrefabInstance || prefabType == PrefabType.PrefabInstance) { EditorUtility.SetDirty(ComponentObject); } } //缓存修改信息 包括属性、字段 public void SaveValues(Component ComponentObject) { if (SavingGroups.Contains(ComponentObject)) { RefreshValues(ComponentObject); return; } Dictionary<string, object> values = new Dictionary<string, object>(); List<PropertyInfo> properties = GetProperties(ComponentObject); List<FieldInfo> fields = GetFields(ComponentObject); foreach (PropertyInfo property in properties) { values.Add(property.Name, property.GetValue(ComponentObject, null)); } foreach (FieldInfo field in fields) { values.Add(field.Name, field.GetValue(ComponentObject)); } //添加需要保存的组件 SavingGroups.Add(ComponentObject); SavingIDs.Add(ComponentObject.GetInstanceID()); SavingDatas.Add(ComponentObject.GetInstanceID(), new SettingData(values)); } //修正多次点击修改不能更新数据的bug private void RefreshValues(Component ComponentObject) { Dictionary<string, object> values = SavingDatas[ComponentObject.GetInstanceID()].values; List<PropertyInfo> properties = GetProperties(ComponentObject); List<FieldInfo> fields = GetFields(ComponentObject); foreach (PropertyInfo property in properties) { values[property.Name] = property.GetValue(ComponentObject, null); } foreach (FieldInfo field in fields) { values[field.Name] = field.GetValue(ComponentObject); } } //获取私有字段+公有字段列表 private List<FieldInfo> GetFields(Component ComponentObject) { List<FieldInfo> fields = new List<FieldInfo>(); //获取字段包括私有字段、共有字段 修正了私有字段不能正常修改的bug FieldInfo[] infos = ComponentObject.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); //foreach (FieldInfo fieldInfo in ComponentObject.GetType().GetFields()) foreach (FieldInfo fieldInfo in infos) { if (!Attribute.IsDefined(fieldInfo, typeof(HideInInspector))) { fields.Add(fieldInfo); } } return fields; } //获取属性列表 private List<PropertyInfo> GetProperties(Component ComponentObject) { List<PropertyInfo> properties = new List<PropertyInfo>(); foreach (PropertyInfo propertyInfo in ComponentObject.GetType().GetProperties()) { if (!Attribute.IsDefined(propertyInfo, typeof(HideInInspector))) { MethodInfo setMethod = propertyInfo.GetSetMethod(); if (null != setMethod && setMethod.IsPublic) { properties.Add(propertyInfo); } } } return properties; }}//简易的设置数据 自行根据需求扩充public class SettingData{ public Dictionary<string, object> values; public SettingData(Dictionary<string, object> datas) { values = datas; } public void AddData(string name, object value) { if (!values.ContainsKey(name)) { values.Add(name, value); } }}
其中测试脚本如下:
using UnityEngine;public class TestEditParam : MonoBehaviour { public enum ENUM_TEST { ENUM1, ENUM2, ENUM3 } public int mIntValue; //测试整形字段 public float mFloatValue; //测试Float型字段 public Vector3 mVector3Value; //测试Vector型字段 public string mStringValue; //测试String型字段 public ENUM_TEST mEnumType; //测试自定义枚举型字段 //测试私有字段 [SerializeField] private int mPrivateIntValue; [SerializeField] private float mPrivateFloatValue;}
测试Editor脚本:置于Editor文件夹下
using UnityEngine;using UnityEditor;[CustomEditor(typeof(TestEditParam), true)]public class TestEditParamEditor : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); GUILayout.Space(10); if (GUILayout.Button("Save")) { SaveParam(); } } //将TestEditParam 和 Transform 注册到修改队列 void SaveParam() { //将target(即Editor脚本的目标组件TestEditParam)注册到修改队列 Component mTarget = target as Component; PlayModeEditHelper.Instance.SaveValues(mTarget); //将Transform 注册到修改队列 Transform rectCom = mTarget.transform.GetComponent<Transform>(); PlayModeEditHelper.Instance.SaveValues(rectCom); //ApplyParam(); } //提交预设体修改 直接保存prefab的暴力方法 只需更改prefab时直接用此接口即可 void ApplyParam() { Object prefabParent = PrefabUtility.GetPrefabParent(target); TestEditParam mScript = target as TestEditParam; PrefabUtility.ReplacePrefab(mScript.gameObject, prefabParent); }}
使用方法很简单,在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脚本中有个方法。其实是脚本做提交预设体修改的方法。即:
Object prefabParent = PrefabUtility.GetPrefabParent(target); TestEditParam mScript = target as TestEditParam; PrefabUtility.ReplacePrefab(mScript.gameObject, prefabParent);
如果仅是保存prefab需求直接应用即可
由于这是个辅助脚本,用于配合企划的修改需求。不会在正常的项目中实际运行到。所以代码并不优雅,只是给同学们一个思路,也许有更佳的方法
有建议和想法的还望不吝赐教~谢谢
阅读全文
0 0
- Unity3D 如何在退出运行模式后保存修改数据
- 在终端运行程序后如何退出
- centos下修改文件后如何保存退出
- centos下修改文件后如何保存退出
- eclipse在程序修改后,点击运行可以自动保存,如何设置
- 在DataGridView中修改后的数据如何保存到数据库
- 如何在Web页面退出前提示用户保存数据?
- 如何在Web页面退出前提示用户保存数据?
- 如何对dataset中进行修改并把修改后的数据保存到数据库中?
- VS2010正式版如何在程序运行完后等待用户按任意键退出
- 如何在退出一个activity后,很好的取消AsyncTask继续运行?
- Android之 如何在退出一个activity后,很好的取消AsyncTask继续运行
- hibernate保存数据后在修改的时候结果是老数据的问题
- Unity退出后保存数据,PlayerPrefs 玩家偏好
- 在ListView上修改内容后的界面刷新和数据保存
- GridControl编辑后如何保存数据
- GridControl编辑后如何保存数据
- 如何让CMD命令运行后不自动退出
- bootstrap-datetimepicker年视图中endDate设置之后比正常时间提前两个月
- mybatis报Could not find result map java.lang.String
- android recyclerview
- Flume原理
- BZOJ4815: [Cqoi2017]小Q的表格
- Unity3D 如何在退出运行模式后保存修改数据
- CSUACM小组的成绩排名(用qsort函数对double排序需谨慎)
- Android 同步和消息机制
- 心电图各波及波段的组成
- 内存碎片/内存空洞
- UESTC 数据结构专题训练 D,E,F
- 【STM32F103攻城笔记】STM32之MDK(Keil)环境搭建(一)
- [iOS 原生代码实现 二维码的生成与读取(转)]通过滤镜CIFilter生成二维码
- elasticsearch插件安装