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?
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.
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:
- System.Xml,
- 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.
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 } } } } }