Unity Behaviours in DLLs
TL;DR: Unity uses a special way of referencing Behaviours that are compiled inside of an DLL. Unity saves a special fileID inside .prefab and .asset files that is constructed from the namespace and name of a Behaviour. Read the complete article to find out how the fileID is calculated and how to solve occurring issues.
Intro
I personally really like how Unity3d handles references between assets! The “magic” GUID/UUID is showing it’s great potential and linking everything based on a controllable unique string which works on every machine or setup. I sigh pretty often when I work with other engines and see they are using a path based approach. Moving one file can really mess up your project. It is hard to track down the references to a file path. Also you have the platform problems with different path representations and multiple people working on files that have nothing to do with your work but reference a file path that you are about to change.
GUID to rule them all
Just to recap real quick: Unity assigns a .meta file to every asset in your Unity project. An asset can be a texture, script, audio, directory, etc. In this post we are mainly focusing on scripts. When a scene or prefab in Unity wants to use your script it has to hold a reference to it. So it just saves the GUID of the script file and can always find it. Of course there are some limitations. Every script file should only contain one referenceable class (e.g. MonoBehaviour, ScriptableObject, etc.), so Unity does not get confused when one GUID matches two classes.
Script references in DLLs
So what happens when we import a DLL that contains multiple referenceable classes into the Unity project? Unity supports importing .dll files and references the dll into your generated csharp projects. I would love to say that Unity unpacks all referenceable classes and gives them GUIDs. Or they would support an Attribute so you can give a unique id to your classes inside the DLL. But no, this is not what is happening. Unity does scan the DLL file but creates another ID for every referenceable class inside. This specific ID is created from the GUID and the Full-Name of the class inside the DLL. I hope you are already screaming “NOOOOOO!”. Based on namespace and class name? But we have this great GUID system in place. To put it short: If you rename or change the namespace of a referenceable class inside a DLL and it is used by something in your project, it breaks all scenes and prefabs that use the class! Just like file paths but also a bit worse because nowadays the automatic refactoring can change a namespace so quickly. You wouldn’t expect a namespace refactoring to break stuff inside your Unity project.
The magic ID Unity creates for DLL classes
The ID Unity creates is called fileID
and is used also in other places for various things. Here is a snippet from a scene file that saves a GameObject with two MonoBehaviours attached.
MonoBehaviour inside DLL:
--- !u!114 &874929368
MonoBehaviour:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 874929362}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: -879644023, guid: 2535a1a73749ca8469c6de4b834358f8, type: 3}
m_Name:
m_EditorClassIdentifier:
MyInt: 2
MonoBehaviour inside Asset directory:
--- !u!114 &874929369
MonoBehaviour:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 874929362}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e789f710c66e90d40863adc43cb573e6, type: 3}
m_Name:
m_EditorClassIdentifier:
You can see that the m_Script
reference is different for the two snippets. And here we see our fileID
and GUID of the DLL. For Unity Assets
directory based scripts, the fileId is always 11500000. For our MonoBehaviour living inside the DLL the fileID
is calculated as described earlier. In this example I used a class with the namespace ClassLibrary
and type name TestMonoBehaviour
which results in the fileID
-879644023. Figuring out the IDs is actually quite complicated. Basically the ID gets put together by the first four bytes of the MD4 of the string `”s\0\0\0” + type.Namespace + type.Name’ as a little endian 32-byte integer. THIS forum post helped me a lot to figure everything out. Kudos to Lambda Knight!
How handle fileID problems
The most problems you will have is when you rename the name or namespace of a class that is referenceable by Untiy and lies within a DLL. What you have to do in this case: Calculate the fileID
of the class you are refactoring before the rename. Then calculate the fileID
of the refactored namespace and type name. Now you have to go through all .prefab
and .asset
(everything that can reference a script) files and replace the old fileID
with the new fileID
. If you now import the new DLL all the references should not be broken.
Conclusion
Moving your code from the Assets
directory into DLLs is good for encapsulation and improved compile times on script change, but it also always brings disadvantages. I wish that Unity provides an Attribute that lets you specify a GUID for a script class. This way Unity could always be sure that the script references are maintained. You just set the Attribute on referenceable classes inside the DLL and we are good. In Unity 2017.3 you can actually set a directory to be handled as separate DLL when generating the csharp project. This is great! And then you still have a script inside of a DLL but Unity can still grab the GUID for each script and reference that. I would recommend doing that from 2017.3 on. But of course in some cases you still need a separate DLL. I inserted a script below that contains the conversion code from type namespace and type name to fileID
. Enjoy!
using System;
using System.Linq;
using System.Collections.Generic;
using System.Security.Cryptography;
// Taken from http://www.superstarcoders.com/blogs/posts/md4-hash-algorithm-in-c-sharp.aspx
// Probably not the best implementation of MD4, but it works.
public class MD4 : HashAlgorithm
{
private uint _a;
private uint _b;
private uint _c;
private uint _d;
private uint[] _x;
private int _bytesProcessed;
public MD4()
{
_x = new uint[16];
Initialize();
}
public override void Initialize()
{
_a = 0x67452301;
_b = 0xefcdab89;
_c = 0x98badcfe;
_d = 0x10325476;
_bytesProcessed = 0;
}
protected override void HashCore(byte[] array, int offset, int length)
{
ProcessMessage(Bytes(array, offset, length));
}
protected override byte[] HashFinal()
{
try
{
ProcessMessage(Padding());
return new [] {_a, _b, _c, _d}.SelectMany(word => Bytes(word)).ToArray();
}
finally
{
Initialize();
}
}
private void ProcessMessage(IEnumerable<byte> bytes)
{
foreach (byte b in bytes)
{
int c = _bytesProcessed & 63;
int i = c >> 2;
int s = (c & 3) << 3;
_x[i] = (_x[i] & ~((uint)255 << s)) | ((uint)b << s);
if (c == 63)
{
Process16WordBlock();
}
_bytesProcessed++;
}
}
private static IEnumerable<byte> Bytes(byte[] bytes, int offset, int length)
{
for (int i = offset; i < length; i++)
{
yield return bytes[i];
}
}
private IEnumerable<byte> Bytes(uint word)
{
yield return (byte)(word & 255);
yield return (byte)((word >> 8) & 255);
yield return (byte)((word >> 16) & 255);
yield return (byte)((word >> 24) & 255);
}
private IEnumerable<byte> Repeat(byte value, int count)
{
for (int i = 0; i < count; i++)
{
yield return value;
}
}
private IEnumerable<byte> Padding()
{
return Repeat(128, 1)
.Concat(Repeat(0, ((_bytesProcessed + 8) & 0x7fffffc0) + 55 - _bytesProcessed))
.Concat(Bytes((uint)_bytesProcessed << 3))
.Concat(Repeat(0, 4));
}
private void Process16WordBlock()
{
uint aa = _a;
uint bb = _b;
uint cc = _c;
uint dd = _d;
foreach (int k in new [] { 0, 4, 8, 12 })
{
aa = Round1Operation(aa, bb, cc, dd, _x[k], 3);
dd = Round1Operation(dd, aa, bb, cc, _x[k + 1], 7);
cc = Round1Operation(cc, dd, aa, bb, _x[k + 2], 11);
bb = Round1Operation(bb, cc, dd, aa, _x[k + 3], 19);
}
foreach (int k in new [] { 0, 1, 2, 3 })
{
aa = Round2Operation(aa, bb, cc, dd, _x[k], 3);
dd = Round2Operation(dd, aa, bb, cc, _x[k + 4], 5);
cc = Round2Operation(cc, dd, aa, bb, _x[k + 8], 9);
bb = Round2Operation(bb, cc, dd, aa, _x[k + 12], 13);
}
foreach (int k in new [] { 0, 2, 1, 3 })
{
aa = Round3Operation(aa, bb, cc, dd, _x[k], 3);
dd = Round3Operation(dd, aa, bb, cc, _x[k + 8], 9);
cc = Round3Operation(cc, dd, aa, bb, _x[k + 4], 11);
bb = Round3Operation(bb, cc, dd, aa, _x[k + 12], 15);
}
unchecked
{
_a += aa;
_b += bb;
_c += cc;
_d += dd;
}
}
private static uint ROL(uint value, int numberOfBits)
{
return (value << numberOfBits) | (value >> (32 - numberOfBits));
}
private static uint Round1Operation(uint a, uint b, uint c, uint d, uint xk, int s)
{
unchecked
{
return ROL(a + ((b & c) | (~b & d)) + xk, s);
}
}
private static uint Round2Operation(uint a, uint b, uint c, uint d, uint xk, int s)
{
unchecked
{
return ROL(a + ((b & c) | (b & d) | (c & d)) + xk + 0x5a827999, s);
}
}
private static uint Round3Operation(uint a, uint b, uint c, uint d, uint xk, int s)
{
unchecked
{
return ROL(a + (b ^ c ^ d) + xk + 0x6ed9eba1, s);
}
}
}
public static class FileIDUtil
{
public static int Compute(Type t)
{
string toBeHashed = "s\0\0\0" + t.Namespace + t.Name;
using (HashAlgorithm hash = new MD4())
{
byte[] hashed = hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(toBeHashed));
int result = 0;
for(int i = 3; i >= 0; --i)
{
result <<= 8;
result |= hashed[i];
}
return result;
}
}
}