- Home /
Generate Script in Editor Script and GetType() returns null.
So I've got an Editor Script which holds a MenuItem, which when called, should create a new script with a supplied name, and automatically add the new script as a component to an object. This will never be done during play mode, so speed isn't an issue. (Obviously a 5 day wait is out of the question).
However I can create the script, import it into the asset database, that's all fine, the issue is when I want to add it as a component.
If I use GetType(), and supply the new script as a string, I always get null returned. I've tried supplying different Assembly strings, to no avail. I've tried using Activator.CreateInstance(), and for some reason I get a completely unrelated error with a third party plugin.
I don't have the code at hand, but this is what I'm doing:
public class Foo : Editor
{
[MenuItem(...)]
public static void Bar()
{
string newClassName = "FooBar"; //This is actually gotten from a save dialog box.
string[] lines = File.ReadAllLines( "Template.cs" );
foreach( line in lines ) // I'm aware you can't re-assign line, but pesudocode.
{
line = line.replace("CLASS_NAME", newClassName);
}
File.WriteAllLines( newClassName + ".cs", lines);
GameObject go = new GameObject();
AssetDatabase.Refresh( ImportAssetDatabase.LoadSync );
Type type = Type.GetType( newClassName );
// type is null.
go.AddComponent( type );
}
}
Is there anyway to properly refresh the AssetDatabase and gain access to the type? Am I doing this the wrong way?
(The worst part of all this, is I got it to work, changed something, and never got it to work again. In my frantic Ctrl/Cmd+Z frenzy, guess what idiot hit another key. Anyway my point is, I know this is possible, which is why I haven't given up.)
Any ideas?
(Clarify: I want to get the Type of a generated script. For some reason AssetDatabase.Refresh() still makes GetType() return null even though it's done a full recompile.)
I might not know much about GetType() but if you can specify the type can't you do something like GetType(ClassName) as ClassName? $$anonymous$$aybe you should try debugging where exactly things start to fail. (To see if it's the class name retrieval that is causing the issue) Also, I'm not quite sure but you'd want to check your compile order. I noticed your class was ".cs" and the code above in JS. Not sure if that has anything to do with it. Finally, according to this: http://docs.unity3d.com/ScriptReference/GameObject.AddComponent.html
It seems you can add components by name (String) without having to specify Type. That might be a workaround if you don't need the type specifically. Unfortunately I haven't worked with AssetDatabase so I don't know much haha. EDIT: comment links are annoying
What version of Unity is this? In 5 trying to synchronously import a script is an error.
Solved the issue. It was unity 5.1, and AddComponent(string) is deprecated, so that was a no-go sadly. This was a nightmare to debug as well, because GetType() never gave me any useful errors, and it was all during "Editor-time", so everything was a nightmare. Also @SomeGuy22, it's C#! Don't know how you think it's JS! (Albeit there's a little pesudo in there)
Also trying to Sync and import is an error, but it still works, I think. Some guy argued to keep it in, despite Unity's interest. (Although don't quote me on that)
Oh my goodness I must've been hallucinating xD Sorry I have no clue why I thought it was JS~ I'm just curious as to why AddComponent(string) is deprecated?? Was that recent?
GetComponent with a string argument was bad coding design. It was pron to typos and it is very slow. I also noticed a lot of new developers using this when the generic one was much more fitting.
There are a few very specific situations where it's needed but this is better replaced with using GetType().
Answer by BMayne · Jul 15, 2015 at 01:12 AM
Oh the bane of my Code Generation existence.
@Bunny83 had the correct idea. The only change I would suggest is using an EditorPref and a callback when scripts are generated. Lucky for your this is very easy to do. The thing I hate about it is the fact that it's hard to follow.
internal static void AddComponents(string[] classes)
{
EditorPrefs.SetString(PENDING_CLASSES_KEY, JSON.Dump(classes));
AssetDatabase.Refresh();
}
[DidReloadScripts]
internal static void AddComponentsCheck()
{
if (EditorPrefs.HasKey(PENDING_CLASSES_KEY))
{
string[] pendingClasses;
JSON.MakeInto<string[]>(JSON.Load(EditorPrefs.GetString(PENDING_CLASSES_KEY)), out pendingClasses);
for (int i = 0; i < pendingClasses.Count; i++)
{
//Do your logic with your classes
}
EditorPrefs.DeleteKey(PENDING_CLASSES_KEY);
}
}
}
That is just one stripped out example of how I do it. Keep in mind I use TinyJson. It is really useful for this type of task.
Cheers,
Aaah! You beauty! It's amazing what a nights sleep can do as well. @Bunny83 helped me realise that I definitely need to exit the script before I can do anything more, and this morning I thought about using PlayerPrefs, but EditorPrefs! Never knew!
Answer by Bunny83 · Jul 15, 2015 at 12:39 AM
I would say the way you have it at the moment it's impossible. When Unity (re-) compiles a script it actually compiles all scripts of the same compilation group. As result a new assembly is created (which will replace the old one). However before Unity can exchange / update the assembly, the whole scripting environment is shut down. Before that happens Unity serializes everything it can. Every C# object will be recreated and deserialized after the recompilation. That's why you can't have any code running when Unity does the recompile. The current execution stack can't be serialized and restored.
What might work is, when you save the script name in a member variable of a serializable object as well as the information that you want to add the new component and to which gameobject. An EditorWindow would be the most straight forward. When the recompilation is finished your EditorWindow should receive another OnEnable callback which you can use to check if an "add component task" is pending, execute that task and remove / destroy it.
This is all just a theoretical solution. You might face more problems which might involve thread concurrency depending on how and where you implement your code.
Just to make that clear: Your "Bar" method has to finish before Unity can do it's "serialize->destroy->assembly reload->recreate->deserialize" thing which is required to access the new class.
+1. It may be slightly more correct to use the DidReloadScripts callback but this would be more annoying than sticking the same checking logic in OnEnable since DidReloadScripts can only be used on a static method.
Answer by Valeour · Jul 15, 2015 at 08:59 AM
I don't know the usual convention for posting an answer when someone gave me the answer, so I apologise if this shouldn't be here, but I wanted to share my findings:
So with the glorious input of @BMayne and @Bunny83, here's how it goes:
public class Foo : Editor
{
[MenuItem("Code Generator", false, 0)]
public static void CreateStory()
{
// Get the name wanted, using this as an input dialog.
string path = EditorUtility.SaveFilePanelInProject(...);
if (string.IsNullOrEmpty(path))
{
Debug.LogWarning("No name specified");
return;
}
// Take out all the path junk, and format it.
string className = System.IO.Path.GetFileNameWithoutExtension (path);
string classToFile = className.Replace( " ", "" ).Replace( "-", "" );
//(Basically remove all illegal characters, I know I’m missing a few)
// Creating an object to reference later to add the component to.
GameObject go = new GameObject();
go.name = classToFile;
// Get the location of the new script and the template script.
string newPath = Application.dataPath + "/Scripts/Generated/" + classToFile + ".cs";
string templatePath = Application.dataPath + "/Scripts/Template/Template.txt" //.txt so it doesn’t compile
// Copy the files, this apparently can be done in lots of ways.
File.Copy (templatePath, newPath);
// So copy all the template lines, replace the template names, and write them to the new file.
string[] lines = File.ReadAllLines (newPath);
for(int i = 0; i < lines.Length; i++)
{
lines[i] = lines[i].Replace("CLASS_TEMPLATE", classToFile);
}
File.WriteAllLines (newPath, lines);
// @BMayne’s amazing suggestion, set the name for later reference.
EditorPrefs.SetString ("New Class Name", classToFile);
// You probably don’t need to do both of these, but I’m just making sure.
// (Experiment with the ones you might or might not need)
AssetDatabase.ImportAsset (newPath);
AssetDatabase.Refresh ( ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate );
}
[UnityEditor.Callbacks.DidReloadScripts]
private static void ScriptReloaded( )
{
// If the key doesn’t exist, don’t bother, as we’re not generating stuff.
if (!EditorPrefs.HasKey ("New Class Name"))
{
return;
}
// If they key does exist and the object doesn’t, it’s just a left over key.
string name = EditorPrefs.GetString ("New Class Name");
GameObject go = GameObject.Find( name );
if (go == null)
{
return;
}
// Get the new type from the reloaded assembly!
// (It won’t work without assembly specification, because this
// is an editor script, so the default assembly is “Assembly-CSharp-editor”)
Type type = Types.GetType (name, "Assembly-CSharp");
go.AddComponent( type );
//Delete the key because we don’t need it anymore!
EditorPrefs.DeleteKey("New Class Name");
}
}
Types.GetType() appears to be obsolete. $$anonymous$$now of any alternatives?
Type type = Activator.CreateInstance("Assembly-CSharp", name).Unwrap().GetType();
Did the trick for me, hope it works for you too :)