在Unity编辑器中实现组件的复制与粘贴:完整指南

发布于:2024-10-09 ⋅ 阅读:(46) ⋅ 点赞:(0)

内容将会持续更新,有错误的地方欢迎指正,谢谢!
 

在Unity编辑器中实现组件的复制与粘贴:完整指南
     
TechX 坚持将创新的科技带给世界!

拥有更好的学习体验 —— 不断努力,不断进步,不断探索
TechX —— 心探索、心进取!

助力快速掌握 组件复制与粘贴

为初学者节省宝贵的学习时间,避免困惑!


前言:

  在Unity的开发过程中,特别是当你在编辑场景中处理多个对象时,如何高效地管理和操作组件是一项关键技能。手动为每个GameObject重新添加组件,尤其是在处理复杂场景时,是一个耗时的过程。

  为此,本文将介绍如何在Unity编辑器中编写一个工具,允许你复制和粘贴组件,并提供多种粘贴模式,如序列化和非序列化,同时忽略某些不必要的组件。你还可以根据需要清除特定组件。



一、创建组件忽略列表


在复制组件的时候,我们允许忽略一些组件不进行复制,那么在复制组件的时候就不会复制这些类型的组件,同时也不会进行粘贴。

using UnityEngine;
using System.Collections.Generic;

namespace CopyPasteComponents.Editor
{
    [CreateAssetMenu(fileName = "ComponentsIgnoreList", menuName = "ScriptableObjects/ComponentsIgnoreList", order = 1)]
	public class ComponentsIgnoreList : ScriptableObject
	{
        public List<string> IgnoreList = new List<string>();
    }
}

在这里我们对Transform、MeshRenderer和MeshFilter组件进行忽略,因为这3个组件是组成一个游戏对象最基本的组件,一般不需要进行复制粘贴。

在这里插入图片描述



二、实现组件复制粘贴删除功能


以下是组件的复制粘贴与删除的具体功能:

public class CopyPasteComponents
{
    static Component[] copiedComponentCaches;
    static Dictionary<GameObject, Component[]> pastedComponentCaches = new Dictionary<GameObject, Component[]>();
    static ComponentsIgnoreList componentsIgnoreList;
    static Type[] ignoreComponentTypes;

    [InitializeOnLoadMethod]
    static void Init()
    {
        componentsIgnoreList = Resources.Load<ComponentsIgnoreList>("ComponentsIgnoreList");

        Assembly assembly = Assembly.Load("UnityEngine.CoreModule");

        ignoreComponentTypes = componentsIgnoreList.IgnoreList.Select(item =>assembly.GetType(item)).ToArray();
    }
 
    static Component GetNewAddComponents(GameObject targetObj)
    {
        // 获取粘贴后的SerializedObject
        SerializedObject serializedObjectAfter = new SerializedObject(targetObj);
        SerializedProperty componentProperty = serializedObjectAfter.FindProperty("m_Component");
        serializedObjectAfter.Update();

        // 获取新添加的组件
        SerializedProperty newComponentProperty = componentProperty.GetArrayElementAtIndex(componentProperty.arraySize - 1);
        SerializedProperty componentReference = newComponentProperty.FindPropertyRelative("component");
        Object componentObject = componentReference.objectReferenceValue;
        return componentObject as Component;
    }

    static void StorePastedComponent(GameObject targetObj, Component[] component)
    {
        if (!pastedComponentCaches.ContainsKey(targetObj))
        {
            pastedComponentCaches.Add(targetObj, null);
        }

        pastedComponentCaches[targetObj] = component;
    }

    static bool HasDisallowMultipleComponent(Type type)
    {
        object[] attributes = type.GetCustomAttributes(typeof(DisallowMultipleComponent), false);
        return attributes.Length > 0;
    }

2.1、复制组件


首先,让我们来看看如何实现组件的复制功能。我们提供了三种不同的组件复制方法:复制所有组件、复制自定义Mono组件以及复制内置组件。

2.1.1 复制所有组件


这里通过GetComponents()来获取对象上的所有继承了Component的组件,包含所有的内建组件和Mono组件。

同时对于在忽略列表里面的组件类型不会进行复制。

[MenuItem("GameObject/ComponentsCopy_Paste/Copy All Components", priority = 0)]
static void CopyAllComponents(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    copiedComponentCaches = targetObj.GetComponents<Component>();

    copiedComponentCaches = copiedComponentCaches.Where(item => !ignoreComponentTypes.Contains(item.GetType())).ToArray();
}

2.1.2 复制所有自定义Mono脚本组件


这里通过GetComponents()来获取对象上的所有的自定义脚本组件。

同时对于在忽略列表里面的组件类型不会进行复制。

 [MenuItem("GameObject/ComponentsCopy_Paste/Copy Mono Components", priority = 1)]
 static void CopyMonoComponents(MenuCommand menuCommand)
 {
     GameObject targetObj = menuCommand.context as GameObject;

     copiedComponentCaches = targetObj.GetComponents<MonoBehaviour>();

     copiedComponentCaches = copiedComponentCaches.Where(item => !ignoreComponentTypes.Contains(item.GetType())).ToArray();
 }

2.1.3 复制所有内建组件


这里通过GetComponents()来获取对象上的所有继承了Component的组件,并通过判断获取的到的组件是否继承自MonoBehaviour,如果不继承MonoBehaviour那么认为是内建组件。

同时对于在忽略列表里面的组件类型不会进行复制。

[MenuItem("GameObject/ComponentsCopy_Paste/Copy Built-in Components", priority = 2)]
static void CopyBuiltInComponents(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    copiedComponentCaches = targetObj.GetComponents<Component>();

    copiedComponentCaches = copiedComponentCaches.Where(item => !typeof(MonoBehaviour).IsAssignableFrom(item.GetType())).ToArray();

    copiedComponentCaches = copiedComponentCaches.Where(item => !ignoreComponentTypes.Contains(item.GetType())).ToArray();
}

2. 2、粘贴组件


粘贴功能分为多种模式,用户可以选择是否将复制的组件序列化粘贴,或者选择以附加模式粘贴(保留原有组件),或者选择覆盖模式(覆盖同类型组件)。

在粘贴时,我们会检查目标GameObject上是否已经存在相同类型的组件。如果不允许重复添加的组件已经存在,则不会粘贴该组件。

2.2.1 附加模式粘贴组件


以附加模式进行粘贴组件会在不改变对象所有的原有组件的情况下,对对象添加新的组件,在这种模式下提供非序列化和序列化两种方法。

当使用非序列化模式时,这个时候就只会添加组件并不会对组件复制,相当于使用AddComponent()方法添加组件。

当使用序列化模式时,这个时候不仅会添加组件同时还会把组件的值也复制过来,相当与组件右键菜单中的Copy Component 和Paste Component As New。

[MenuItem("GameObject/ComponentsCopy_Paste/Paste Components Additional_NoSerialize", priority = 13)]
static void PasteComponents_Additional_NoSerialize(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    Component[] newAddComponents = PasteComponents_Additional(targetObj, false, out List<Component> ignoreComponents);

    StorePastedComponent(targetObj, newAddComponents);
}

[MenuItem("GameObject/ComponentsCopy_Paste/Paste Components Additional_Serialize", priority = 14)]
static void PasteComponents_Additional_Serialize(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    Component[] newAddComponents = PasteComponents_Additional(targetObj, true, out List<Component> ignoreComponents);

    StorePastedComponent(targetObj, newAddComponents);
}
 
static Component[] PasteComponents_Additional(GameObject targetObj, bool isSerialize, out List<Component> ignoreComponents)
{
    List<Component> newAddComponents = new List<Component>();

    ignoreComponents = new List<Component>();

    for (int i = 0; i < copiedComponentCaches.Length; i++)
    {
        Component[] targetComs = targetObj.GetComponents(copiedComponentCaches[i].GetType());

        //对于新添加的组件,需要判断该组件是否允许多次添加,
        //如果允许直接添加,如果不允许,判断是否已经存在,如果存在,则不粘贴,如果不存在,则粘贴

        if (targetComs == null || !HasDisallowMultipleComponent(copiedComponentCaches[i].GetType()))
        {
            if (isSerialize)
            {
                ComponentUtility.CopyComponent(copiedComponentCaches[i]);

                ComponentUtility.PasteComponentAsNew(targetObj);
            }
            else
            {
                targetObj.AddComponent(copiedComponentCaches[i].GetType());
            }
        }
        else
        {
            ignoreComponents.Add(copiedComponentCaches[i]);
        }

        Component newAddCom = GetNewAddComponents(targetObj);

        if (!newAddComponents.Contains(newAddCom))
            newAddComponents.Add(newAddCom);
    }

    return newAddComponents.ToArray();
}

2.2.2 覆盖模式粘贴组件


以覆盖模式进行粘贴组件在粘贴组件时会改变对象的原有组件,在为对象粘贴组件时,在这种模式下提供非序列化和序列化两种方法。

当使用非序列化模式时,如果粘贴组件时原对象上已经有同类型的组件,那么不对该组件做任何处理,对于粘贴的组件在原对象上没有的或则同类型粘贴的组件比原对象多的都会添加为新脚本,因为是非序列化所以新家的脚本不进行赋值。

当使用序列化模式时,对于同类型的组件会对原有组件进行赋值,对于粘贴的组件在原对象上没有的或则同类型粘贴的组件比原对象多的都会添加为新脚本并且进行赋值。

[MenuItem("GameObject/ComponentsCopy_Paste/Paste Components Override_NoSerialize", priority = 15)]
static void PasteComponents_Override_NoSerialize(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    Component[] newAddComponents = PasteComponents_Override(targetObj, false, out List<Component> ignoreComponents);

    StorePastedComponent(targetObj, newAddComponents);
}

[MenuItem("GameObject/ComponentsCopy_Paste/Paste Components Override_Serialize", priority = 16)]
static void PasteComponents_Override_Serialize(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    Component[] newAddComponents = PasteComponents_Override(targetObj, true, out List<Component> ignoreComponents);

    StorePastedComponent(targetObj, newAddComponents);
}

static Component[] PasteComponents_Override(GameObject targetObj, bool isSerialize, out List<Component> ignoreComponents)
{
    List<Component> targetComCaches = new List<Component>();

    List<Component> newAddComponents = new List<Component>();

    ignoreComponents = new List<Component>();

    for (int i = 0; i < copiedComponentCaches.Length; i++)
    {
        ComponentUtility.CopyComponent(copiedComponentCaches[i]);

        //刷新顺序依赖于GetComponents的顺序
        Component[] targetComs = targetObj.GetComponents(copiedComponentCaches[i].GetType());
        Component targetCom = null;

        //剔除已经操作过的组件
        if (targetComs != null)
            targetCom = targetComs.Where(item => !targetComCaches.Contains(item))?.FirstOrDefault();

        if (targetCom == null)
        {
            //对于新添加的组件,需要判断该组件是否允许多次添加,
            //如果允许直接添加,如果不允许,判断是否已经存在,如果存在,则不粘贴,如果不存在,则粘贴
            if (targetComs == null || !HasDisallowMultipleComponent(copiedComponentCaches[i].GetType()))
            {
                if (isSerialize)
                    ComponentUtility.PasteComponentAsNew(targetObj);  // 粘贴新的组件
                else
                    targetObj.AddComponent(copiedComponentCaches[i].GetType());
            }
            else
            {
                ignoreComponents.Add(copiedComponentCaches[i]);
            }

            targetCom = GetNewAddComponents(targetObj);

            if (!newAddComponents.Contains(targetCom))
                newAddComponents.Add(targetCom);
        }
        else
        {
            //对已经存在的脚本,不序列化时不做任何处理
            if (isSerialize)
                ComponentUtility.PasteComponentValues(targetCom);
        }

        targetComCaches.Add(targetCom);
    }

    return newAddComponents.ToArray();
}

2.3、删除组件


为防止粘贴过多组件后影响GameObject原有的结构,我们也提供了清除组件的功能。清除功能包括:

  • 清除粘贴的组件: 仅删除通过工具粘贴的组件。
  • 清除所有组件: 删除除忽略列表之外的所有组件。
  • 清除粘贴的组件: 删除Mono组件。
  • 清除所有组件: 删除内建组件。

清除粘贴的组件只能删除从粘贴而来的组件,在粘贴组件时会将对象和组件放入缓存中,删除时只能删除与缓存一样的组件,不可以影响到原有的组件。

在删除所有组件时需要注意,这里会忽略掉忽略列表中的组件。

在删除内建组件的时候注意,Transform组件是不允许删除的。

[MenuItem("GameObject/ComponentsCopy_Paste/Clear Pasted Components", priority = 28)]
static void ClearPastedComponents(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    if (pastedComponentCaches.ContainsKey(targetObj))
    {
        Component[] components = pastedComponentCaches[targetObj];

        foreach (var item in components)
        {
            Object.DestroyImmediate(item);
        }
    }
}

[MenuItem("GameObject/ComponentsCopy_Paste/Clear All Components", priority = 29)]
static void ClearAllComponents(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    var components = targetObj.GetComponents<Component>();

    components = components.Where(item => !ignoreComponentTypes.Contains(item.GetType())).ToArray();

    for (int i = 0; i < components.Length; i++)
    {
        Object.DestroyImmediate(components[i]);
    }
}

[MenuItem("GameObject/ComponentsCopy_Paste/Clear Mono Components", priority = 29)]
static void ClearMonoComponents(MenuCommand menuCommand)
{
    GameObject targetObj = menuCommand.context as GameObject;

    var components = targetObj.GetComponents<MonoBehaviour>();

    components = components.Where(item => !ignoreComponentTypes.Contains(item.GetType())).ToArray();

    for (int i = 0; i < components.Length; i++)
    {
        Object.DestroyImmediate(components[i]);
    }
}

[MenuItem("GameObject/ComponentsCopy_Paste/Clear Built-in Components", priority = 29)]
 static void ClearBuiltInComponents(MenuCommand menuCommand)
 {
     GameObject targetObj = menuCommand.context as GameObject;

     var components = targetObj.GetComponents<Component>();

     components = components.Where(item => !typeof(MonoBehaviour).IsAssignableFrom(item.GetType()) && item.GetType() != typeof(Transform)).ToArray();

     for (int i = 0; i < components.Length; i++)
     {
         Object.DestroyImmediate(components[i]);
     }
 }


三、项目地址


以下是项目地址,已经整理成了Package包,有需要的小伙伴门可以自取:

https://gitcode.com/CTLittleNewbie/com.fxb.copypastecomponents_v1.0.0/overview





TechX —— 心探索、心进取!

每一次跌倒都是一次成长

每一次努力都是一次进步


END
感谢您阅读本篇博客!希望这篇内容对您有所帮助。如果您有任何问题或意见,或者想要了解更多关于本主题的信息,欢迎在评论区留言与我交流。我会非常乐意与大家讨论和分享更多有趣的内容。
如果您喜欢本博客,请点赞和分享给更多的朋友,让更多人受益。同时,您也可以关注我的博客,以便及时获取最新的更新和文章。
在未来的写作中,我将继续努力,分享更多有趣、实用的内容。再次感谢大家的支持和鼓励,期待与您在下一篇博客再见!