遇到一个好帖子,所以记下来:
其实说白了就是对“文件”进行处理。所以一定要掌握好文件处理的方法。
今天我们分享一下,如何通过脚本批量修改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 里,修改组件中对应属性,然后再保存下来就可以了。
那么流程如下:
-
读取prefab文件
-
找到对应的transform 节点
-
获取对应组件
-
SerializedObject化组件(序列化组件)
-
修改组件参数
-
保存修改
这里我就不赘述步骤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://www.cnblogs.com/luguoshuai/p/12323186.html