深入理解Unity的prefab和序列化过程

遇到一个好帖子,所以记下来:

https://connect.unity.com/p/unityeditorzhi-tong-guo-serializedobjectlei-shi-xian-pi-liang-xiu-gai-prefabzhong-zu-jian-can-shu

其实说白了就是对“文件”进行处理。所以一定要掌握好文件处理的方法。


今天我们分享一下,如何通过脚本批量修改Prefab中的组件参数~

在使用Unity开发游戏中,Prefab一直是绕不开的一个话题。无论大游戏小游戏,都必然需要用到Prefab这个游戏对象。

随着游戏逐渐完善,Prefab的数量逐渐的增加,想要批量修改这些Prefab的工作难度也会随之增加。

修改一项功能时,可能就会涉及到多个Prefab中数据的修改。这些Prefab内容结构各异,组件千差万别。

如何避免手工劳动,使用脚本来实现批量修改呢? 我们今天介绍如何使用 “SerializedObject”来对Unity中的 Prefab 进行批量修改。

如果现在需要你给所有Text中的内容加上方括号,一个个修改是不是很慢呢?

SerializedObject 是什么?

SerializedObject 是Unity对其Object 在本地保存的一个中间产物。也就是说,所有的Unity中的游戏对象,在项目中保存都是先装换为 SerializedObject 然后再序列化为一个 “文件“(如:.prefab,.mat,.anim) 和一个 “.meta” 文件。(如果是导入的资源,如图片之类的,则只生成一个 “.meta”文件 )

并且编辑器中编辑这些游戏对象或者资源,其实也是通过反序列化这些文件,然后再展示在编辑器上。

所以 SerializedObject 对于扩展编辑器来说,是一个特别重要的工具。

Unity里加入的任何资源文件都会附带一个meta文件

SerializedObject 修改的优势是什么?

有的同学可能会说了,如果只是修改属性的话,我在Hierarchy 面板选中所有要修改的对象,然后在Inspector 面板修改组件参数为同一个值不就好了吗?

但是如果你需要修改的Prefab数量很大,涉及到100个以上了,这样做肯定就不现实了。

那为什么不直接获取组件类呢? 而要用SerializedObject类呢? 因为有的组件类中,某些参数的修改方法可能并没有提供。 并且SerializedObject 不仅仅可以修改组件参数,甚至还可以修改其他的资源的配置参数。 可以说是功能更加强大,使用范围更加广,更灵活。

并且,如果你今天要批量修改UI,明天要批量修改粒子系统,后天修改碰撞体。你就得争对这3个组件来写对应的脚本。而使用SerializedObject就只需要一套就好了~

怎么使用SerializedObject修改组件参数?

我们既然知道 SerializedObject 有这么强大的功能,那么我们只需要把 Prefab中的组件装载在 SerializedObject 里,修改组件中对应属性,然后再保存下来就可以了。

那么流程如下:

  1. 读取prefab文件

  2. 找到对应的transform 节点

  3. 获取对应组件

  4. SerializedObject化组件(序列化组件)

  5. 修改组件参数

  6. 保存修改

这里我就不赘述步骤1~2如何获取prefab以及组件的过程了。在之后的总代码里列出

下面我以文章开头的案例来举例,重点介绍一下通过SerializedObject 修改一个组件的参数

获取对应组件:

Component SerializedObject = prefabTransform.GetComponent("Text"); //直接使用组件名字来获取组件,这样是自定义脚本也可以轻松获取

序列化组件:

SerializedObject so = new SerializedObject(SerializedObject);//创建一个SerializedObject 对象来修改数据

修改并保存组件参数:

so.Update();//这里每次总获取该对象最新的数据
var sp = so.GetIterator(); //获取SerializedObject的迭代器
while (sp.NextVisible(true)) //对其中的所有参数进行迭代
{
   //if (sp.propertyType == SerializedPropertyType.String) // 获取参数类型,然后去判断是否是需要修改的参数,这里的参数类型有很多种更具自己需求选择
   //{
        if (sp.name == "m_Text") //更具参数名来找到自己需要修改的参数
        {
            sp.stringValue =   "【"+ sp.stringValue + "】"; //更具参数类型选择对应的value进行修改                                                                                                                      
            //dosomething....
        }
        if (sp.name == "m_HorizontalOverflow" || sp.name == "m_VerticalOverflow")
        {
            sp.intValue = 1;
        }
   //}
}
so.ApplyModifiedProperties();//将修改保存

保存游戏物体修改:

EditorUtility.SetDirty(prefabGameObj) //记得要把你修改的prefab设置为dirty状态
AssetDatabase.SaveAssets() ; //以及最后记得要保存资源的修改

有了上述方法以及思路,我们几乎能对Unity中任意一个游戏物体的任意参数,进行脚本修改了。

只要点一下按钮,就全部修改完成了~

 

那么有的同学可能会问了,为什么我在修改参数的时候,输入了参数名,但是没有修改成功呢?

那是因为,有可能你输入的参数名(property的名字)可能输入错误了。

Unity编辑器中显示的参数名字,不一定是他在代码里面所叫的那个名字。

那么怎么才能知道编辑器中的某个参数真正的名字呢?这里有几种可能性。

第一种,通常Unity是有着严格并且十分标准的命名方式的,通常你在面板种看到的参数叫什么,他的propertyName 一般就叫 “m_”+ 参数名。 如:Text参数的 propertyName 就叫“m_Text”

用Notepad++打开Prefab文件可以看到Unity的命名方式是十分标准的

第二种,但是也有意外,如粒子系统的这里,它的Duration的propertyName就不叫m_Duration 。

粒子系统的Duration 的内部名字就不叫“m_Duration ”

那我们可以先选中这个游戏物体,然后在Inspector 面板的右上角,进入Debug 模式。这样我们就能看到所有的该组件的参数,以及他们的真实名字了。

进入Debug模式查看参数名字

我们可以看到这个Duration 在Debug模式下叫LengthInSec ,这种类型的参数名字多半为 首字母小写。 如:Length In Sec 的propertyName就叫 lengthInSec,这一点在notepad++里也能印证。

可以看到debug模式下的Duration参数叫做LengthInSec

粒子系统的参数保存下来之后,于第一种的命名方式不一样,是首字母小写的形式。

参数名这个问题,最好的解决办法,其实就是先通过notepad++打开资源文件或者meta文件,看下这个参数在文件中,究竟是叫什么~这样就准确无误了~

最后贴一下完整代码:

获取游戏物体,并且遍历其子物体找出并修改目标组件,最后保存

 [MenuItem("编辑器脚本/Prefab工具/修改Text参数")]
    public static void EditorPrefab()
    {
        GameObject[] Prefabs = GetSelectObject<GameObject>();
        for(int i = 0;i<Prefabs.Length;i++)
        {
            Debug.Log(Prefabs[i].name);
            EditorChild(Prefabs[i].transform, "Text");
            EditorUtility.SetDirty(Prefabs[i]); //记得要把你修改的prefab设置为dirty状态
            AssetDatabase.SaveAssets(); //以及最后记得要保存资源的修改
        }
    }
    
    public static T[] GetSelectObject<T>()
    {
        return Selection.GetFiltered<T>(SelectionMode.DeepAssets);
    }

使用递归来找到符合名字的子物体,并且使用SerializedObject来修改它

 public static void EditorChild(Transform transform, string name)
    {
        for(int i = 0;i<transform.childCount;i++)
        {
            Transform child = transform.GetChild(i);
            if (child.name == name)
            {
                EditorStringValueBySerializedObject(child, "Text", "text");
            }
            if(child.childCount!=0)
            {
                EditorChild(child, name);
            }
        }
    }

通过给到的游戏物体,组件名字,参数名字,以及目标值来进行修改。

 public static void EditorStringValueBySerializedObject(Transform prefabTransform,string componentName,string propertyName)
    {
        
        Component SerializedObject = prefabTransform.GetComponent(componentName);
        SerializedObject so = new SerializedObject(SerializedObject);

        so.Update();
        var sp = so.GetIterator(); //获取SerializedObject的迭代器
        while (sp.NextVisible(true)) //对其中的所有参数进行迭代
        {
            //if (sp.propertyType == SerializedPropertyType.String) // 获取参数类型,然后去判断是否是需要修改的参数,这里的参数类型有很多种更具自己需求选择
            //{
            if (sp.name == "m_Text") //更具参数名来找到自己需要修改的参数
            {
                sp.stringValue =  "【"+ sp.stringValue + "】"; //更具参数类型选择对应的value进行修改
                //do something...
            }
            if (sp.name == "m_HorizontalOverflow" || sp.name == "m_VerticalOverflow")
            {
                sp.intValue = 1;
            }
            //}
        }
        so.ApplyModifiedProperties();
    }

 感谢这个大佬,终极解答了本博主对序列化和prefab文件的疑惑,大佬的例子是对prefab的一些参数进行批量修改。

下面本博主举一个例子,对已有prefab批量添加组件、删除组件,其实这种操作的思路有很多。可以直接改文件,也可以通过Unity的序列化、反序列化进行。

对于添加组件来说,一个思路就是GameObject.AddComponent<>(),但是这个添加是对unity下的游戏物体进行添加组件,实际上并没有真正的写在对应的prefab文件内。所以在加完组件以后,一定要写上这两句代码,注意,这种序列化的过程,非常的慢慢慢。当然如果你懒得写也不是不行,因为Unity会自动序列化。

EditorUtility.SetDirty(Prefabs[i]); //记得要把你修改的prefab设置为dirty状态
AssetDatabase.SaveAssets(); //以及最后记得要保存资源的修改

对于删除组件来说,有两个思路,第一个是直接操纵.prefab文件;第二个思路是在Unity游戏物体上删除组件,然后序列化保存。

第一种思路:直接改文件

看这个帖子:https://www.cnblogs.com/luguoshuai/p/12323186.html

using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Text.RegularExpressions;

namespace LgsProject
{
    public partial class LgsTools_Optimization
    {
        [MenuItem("LgsTools/智能检测/Remove Missing-MonoBehavior Component")]
        static public void RemoveMissComponent()
        {
            string fullPath = Application.dataPath + "/Art/Prefabs";
            fullPath = fullPath.Replace("/", @"\");
            //List<string> pathList = GetAssetsPathByFullPath(fullPath, "*.prefab", SearchOption.AllDirectories);
            List<string> pathList = GetAssetsPathByRelativePath(new string[] { "Assets/Art/Prefabs" }, "t:Prefab", SearchOption.AllDirectories);
            int counter = 0;
            for (int i = 0, iMax = pathList.Count; i < iMax; i++)
            {
                EditorUtility.DisplayProgressBar("处理进度", string.Format("{0}/{1}", i + 1, iMax), (i + 1f) / iMax);
                if (CheckMissMonoBehavior(pathList[i]))
                    ++counter;
            }
            EditorUtility.ClearProgressBar();
            EditorUtility.DisplayDialog("处理结果", "完成修改,修改数量 : " + counter, "确定");
            AssetDatabase.Refresh();
        }

        /// <summary>
        /// 获取项目中某种资源的路径
        /// </summary>
        /// <param name="fullPath">win的路径格式,以 "\"为分隔符</param>
        /// <param name="filter">win的资源过滤模式 例如 : *.prefab</param>
        /// <param name="searchOption">目录的搜索方式</param>
        /// <returns></returns>
        static List<string> GetAssetsPathByFullPath(string fullPath, string filter, SearchOption searchOption)
        {
            List<string> pathList = new List<string>();
            string[] files = Directory.GetFiles(fullPath, filter, searchOption);
            for (int i = 0; i < files.Length; i++)
            {
                string path = files[i];
                path = "Assets" + path.Substring(Application.dataPath.Length, path.Length - Application.dataPath.Length);
                pathList.Add(path);
            }

            return pathList;
        }


        /// <summary>
        /// 获取项目中某种资源的路径
        /// </summary>
        /// <param name="relativePath">unity路径格式,以 "/" 为分隔符</param>
        /// <param name="filter">unity的资源过滤模式 https://docs.unity3d.com/ScriptReference/AssetDatabase.FindAssets.html </param>
        /// <param name="searchOption"></param>
        /// <returns></returns>
        static List<string> GetAssetsPathByRelativePath(string[] relativePath, string filter, SearchOption searchOption)
        {
            List<string> pathList = new List<string>();
            string[] guids = AssetDatabase.FindAssets(filter, relativePath);
            for (int i = 0; i < guids.Length; i++)
            {
                string path = AssetDatabase.GUIDToAssetPath(guids[i]);
                pathList.Add(path);
            }

            return pathList;
        }

        /// <summary>
        /// 删除一个Prefab上的空脚本
        /// </summary>
        /// <param name="path">prefab路径 例Assets/Resources/FriendInfo.prefab</param>
        static bool CheckMissMonoBehavior(string path)
        {
            bool isNull = false;
            string textContent = File.ReadAllText(path);
            Regex regBlock = new Regex("MonoBehaviour");
            // 以"---"划分组件
            string[] strArray = textContent.Split(new string[] { "---" }, StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < strArray.Length; i++)
            {
                string blockStr = strArray[i];
                if (regBlock.IsMatch(blockStr))
                {
                    // 模块是 MonoBehavior
                    //(?<名称>子表达式)  含义:将匹配的子表达式捕获到一个命名组中
                    Match guidMatch = Regex.Match(blockStr, "m_Script: {fileID: (.*), guid: (?<GuidValue>.*?), type: [0-9]}");
                    if (guidMatch.Success)
                    {
                        string guid = guidMatch.Groups["GuidValue"].Value;
                        if (!File.Exists(AssetDatabase.GUIDToAssetPath(guid)))
                        {
                            isNull = true;
                            textContent = DeleteContent(textContent, blockStr);
                        }
                    }

                    Match fileIdMatch = Regex.Match(blockStr, @"m_Script: {fileID: (?<IdValue>\d+)}");
                    if (fileIdMatch.Success)
                    {
                        string idValue = fileIdMatch.Groups["IdValue"].Value;
                        if (idValue.Equals("0"))
                        {
                            isNull = true;
                            textContent = DeleteContent(textContent, blockStr);
                        }
                    }
                }
            }
            if (isNull)
            {
                // 有空脚本 写回prefab
                File.WriteAllText(path, textContent);
            }
            return isNull;
        }

        // 删除操作
        static string DeleteContent(string input, string blockStr)
        {
            input = input.Replace("---" + blockStr, "");
            Match idMatch = Regex.Match(blockStr, "!u!(.*) &(?<idValue>.*?)\n");
            if (idMatch.Success)
            {
                // 获取 MonoBehavior的fileID
                string fileID = idMatch.Groups["idValue"].Value;
                Regex regex = new Regex("  - (.*): {fileID: " + fileID + "}\n");
                input = regex.Replace(input, "");
            }

            return input;
        }
    }
}

可以看到删完之后,文件是非常干净的,就只剩下transform组件:

 

这里仔细观察.prefab文件,你会发现自定义的Script组件在文件中和Unity自带的transform组件的形式是不一样的。来我们对比一下。更深入的我也就先不细纠了,如果有需要再说:

第二种思路:通过Unity来反序列化prefab,然后序列化组件,直接删除,保存修改到本地。

但是我感觉不太好用。目前没有找到最佳答案,所以还是建议直接修改文件。或者用Unity最新版,Unity给直接提供了API可以调用,删除组件。


心得:

正则表达式真的很好用,一定要学会熟练使用。


参考博客:

https://www.cnblogs.com/elfnaga/p/5672986.html

https://connect.unity.com/p/unityeditorzhi-tong-guo-serializedobjectlei-shi-xian-pi-liang-xiu-gai-prefabzhong-zu-jian-can-shu

https://www.cnblogs.com/luguoshuai/p/12323186.html

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页