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

匿名 (未验证) 提交于 2019-12-03 02:28:01

问题:

I'm currently making a system for the Unity Editor (custom inspector and custom windows) that will automate and make things easier for the artists working on the game we're making, but I've hit a brick wall.

I'm trying to find a way to dynamically add, through an editor Textfield input and a GUI button, an unknown script to a gameobject in the scene. The artist/programmer will type the name of the script in the textfield and it will search and add to a gameobject, but I don't know how to proceed with this, specially since some functions of gameObject.AddComponent() are deprecated as of Unity 5.3

Here's what I tried to do:

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

(Of course, this is a summed up version, I copied the relevant lines from different parts of the script).

But it doesn't work. Any ideas?

回答1:

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.



回答2:

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.



回答3:

This is still possible. Use this

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

Hope that helps



回答4:

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


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