背景
项目(手游)进行到中后期,在进行代码重构过程中,难免会遇到一些与数据相关的问题。
例如我今天遇到的一个:Unity Scene 中存在了一些关卡数据,这些关卡数据是由关卡设计者放在场景中的物体上配置的(Monobehavior),借由场景进行数据的保存。
当然,如果关卡设计不是使用 unity scene 作为媒介来保存,可能就不存在这个问题了。
言归正传,重构(Refactor)过程中,我需要将 Monobehavior 组件中散乱存储的数据项,集中到一个数据结构中,做一个聚合,让代码结构更加优化。此重构的优化目标此处不做讨论。
假设原先有 十几个场景,每个场景有十几个保存数据的 Monobehavior,如何将原来的数据迁移到新的结构中呢?
Unity 中对于重构的支持
Unity 通过 Monobehavior 中的 SerializeFiled 属性标记需要序列化存储的成员。然而成员变量名字的修改,会导致序列化数据的丢失。原因是识别序列化数据和对应成员是使用成员的名字来进行的,改名会导致 Unity 认为你删除了一个旧的成员,而添加了一个新的成员。
简单重命名可以使用 以下方法来避免数据丢失:
_playerPrefab 修改为 _newPlayerPrefab
//改名之前
[SerializeField]
private GameObject _playerPrefab;
//改名之后
[SerializeField]
[UnityEngine.Serialization.FormerlySerializedAs("_playerPrefab")]
private GameObject _newPlayerPrefab;
然而对于这样的修改:
[SerializeField]
private float _a;
[SerializeField]
private float _b;
_a 和 _b 字段聚集到一个类 AB 中
[System.Serializable]
public class AB
{
public float A;
public float B;
}
最后需要这样的存储方式:
[SerializeField]
private AB _ab;
暂时还未找到方便的 Unity 内置提供解决方案。
各位看官如果知道,可以指出来~(虚心求教)
解决思路
- 遍历所有场景
- 遍历所有目标脚本
- 将存储在原来位置的数据放到目标结构中
- 存储修改后数据
过程
思路很清楚了,不过过程是艰辛的。(似乎是似曾相识是吗?)
遍历所有场景
public static string[] FindAllScene(string scenePath)
{
return System.IO.Directory.GetFiles(scenePath, "*.unity");
}
遍历所有目标脚本
public static T[] FindAllComponent<T>() where T : Object
{
return Resources.FindObjectsOfTypeAll<T>();
}
将存储在原来位置的数据放入目标结构中
public static void MoveData(MonoBehaviour ab)
{
SerializedObject serializedObject = new SerializedObject(ab);
serializedObject.FindProperty("_ab.A").floatValue = serializedObject.FindProperty("_a").floatValue;
serializedObject.FindProperty("_ab.B").floatValue = serializedObject.FindProperty("_b").floatValue;
}
存储修改后数据
这个部分包含了两个部分,一个是在数据移动的方法中添加一个调用:
serializedObject.ApplyModifiedProperties();
添加后:
public static void MoveData(MonoBehaviour ab)
{
SerializedObject serializedObject = new SerializedObject(ab);
serializedObject.FindProperty("_ab.A").floatValue = serializedObject.FindProperty("_a").floatValue;
serializedObject.FindProperty("_ab.B").floatValue = serializedObject.FindProperty("_b").floatValue;
serializedObject.ApplyModifiedProperties();
}
另一部分则是场景的保存:
EditorSceneManager.SaveScene(scene);
最后给出最终的完成代码:
public static void Move()
{
EditorSceneManager.SaveOpenScenes();
string[] paths = System.IO.Directory.GetFiles("Assets/Maps", "*.unity");
for (int i = 0; i < paths.Length; i++)
{
Scene scene = EditorSceneManager.OpenScene(paths[i]);
BoostArea[] scripts = Resources.FindObjectsOfTypeAll<BoostArea>();
for (int j = 0; j < scripts.Length; j++)
{
MoveData(scripts[j]);
}
EditorSceneManager.SaveScene(scene);
}
}
注意几点:
- 使用 EditorUtility.SetDirty() 是不能正确标记场景被修改过的。具体看文档
- 使用 serializedObject.ApplyModifiedProperties(); 会使得脚本所属的 prefab 也会被修改保存
- 经过测试 EditorSceneManager.MarkSceneDirty 也不能完成这个多场景遍历同时保存修改的操作。(没有使用 serializedObject.ApplyModifiedProperties(),而是直接使用脚本的实例相互赋值)