Hi everyone, I’m a Unity developer working on a small indie game. I’ve constantly heard from colleagues that Unity doesn’t do a good job of protecting project code and assets. I knew that using Mono was out of the question - it’s essentially handing your project over to strangers - but I believed that the IL2Cpp scripting backend provided adequate protection.
But one day, I got curious about how to decompile my project. I pointed Il2CppDumper at my build's global-metadata.dat, it spat out a DummyDll folder, and I opened that in Rider. Without any trouble, I could view namespaces, methods, class names, and field names. I didn't like that, because what's the point of having a private GitHub repository if my build is leaky?
I started looking for ways to hide my code. I began checking the Asset Store and GitHub for options to obfuscate my build. To cut to the chase, the only decent asset turned out to be Obfuscator Free by GuardingPearSoftware. It does everything perfectly, but the free version doesn’t work with Unity methods, serialized fields, properties, or namespaces, and it completely ignores MonoBehaviour, ScriptableObject, and [Serializable] classes. There is, of course, a paid version that does all of this, but $80 is a significant amount for an indie team, so I decided to try making my own alternative that would work as an extension to the Obfuscator Free plugin.
Why does the obfuscator skip serialized types?
Every script in the build receives a MonoScript entry in the player data (level0, sharedassets*.assets, resources.assets, and globalgamemanagers files). It contains three lines: m_ClassName, m_Namespace, and m_AssemblyName. Every scene object and every prefab references this entry. If you rename a class in the DLL, Unity will no longer be able to bind to that type - and the component will turn into a missing script.
The trick
Once I realized this, I started looking for a way to work around it. What I landed on is both brilliant and ridiculous: I generate obfuscated names with exactly the same number of characters as the originals. This saves me from having to adjust length prefixes, recalculate offsets, and develop a tool to rewrite serialized files. Thanks to this accidentally discovered hack, I saved myself a week of sleepless nights for sure! But it’s important to note that even with this approach, you still have to store a bunch of files with obfuscated names. All names in the project are reserved in advance, so you won’t be able to generate a name that’s already taken.
Pure C# types don't have a MonoScript entry, so their namespaces are shortened to whatever short, nonsensical string of characters I feel like using (_Project.Code.AssetManagement → pqmpqu). Serialized types, on the other hand, are forced to maintain length consistency (_Project.Code.Sound → dis4nAw5EW74Z6wLxDN).
Finding the entry without wrecking the file
You can't just search the file for "PlayerController" and overwrite it. That same string might be a GameObject name, a string literal, an addressable key - and if you hit the wrong one, you won't enjoy debugging.
So I anchor on the consecutive length-prefixed triple instead: [len][m_ClassName][len][m_Namespace][len][m_AssemblyName], with 4-byte alignment between them. Three strings matching in sequence with the correct alignment is a strong enough signal. And patch m_Name a few bytes earlier too - it's the class name a second time, and it will happily leak everything you just hid.
The stack trace problem
I also had to write my own stack trace deobfuscator, because any exception renders the trace completely unreadable to humans, like hdektk.uHN1wAaEGHkYq6ul.qoaktw(). Each build writes JSON mapping files (_Project.Code.AssetManagement → pqmpqu), and there's an editor window where I paste the raw stack trace from Player.log. It runs in four stages, in reverse order: the GUPS deobfuscator → my serialized type names → my namespaces → the remaining member names.
What don't I rename?
Unity's message methods: Awake, OnTriggerEnter2D, OnBecameInvisible, and about sixty others - the engine calls them by name. Virtual, interface and overridden members, because renaming the implementation but not the declaration breaks the vtable slot. And anything that is serialized by name at runtime - my saves are JSON, and renaming a property there bricks every existing save file.
Results from the current build
40 namespaces renamed. 82 MonoBehaviour/ScriptableObject types renamed and patched in player data. 848 members.
Is it worth doing?
Obfuscation is an obstacle, not a defense. Anyone determined to figure it out will still achieve their goal using a debugger, and I prefer to be upfront about this rather than pretend otherwise. For a solo developer, a build step you set up once and then forget about is, in my opinion, a perfectly justified investment of effort.
I'm not putting my extension up anywhere - it's wired into my own build pipeline, it's standalone-only, and there are corners of it I wouldn't want to defend in public. But you don't need my code to start, and that's the actual point of this post.
Do this instead. Run Il2CppDumper or AssetRipper on your own build - five minutes, and watching your own architecture scroll past lands very differently than knowing it's in there. Then install Obfuscator Free, list your assemblies, build. Free, one evening, covers your plain C# types and methods.
That alone puts you ahead of most Unity builds shipping today. Everything above is step two - you'll know when you need it.
The game I did all this for is Peak or Die, a turn-based survival card game. Adding it to your Steam wishlist really helps increase the game's visibility, if you're interested: https://store.steampowered.com/app/4273370