Unity:如何将未知脚本动态附加到游戏对象(自定义编辑器)

Unity: How to dynamically attach an unknown script to a GameObject (custom editor)

我目前正在为 Unity 编辑器(自定义检查器和自定义 windows)制作一个系统,该系统将自动化并使我们正在制作的游戏的美工人员更轻松,但我已经撞墙了。

我正在尝试找到一种方法,通过编辑器文本字段输入和 GUI 按钮将未知脚本动态添加到场景中的游戏对象。 artist/programmer 将在文本字段中键入脚本的名称,它会搜索并添加到游戏对象,但我不知道如何进行此操作,特别是因为 gameObject.AddComponent() 的某些功能已被弃用从 Unity 5.3 开始

这是我尝试做的事情:

public string scriptname;
GameObject obj = null;
scriptname = EditorGUILayout.TextField("Script name:", scriptname, GUILayout.MaxHeight(25));
if (GUILayout.Button("Attach script"))
{
    //search for the script to check if it exists, using DirectoryInfo
    DirectoryInfo dir = new DirectoryInfo(Application.dataPath);
    FileInfo[] info = dir.GetFiles("*.*", SearchOption.AllDirectories);
    foreach (FileInfo f in info) // cycles through all the files
    {
        if(f.Name == scriptname)
        {
            //attaches to the gameobject (NOT WORKING)
            System.Type MyScriptType = System.Type.GetType(scriptname + ",Assembly-CSharp"); 
            obj.AddComponent(MyScriptType);
        }
    }
}

(当然,这是一个总结版,我从脚本的不同部分复制了相关行)。

但是没用。 有什么想法吗?

我建议这样做:

if(GUILayout.Button("Attach script"))
{
    // check if type is contained in your assembly:
    Type type = typeof(MeAssemblyType).Assembly.GetTypes().FirstOrDefault(t => t.Name == scriptname);
    if(type != null)
    {
        // script exists in the same assembly that MeAssemblyType is
        obj.AddComponent(type); // add the component
    }
    else
    { 
        // display some error message
    }
}

当然,如果您使用一些包含其他组件的插件(依赖项),这将失败,但要解决这个问题,您可以只检查程序集的依赖项:

typeof(MeAssemblyType) // your type from Assembly-CSharp 
    .Assembly // Assembly-CSharp assembly
    .GetReferencedAssemblies() // get referenced assemblies
    .FirstOrDefault(m => 
        m.Assembly // from this assembly
        .GetTypes() // get all types
        .FirstOrDefault(t => 
            t.Name == scriptname // select first one that matches the name
        )
    )

备注:

GetReferencedAssemblies 方法只会 return 由您的程序集"used"(加载)的程序集。为清楚起见,假设您正在引用这些程序集:

  1. System.Xml,
  2. NewtonsoftJson

还有这段代码:

static void Main()
{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml(<some_xml_input>);
}

然后 GetReferencedAssemblies 的输出看起来有点像:

>>> System.Xml, Version=<version>, Culture=neutral, PublicKeyToken=<key>

意味着它不会加载 NewtonsoftJson 因为它没有在该程序集内部使用。

更好的建议:

我建议您混合使用 答案中的方法,但不要加载程序集,因为当 Unity 的编辑器启动您的项目时它们已经加载。而是使用 GetReferencedAssemblies 方法,您应该从那里调用 GetTypes 方法来检索该程序集中所有可能的类型。 (它会很慢,但会保证你得到想要的结果)之后你可以使用 FirstOrDefault 或者自己遍历 Type[] 来找到你想要的。

这还是有可能的。使用这个

UnityEngineInternal.APIUpdaterRuntimeServices.AddComponent(GameObject go, "", string componentName);

希望对您有所帮助

经过广泛的实验,我得到了这个。这也涵盖 所有 Unity 组件。只是使它成为一种使生活更轻松的扩展方法。

public static class ExtensionMethod
{
    public static Component AddComponentExt(this GameObject obj, string scriptName)
    {
        Component cmpnt = null;


        for (int i = 0; i < 10; i++)
        {
            //If call is null, make another call
            cmpnt = _AddComponentExt(obj, scriptName, i);

            //Exit if we are successful
            if (cmpnt != null)
            {
                break;
            }
        }


        //If still null then let user know an exception
        if (cmpnt == null)
        {
            Debug.LogError("Failed to Add Component");
            return null;
        }
        return cmpnt;
    }

    private static Component _AddComponentExt(GameObject obj, string className, int trials)
    {
        //Any script created by user(you)
        const string userMadeScript = "Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
        //Any script/component that comes with Unity such as "Rigidbody"
        const string builtInScript = "UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "Image"
        const string builtInScriptUI = "UnityEngine.UI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "Networking"
        const string builtInScriptNetwork = "UnityEngine.Networking, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "AnalyticsTracker"
        const string builtInScriptAnalytics = "UnityEngine.Analytics, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";

        //Any script/component that comes with Unity such as "AnalyticsTracker"
        const string builtInScriptHoloLens = "UnityEngine.HoloLens, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";

        Assembly asm = null;

        try
        {
            //Decide if to get user script or built-in component
            switch (trials)
            {
                case 0:

                    asm = Assembly.Load(userMadeScript);
                    break;

                case 1:
                    //Get UnityEngine.Component Typical component format
                    className = "UnityEngine." + className;
                    asm = Assembly.Load(builtInScript);
                    break;
                case 2:
                    //Get UnityEngine.Component UI format
                    className = "UnityEngine.UI." + className;
                    asm = Assembly.Load(builtInScriptUI);
                    break;

                case 3:
                    //Get UnityEngine.Component Video format
                    className = "UnityEngine.Video." + className;
                    asm = Assembly.Load(builtInScript);
                    break;

                case 4:
                    //Get UnityEngine.Component Networking format
                    className = "UnityEngine.Networking." + className;
                    asm = Assembly.Load(builtInScriptNetwork);
                    break;
                case 5:
                    //Get UnityEngine.Component Analytics format
                    className = "UnityEngine.Analytics." + className;
                    asm = Assembly.Load(builtInScriptAnalytics);
                    break;

                case 6:
                    //Get UnityEngine.Component EventSystems format
                    className = "UnityEngine.EventSystems." + className;
                    asm = Assembly.Load(builtInScriptUI);
                    break;

                case 7:
                    //Get UnityEngine.Component Audio format
                    className = "UnityEngine.Audio." + className;
                    asm = Assembly.Load(builtInScriptHoloLens);
                    break;

                case 8:
                    //Get UnityEngine.Component SpatialMapping format
                    className = "UnityEngine.VR.WSA." + className;
                    asm = Assembly.Load(builtInScriptHoloLens);
                    break;

                case 9:
                    //Get UnityEngine.Component AI format
                    className = "UnityEngine.AI." + className;
                    asm = Assembly.Load(builtInScript);
                    break;
            }
        }
        catch (Exception e)
        {
            //Debug.Log("Failed to Load Assembly" + e.Message);
        }

        //Return if Assembly is null
        if (asm == null)
        {
            return null;
        }

        //Get type then return if it is null
        Type type = asm.GetType(className);
        if (type == null)
            return null;

        //Finally Add component since nothing is null
        Component cmpnt = obj.AddComponent(type);
        return cmpnt;
    }
}

用法:

gameObject.AddComponentExt("YourScriptOrComponentName");

了解我是如何做到的很重要,这样您就可以在以后的任何 Unity 更新中添加对新组件的支持。

对于用户创建的任何脚本:

1.找出Assembly.Load函数中的???需要什么.

Assembly asm = Assembly.Load("???");

您可以通过将其放入脚本中来做到这一点:

Debug.Log("Info: " + this.GetType().Assembly);

我得到了:Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

我们现在应该用它替换 ???

Assembly asm = Assembly.Load("Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");

2.找出asm.GetType函数中的???需要什么

Assembly asm = Assembly.Load("Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
Type type = asm.GetType(???); 

在本例中,它只是您要添加到游戏对象的脚本的名称。

假设您的脚本名称是 NathanScript:

Assembly asm = Assembly.Load("Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
Type type = asm.GetType("NathanScript"); 
gameObject.AddComponent(type);

对于 Unity 内置 scripts/components 非用户创建的脚本:

例如 RigidbodyLinerendererImage 组件。任何不是由用户创建的组件。

1.找出Assembly.Load函数中的???需要什么.

Assembly asm = Assembly.Load("???");

您可以通过将其放入脚本中来做到这一点:

ParticleSystem pt = gameObject.AddComponent<ParticleSystem>();
Debug.Log("Info11: " + pt.GetType().Assembly);

我得到了:UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

我们现在应该用它替换 ???

Assembly asm = Assembly.Load("UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");

2.找出asm.GetType函数中的???需要什么

Assembly asm = Assembly.Load("UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
Type type = asm.GetType(???); 

您可以通过将其放入脚本中来做到这一点:

ParticleSystem pt = gameObject.AddComponent<ParticleSystem>();
Debug.Log("Info: " + pt.GetType());

我得到了:UnityEngine.ParticleSystem

记住这里以ParticleSystem为例。因此,将转到 asm.GetType 函数的最终字符串将按如下方式计算:

string typeString = "UnityEngine." + componentName;

假设您要添加的组件是 LineRenderer:

Assembly asm = Assembly.Load("UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null");
string typeString = "UnityEngine." + "LineRenderer";
Type type = asm.GetType(typeString); 
gameObject.AddComponent(type);

用扩展方法把它放在一起:

如您所见,添加您创建的脚本和 Unity 附带的 script/components 需要完全不同的过程。您可以通过检查类型是否 null 来解决此问题。如果类型是null,则执行其他步骤。如果另一个步​​骤也是 null 那么脚本只是 not 退出。

反编译Unity的AddComponentWindow。还添加了 link:
AddComponentAdjusted

然后像这样调用 window:

  ws.winx.editor.windows.AddComponentWindow.Show(rect);

            ws.winx.editor.windows.AddComponentWindow.OnClose += OnCloseComponentSelectedFromPopUpMenu;
            ws.winx.editor.windows.AddComponentWindow.ComponentSelected += (menuPath) => ComponentSelectedFromPopUpMenu(positionData.Item1, menuPath);

句柄return

    private void ComponentSelectedFromPopUpMenu(Vector2 position, string menuPath) {
        
        
                    MonoScript monoScript;
        
                    char[] kPathSepChars = new char[]
                    {
                        '/',
                        '\'
                    };
        
                    menuPath = menuPath.Replace(" ", "");
                    string[] pathElements = menuPath.Split(kPathSepChars);
        
                    string fileName = pathElements[pathElements.Length - 1].Replace(".cs", "");
        
        
        
        
                    if (pathElements[0] == "Assets") {
        
                        Debug.LogWarning("Unity need to compile new added file so can be included");
        
        
                    } else if (pathElements.Length == 2) {
        
//use fileName
                        //do something
        
                        
                    } else if (pathElements[1] == "Scripts") {//Component/Scripts/MyScript.cs
                        
        
                        string[] guids = AssetDatabase.FindAssets("t:Script " + fileName.Replace(".cs", ""));
        
                        if (guids.Length > 0) {
        
                            for (int i = 0; i < guids.Length; i++) {
                                monoScript = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]), typeof(MonoScript)) as MonoScript;
                                Type typet = monoScript.GetClass();
        
                                if (typet == null) continue;
        
        
                               
        
                    } else {//Component/Physics/Rigidbody
                        //try to find by type, cos probably Unity type
                        Type unityType = ReflectionUtility.GetType("UnityEngine." + fileName);
        
                        if (unityType != null) {
        
    //do something
        
                            return;
        
                        }
        
        
        
        
        
        //Based on attribute  [AddComponentMenu("Logic/MyComponent")] 
                        //Component/Logics/MyComponent
                        string[] guids = AssetDatabase.FindAssets("t:Script " + fileName.Replace(".cs", ""));
        
                        if (guids.Length > 0) {
        
                            for (int i = 0; i < guids.Length; i++) {
                                monoScript = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guids[i]), typeof(MonoScript)) as MonoScript;
                                Type typet = monoScript.GetClass();
        
                                if (typet == null) continue;
        
                                object[] addComponentMenuAttributes = typet.GetCustomAttributes(typeof(AddComponentMenu), true);
        
        
        
                                if (addComponentMenuAttributes != null && addComponentMenuAttributes.Length > 0 && "Component/" + ((AddComponentMenu)addComponentMenuAttributes[0]).componentMenu == menuPath)
                                {
        
                                    //do somethings
        
                                }
                            }
        
        
                        }
        
        
                    }
                }