In unity there is a difference between adding a component to a GameObject at runtime and adding a component at editor time. That difference being the necessity for the editor time one to be serialized to disk and loaded later on.
If you look in the *.unity, *.prefab (as well as SOâs for *.asset) youâll see something like this scattered about in it:
--- !u!114 &7255019216938518865
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7255019216938518867}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e903f0bc27efa1547accf61bf2537cb7, type: 3}
m_Name:
m_EditorClassIdentifier:
//SNIP - any serialized fields pertaining to the script
This is how your script is serialized. We have a lot of data here including what gameobject in the scene/prefab weâre attached to, enabled, editor flags, etc⌠but very specifically we have:
m_Script: {fileID: 11500000, guid: e903f0bc27efa1547accf61bf2537cb7, type: 3}
This is what points to what script its attached to. And you may notice⌠thereâs nothing about the actual class name!
Instead there is a guid. Now if you were to go to the script location in the assets folder youâll find a meta file (the file may be hidden if you donât have them configured to be visible). In it youâll find info like so:
fileFormatVersion: 2
guid: e903f0bc27efa1547accf61bf2537cb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
Note the guid matches here. If you also used AssetDatabase and called a method like AssetPathToGUID it would return this same value:
THIS is how it attaches scripts via the serializer. Itâs a file association rather than a type association. This is also why your script file needs to be named the same thing as your class itself. They need to match.
Not that a generic classâs name would not match the filename since it literally represents multiple class types and the class definition doesnât contain any of those names but rather a âTâ or in your case a âCâ and an âEâ.
âŚ
Now someone might come along and say âBut lordofduct, how do dllâs work??? I have a dll and I can add scripts from it!â
When you use a dll the guid will now be associated with the guid connected to the *.dll itself (thereâll be a *.dll.meta file with it when you import it into your project). And the m_Script line will look something like:
m_Script: {fileID: -790813810, guid: a8c78d74a98c0bc46ae90e875babbdaf, type: 3}
Note that the âfileIDâ in this case is a much different number. Where as all the scripts that are directly referenced are always 11500000. Well thatâs because this âfileIDâ holds a hash for the class name. So Unity can actually load that assembly create a table of all the scripts in the dll and their hashed names and use that to lookup which script is to be attached.
Now you might ask âwell why not use that to support generics then???â
You canât.
Cause the problem with generics. That: MyClass<T,K> isnât ACTUALLY the class. Thatâs not how C#/.Net works. Lets say we created this class:
public class MyClass<T>
{
public T value;
}
Then we were to do this:
var obj = new MyClass<string>();
Debug.Log(obj.GetType().FullName);
Debug.Log(obj.GetType().Name);
Youâll get this output:
ConsoleTest02.MyClass`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
MyClass`1
The classâs name is MyClass`1.
We could create another MyClass and weâd get a similar result of:
ConsoleTest02.MyClass`1[[System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
MyClass`1
We could even call GetType().GetGenericDefinition() which returns the System.Type that represents the MyClass.
Whatâs going on here?
Well⌠generics exploits the fact that C#/.Net is a JIT compiled language (just in time). Your program isnât compiled into machine code until it is needed while the program is running. This means you can create types in whole at runtime (this is the same reason you can use âemitâ to create types at runtime).
Problem is your MyClass or other generic variant can have an unknown number of types shoved into T! And the type very well may not exist at runtime when it goes to deserialize!
OK⌠so then why doesnât Unity just you know⌠cause it to exist?
Well, thereâs 2 problems. We donât know what the type is⌠we only know what the type name hashes too. The type name would have to pre-exist to compute the hash to then lookup to get the type. This is why non-generic classes in the dll works⌠we can list all the concrete types in the assembly.
And the other problem is that we also need to support IL2CPP!
C++ is NOT a jitted language. It is compiled ahead of time for its target platform. This means all types must be known at compile time!
How IL2CPP will do this is to look through your code and find every List and MyClass and what not and make sure to create corresponding concrete C++ classes for each and then compile all those. This is why IL2CPP doesnât allow things like âemitâ and the sort.
âŚ
Could Unity maybe engineer a solution to all of this that ensured your generic types were known about? Sure. I could think of ways to hack it into the existing serialization engine. But⌠itâd be a pretty heavy lift and pretty hacky to build it into the existing design of the yaml, or it would require altering the yaml layout (which could have huge ramifications and cause bugs through out the serialization system that relies on the existing yaml format). And for what? Let you attach a generic class?
Just create concrete version of it. Youâre done.
âŚ
Note this doesnât mean in years to come the feature ends up getting added. It could⌠unity added support for generic fields not too long ago. I didnât even know they worked cause for a decade they didnât work (with the exception of List) until one day I just happened to have a generic field and I saw it in the editor and was like âWOAH! That works now!!??!?â
And who knows, maybe one day this will work. But Iâm not holding my breathe, and well, at least now you know why it doesnât work currently.