Help us improve Softanics
We use analytics cookies to understand which pages and downloads are useful. No ads. Privacy Policy
Artem Razin
Low-level software protection engineer with 20+ years in native and managed code security. Creator of ArmDot, protecting commercial .NET applications since 2014.

Unity C# Obfuscation: Protecting Your Game Code

If you have been told that switching to the IL2CPP scripting backend protects your Unity game code, this page is for you. That belief is widespread in Unity developer communities, repeated in forum answers, and wrong in ways that matter practically. Understanding exactly why it is wrong is the most important thing this page delivers.

What IL2CPP actually does - and what it does not

IL2CPP converts your C# code from Intermediate Language bytecode to C++ source, which is then compiled to native machine code for the target platform. The resulting binary is native - ILSpy cannot open it. In that sense the claim is technically accurate.

What IL2CPP does not do is remove the metadata that makes your game's structure readable.

Unity's IL2CPP build process generates a file called global-metadata.dat which ships alongside the native binary. This file contains the names of every class, method, field, and other symbol from your original C# code. It is a complete map of your game's architecture, written in plaintext, required by the IL2CPP runtime to support reflection and other .NET features.

Tools like Il2CppDumper read global-metadata.dat and reconstruct a full symbol table from it. This symbol table is then used to annotate a disassembly of the native binary in Ghidra or IDA Pro. The result is not as clean as a decompiled IL assembly, but it is more than adequate for a motivated analyst. Your class names are there. Your method names are there. The structure of your game's code is exposed.

IL2CPP raises the reverse engineering cost compared to plain Mono. It does not protect your code.

The Mono case: full exposure, no special tooling required

For developers using the Mono scripting backend - still common for PC/Mac standalone builds, older projects, and situations where IL2CPP build times are prohibitive - the situation is more direct.

The compiled Assembly-CSharp.dll inside the Unity player's Managed folder is a standard .NET assembly. Open it in ILSpy. Every class name, every method name, every string literal is there, reconstructed as readable C# without any special tooling or expertise.

Mono is not just a Unity concept. It is a cross-platform .NET runtime used in server-side Linux deployments, embedded systems, and historically in Xamarin. Unity popularized it in game development and gave it a place in the broader runtime ecosystem, but the vulnerability is identical everywhere Mono is used: the compiled assembly is a standard .NET assembly, fully readable.

ArmDot is built on Mono.Cecil - the library used to read and rewrite .NET assemblies at the binary level. Mono support is not an add-on feature; it is fundamental to how the tool works. Supporting Mono well is a matter of the tool's basic architecture.

Who is concerned about this and why

Indie developers and small studios shipping commercial games on Steam or mobile platforms have a piracy concern. A cracked version of a game that bypasses the purchase requirement, or a modified APK with in-app purchases removed, starts with understanding the game's code. Obfuscation makes that analysis significantly harder.

Studios with proprietary systems have an IP concern. A novel AI behaviour tree, a procedural generation algorithm, a proprietary matchmaking system - these are competitive advantages that represent real investment. The game binary is the distribution medium, and without protection it is also a readable description of how those systems work.

Developers fighting active cheating have a different problem. Cheaters use memory editors, modified clients, and speed hacks by understanding the game's internal structure - field names, method logic, value ranges. Obfuscation makes that analysis harder and raises the cost of maintaining working cheat tools as the game updates.

Unity-specific preservation: what ArmDot handles automatically

Unity's engine calls MonoBehaviour methods by name through reflection. Start, Update, Awake, OnDestroy, OnCollisionEnter, FixedUpdate - every Unity lifecycle method is invoked this way. An obfuscator that renames these methods produces a build where the fundamental game loop stops working.

ArmDot recognises Unity engine callback methods and excludes them from renaming automatically. You do not need to maintain an exclusion list.

SendMessage and BroadcastMessage call methods by string name at runtime. Any method called this way must preserve its name. ArmDot handles this automatically as well - method names passed to SendMessage patterns are preserved.

[SerializeField] fields are used by Unity's serialization system to store values in scenes and prefabs and to support JsonUtility serialization at runtime. ArmDot excludes [SerializeField] fields from renaming by default. This means scene and prefab data deserializes correctly in the obfuscated build, and JsonUtility.ToJson() / JsonUtility.FromJson() round-trips work correctly within the same build.

These three automatic behaviors are what distinguish ArmDot from a generic .NET obfuscator applied naively to a Unity project. A developer who applies a generic obfuscator without Unity-specific exclusions will typically encounter a broken game loop immediately. ArmDot avoids this by design.

One additional risk worth understanding before running any obfuscator on a Unity project: scripts referenced in Prefabs and Scenes are identified by their class name in the serialized YAML. Renaming a MonoBehaviour subclass breaks every Prefab and Scene that references it - the component simply disappears from the scene, silently, with no error at build time. This is one of the most destructive common mistakes when applying obfuscation to a Unity project. ArmDot preserves MonoBehaviour subclass names by default for exactly this reason, but if you are evaluating any obfuscator for Unity, verify this behavior before applying it to a project with scenes.

The build pipeline

Unity does not use standard MSBuild project files, so the NuGet/attribute-based workflow used for other .NET platforms does not apply directly. The integration works differently.

The workflow:

  1. Build your Unity player as normal (File > Build Settings > Build)
  2. Locate Assembly-CSharp.dll in the build output. For a Windows standalone build it is typically in YourBuild_Data/Managed/Assembly-CSharp.dll
  3. Run ArmDotConsole on it:
ArmDotConsole \
  --input-assembly Assembly-CSharp.dll \
  --enable-obfuscate-names \
  --enable-hide-strings \
  --enable-control-flow-obfuscation \
  --create-map-file Assembly-CSharp.map
  1. ArmDotConsole replaces the assembly in-place (or writes to --output-assembly if specified)
  2. Distribute the build containing the obfuscated assembly

For more granular control - specifying which methods to virtualize, configuring exclusions beyond the automatic Unity defaults - use the ArmDot UI to create a saved .armdotproj project file, then run it via command line:

ArmDotConsole --build --project-path MyGame.armdotproj

This approach works in CI/CD pipelines. Install ArmDotConsole as a .NET global tool:

dotnet tool install --global ArmDotConsole

Then add the obfuscation step after the Unity build step in your pipeline configuration. The map file (--create-map-file) is important to keep - it is needed to deobfuscate stack traces from crash reports. For the full ArmDotConsole parameter reference, see the ArmDotConsole documentation →.

A note on IL2CPP: ArmDot supports the Mono scripting backend. IL2CPP produces native binaries rather than managed assemblies, which is a different compilation path that has not been tested with ArmDot. If you are using IL2CPP, obfuscation addresses the global-metadata.dat exposure described above through the Mono-compiled intermediate assemblies before IL2CPP processes them - but this workflow has not been verified and should be tested carefully before relying on it in production.

What to protect

Virtualize the logic that represents your game's competitive advantage or anti-piracy critical path: license checks, anti-tamper routines, proprietary AI or procedural systems. Apply [VirtualizeCode] in code where you can, or configure virtualization in the ArmDot project file for specific methods.

String encryption covers API keys, backend endpoint URLs, analytics tokens, and any internal strings that would help an analyst understand your game's backend architecture or bypass authentication.

Symbol renaming removes all non-Unity-critical identifiers permanently. With ArmDot's automatic Unity exclusions, assembly-wide renaming is safe to apply without manually auditing every MonoBehaviour.

Control flow obfuscation on business logic methods raises the cost of understanding the non-virtualized portions of your game code.

Where obfuscation fits relative to dedicated anti-cheat services

Developers researching Unity code protection will also encounter dedicated anti-cheat services: Easy Anti-Cheat, BattleEye, GameGuard. It is worth being clear about where obfuscation fits relative to these tools, because they are not competitors - they operate at different layers.

Dedicated anti-cheat services work at the process and kernel level. They detect known cheat tools, memory editors, and process injection at runtime. They are effective against known cheats and provide centralised infrastructure for detection and banning. Their weaknesses: they do not protect game logic from static analysis, they require always-online validation which creates friction for single-player games, and they carry significant privacy concerns that generate player backlash.

Obfuscation works at the code level. It makes the game logic harder to understand and modify through static analysis. It does not need a server connection and does not require kernel-level access. Its weakness: a patient analyst with runtime tools can still study game behaviour through dynamic analysis even if the code is obfuscated.

The honest framing: these are complementary layers. A multiplayer game with a real economy should consider both. A single-player indie game probably benefits more from obfuscation - which protects the code itself - than from a heavyweight anti-cheat service that requires always-online infrastructure.

Related: Code Virtualization for .NET → - IL2CPP's native compilation and code virtualization serve a similar purpose from different directions. Virtualization converts IL to custom bytecode; IL2CPP converts IL to native code. Neither is sufficient alone - together they represent the strongest available protection for Unity game logic.

The IL2CPP question and the Native AOT question are structurally identical - native compilation raises the reverse engineering cost but does not eliminate it. A dedicated comparison is coming in a later section of this cocoon.

Back to: .NET Obfuscation Platforms →

Protect your Unity game with ArmDot

ArmDot automatically preserves Unity engine callbacks, SendMessage targets, and [SerializeField] fields, making assembly-wide obfuscation safe to apply without manual exclusion lists. Available via ArmDotConsole for CI/CD integration. Free trial available - protected assemblies stop working after two weeks.