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

◇◆丶佛笑我妖孽 提交于 2019-11-29 12:10:47

After extensive experiment I was able get this. This covers all the Unity components too. Just made it an extension method to make life easier.

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;
    }
}

Usage:

gameObject.AddComponentExt("YourScriptOrComponentName");

It is important to understand how I did it so that you can add support for new components in any future Unity updates.

For any script created by users:

1.Find out what needs to be in the ??? in the Assembly.Load function.

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

You can do that by putting this in your script:

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

I got: Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

We should now replace ??? with that.

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

2.Find out what needs to be in the ??? in the asm.GetType function.

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

In this case, it is simply the name of the script you want to add to the GameObject.

Let's say that your script name is NathanScript:

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

For Unity built in scripts/components scripts not created by users:

Example of this is the Rigidbody, Linerenderer, Image components. Just any component not created by the user.

1.Find out what needs to be in the ??? in the Assembly.Load function.

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

You can do that by putting this in your script:

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

I got: UnityEngine, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

We should now replace ??? with that.

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

2.Find out what needs to be in the ??? in the asm.GetType function.

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

You can do that by putting this in your script:

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

I got: UnityEngine.ParticleSystem

Remember that ParticleSystem is used as an example here. So the final string that will go to the asm.GetType function will be calculated like this:

string typeString = "UnityEngine." + componentName;

Let's say that the Component you want to add is 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);

Putting it together in an extension method:

As you can see, adding scripts you created and the script/components that comes with Unity requires totally different process. You can fix this by checking if the type if null. If the type is null, perform the other step. If the other step is null too then the script simply does not exit.

Mateusz

I would suggest doing this like so :

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
    }
}

Of course this will fail if you use some plugins ( dependencies ) which contains other components but to deal with this you can just check dependencies of your assembly :

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
        )
    )

Remarks :

GetReferencedAssemblies method will only return assemblies that were "used" ( loaded ) by your assembly. To make it a bit clear, assume you're referencing these assemblies:

  1. System.Xml,
  2. NewtonsoftJson

And this piece of code :

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

Then the output of GetReferencedAssemblies will look somewhat like that :

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

Meaning it will not load NewtonsoftJson because it was not used inside of that assembly.

Better suggestion :

I would suggest you mixing up the method from @Programmer answer but not load the assemblies because they are already loaded when Unity's editor starts with your project. Instead use GetReferencedAssemblies method and from there you should call GetTypes method to retrieve all possible types in that assembly. ( It will be slow but will guarantee you the desired results ) After that you can just use FirstOrDefault or just iterate through the Type[] yourself to find the one you want.

This is still possible. Use this

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

Hope that helps

Decompile AddComponentWindow of Unity. Learn how is done. Don't reinvent the wheel. You are lazy see the link

AddComponentAdjusted

Then call the window something like this:

  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);

Handle return (the tricky part, again learn from smart Unity gays)

    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

                                }
                            }


                        }


                    }
                }
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!