【Unity3d开发笔记】-FPS- 通过代码改变物体的组件Components在Inspector内的排序
FPS游戏一般有多个阵营,对于带有联机的FPS,一般还要准备原角色的同步版本,这样下来可能需要制作多个预制体。同步角色与可控角色的区别,主要是其物体对象悬挂的组件的类型和顺序差别。
给场景动态添加一个角色的方式有很多:
- 可以直接从做好的预制体中
Instantiate
一份,只不过可能需要制作多个预制体,每个阵营得分别准备可控角色与同步角色; - 也可以从零开始创建GameObject,然后按照预先设计好的程序把各个部件按照一定Transform的层次装载好,并为一些脚本的公共变量初始化;
- 和第一种类似,但是每个阵营只制作可控角色的预制体,然后通过组件重新排序与设置
enabled
等操作形成同步角色版本。
我采用的就是第3种方案,当然过程中也遇到很多问题,主要在于U3D通过脚本来改变一个物体的组件悬挂顺序(表现在Inspector面板内组件顺序的改变),似乎很费劲。
原因在于:
- 没有特别直观方便的API访问并修改一个物体的组件顺序;
- 对于用
GetComponents<T>()
获取的组件数组直接交换其数组元素的顺序,并不会改变Inspector内的组件顺序;
当然,采用方案1和2就能完成的事,何必用这种晦涩的歪路子解决问题呢。我可能当时脑子坏了,偏偏要在方案3上"浪费时间"。不过好在通过一下午的搜集资料与调试,找到了方案3的一种可行实现。
我的FPS项目对于组件调整的需求是:把指定类型的组件"调到"同类型组件中的最前面,比如下面左图到右图所示。
(a) 红色阵营角色预制体的组件顺序 | (b) 图a中预制体作为同步角色实例化后的组件顺序 |
图1 组件排序代码执行前后的情况
谈组件调整的具体实现之前,可能有人会问,这么做的意义何在?其实,是我为了"偷懒",给每个阵营角色的预制体同时绑定了可控角色脚本PlayerController
以及同步角色脚本SyncPlayerController
(共同派生于BasePlayer
),这两个组件上面有很多预先赋值好的公共成员,动态挂载起来又麻烦,于是便想通过调整这两个组件的先后顺序决定角色的类型——可控角色的PlayerController
组件先于SyncPlayerController
,同步角色则与之相反。
【思路概要】
函数原型:static void changeComponentsOrder<T>(GameObject obj, System.Type type) where T:MonoBehaviour;
- 获取目标物体
obj
的所有带有enabled属性的组件集合——comps : Behaviour[]
- 遍历组件集合
comps
,直到找到第一个类型为给定类型的组件元素,把它置换到comps[0]
处 - 遍历调整顺序后的
comps
,每轮迭代中添加同类型的"空组件"(指没有给字段赋值的组件),利用System.Reflection.FieldInfo
以及System.Type.GetField()
,获取该类型组件字段的所有值,然后通过FieldInfo.SetValue()
赋值给"空组件"的字段 –利用反射复制组件的方法链接 - 最后通过
DestroyImmediate()
而不是Destroy()
来删除comps
对应的组件,因为后者是异步删除物体,删除时点具有不确定性,被这个坑惨了;前者是在主线程中立刻删除物体。 –参考链接
【代码】
static void changeComponentsOrder<T>(GameObject obj, System.Type type)
where T:MonoBehaviour
{
Behaviour[] comps = obj.GetComponents<T>();
if (type == comps[0].GetType())
{ //无需交换
Debug.Log(obj.name + " No Need to change");
return;
}
for (int i = 1; i < comps.Length; ++i)
{
//Debug.Log("Component: " + c.GetType());
if (comps[i].GetType() == type)
{
//交换到最前面
Behaviour temp = comps[i];
comps[i] = comps[0];
comps[0] = temp;
break;
}
}
//按新顺序添加脚本
foreach (Behaviour c in comps)
{
System.Type type_i = c.GetType();
Behaviour copy = obj.AddComponent(type_i) as Behaviour;
System.Reflection.FieldInfo[] fields = type_i.GetFields();//获得该类型组件的所有字段值
foreach (System.Reflection.FieldInfo field in fields)
{
field.SetValue(copy, field.GetValue(c));
}
}
Debug.Log("Before destroy : " + obj.GetComponents<Component>().Length); //AA
//立即销毁装载的脚本
foreach (Behaviour c in comps)
{
//MonoBehaviour.Destroy(c); //别用
MonoBehaviour.DestroyImmediate(c);
}
Debug.Log("After destroy : " + obj.GetComponents<Component>().Length); //BB
}
【特别注意】
- 所有待复制组件的预先赋值字段最好设为
public
,除非是简单类型,否则不要考虑使用[SerializeField] + private
,否则这些字段的值在复制过程中会丢失(亲测); - 使用
DestroyImmediate()
而不是Destroy()
,否则会有诸如GetComponent<T>()
时得到预料之外的null
的情况;另外,通过在destroy()
方法前后放各方一个调试输出语句AA
和BB
,就会发现其数目是一样的,并没有减少,这就是异步删除带来的"后果"。
以上方法是参考大佬们的文章并结合个人实践总结出来的,但不代表它一定适用于所有情况。且以上描述难免有不妥之处,欢迎各位大佬们批评指正~
上篇文章链接:【Unity3d开发笔记】 -FPS- GameObject.GetComponent<T>()获取组件的顺序