【Unity3d开发笔记】-FPS- 通过代码改变物体的组件Components在Inspector内的排序

【Unity3d开发笔记】-FPS- 通过代码改变物体的组件Components在Inspector内的排序

FPS游戏一般有多个阵营,对于带有联机的FPS,一般还要准备原角色的同步版本,这样下来可能需要制作多个预制体。同步角色可控角色的区别,主要是其物体对象悬挂的组件的类型和顺序差别。

给场景动态添加一个角色的方式有很多:

  1. 可以直接从做好的预制体中Instantiate一份,只不过可能需要制作多个预制体,每个阵营得分别准备可控角色与同步角色;
  2. 也可以从零开始创建GameObject,然后按照预先设计好的程序把各个部件按照一定Transform的层次装载好,并为一些脚本的公共变量初始化;
  3. 和第一种类似,但是每个阵营只制作可控角色的预制体,然后通过组件重新排序与设置enabled等操作形成同步角色版本。

我采用的就是第3种方案,当然过程中也遇到很多问题,主要在于U3D通过脚本来改变一个物体的组件悬挂顺序(表现在Inspector面板内组件顺序的改变),似乎很费劲。

原因在于:

  1. 没有特别直观方便的API访问并修改一个物体的组件顺序;
  2. 对于用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;

  1. 获取目标物体obj的所有带有enabled属性的组件集合——comps : Behaviour[]
  2. 遍历组件集合comps,直到找到第一个类型为给定类型的组件元素,把它置换到comps[0]
  3. 遍历调整顺序后的comps,每轮迭代中添加同类型的"空组件"(指没有给字段赋值的组件),利用System.Reflection.FieldInfo以及System.Type.GetField(),获取该类型组件字段的所有值,然后通过FieldInfo.SetValue()赋值给"空组件"的字段 –利用反射复制组件的方法链接
  4. 最后通过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


}

【特别注意】

  1. 所有待复制组件的预先赋值字段最好设为public,除非是简单类型,否则不要考虑使用[SerializeField] + private,否则这些字段的值在复制过程中会丢失(亲测);
  2. 使用DestroyImmediate()而不是Destroy(),否则会有诸如GetComponent<T>()时得到预料之外的null的情况;另外,通过在destroy()方法前后放各方一个调试输出语句AABB,就会发现其数目是一样的,并没有减少,这就是异步删除带来的"后果"。

以上方法是参考大佬们的文章并结合个人实践总结出来的,但不代表它一定适用于所有情况。且以上描述难免有不妥之处,欢迎各位大佬们批评指正~

上篇文章链接:【Unity3d开发笔记】 -FPS- GameObject.GetComponent<T>()获取组件的顺序

猜你喜欢

转载自blog.csdn.net/weixin_42430021/article/details/108542461