# Obfuscating Unity Game Assemblies: Mono, IL2CPP, and the Keep-Rules Serialization Demands


Ship a Unity game built on the Mono scripting backend and you have, in effect, published your source code. The `Assembly-CSharp.dll` sitting in your player's `Managed` folder is ordinary .NET IL, and a decompiler turns it back into readable C# in seconds — your combat formulas, your anti-cheat, your in-app-purchase validation, your secret URLs, all right there. Obfuscation is the standard answer, but Unity is the one .NET environment where naively renaming everything *will* break your game. Unity leans harder on name-based serialization and reflection than almost any other framework, and the obfuscator has to be told what not to touch.

This post covers what each Unity backend actually exposes, the specific things Unity resolves by name, and the keep-rules that let you obfuscate aggressively without a single broken prefab.

## Two backends, two threat models

Unity has two scripting backends, and they change what obfuscation even means.

**Mono** compiles your scripts to managed assemblies that ship as real `.dll` files (`Assembly-CSharp.dll`, `Assembly-CSharp-firstpass.dll`, and any precompiled assemblies/asmdefs). These are standard CLI images: decompilable, and therefore fully obfuscatable. Renaming, control-flow flattening, and string encryption all apply to the shipped binary exactly as they would to any .NET app.

**IL2CPP** takes that same managed IL and transpiles it to C++, which is then compiled to a native library for the target platform. There is no managed assembly to rename at runtime — but IL2CPP emits a `global-metadata.dat` file alongside the binary that preserves type names, method names, field names, and (critically) every string literal in clear text. Tools like Il2CppDumper read that file to reconstruct a near-complete picture of your code. So IL2CPP is *not* obfuscation; it is transpilation with a metadata sidecar. Protection there means transforming the managed assemblies **before** IL2CPP runs, so the names and strings that get baked into `global-metadata.dat` are already encrypted or renamed.

<svg viewBox="0 0 760 250" role="img" aria-labelledby="pipe-title" xmlns="http://www.w3.org/2000/svg" style="max-width:100%;height:auto;font-family:system-ui,sans-serif">
  <title id="pipe-title">Where obfuscation fits in each Unity backend</title>
  <style>
    .box{fill:none;stroke:#64748b;stroke-width:1.5;rx:8}
    .lbl{fill:currentColor;font-size:13px}
    .sm{fill:#64748b;font-size:11px}
    .ob{stroke:#6366f1;stroke-width:2}
    .arr{stroke:#64748b;stroke-width:1.5;marker-end:url(#a)}
  </style>
  <defs><marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0 0L10 5L0 10z" fill="#64748b"/></marker></defs>
  <text class="lbl" x="20" y="24">C# scripts → Roslyn → <tspan font-weight="700">Assembly-CSharp.dll</tspan> (managed IL)</text>
  <line class="arr" x1="150" y1="36" x2="150" y2="64"/>
  <rect class="box ob" x="60" y="66" width="180" height="40"/>
  <text class="lbl" x="78" y="91">Nebula obfuscate (IL)</text>
  <!-- Mono branch -->
  <line class="arr" x1="150" y1="106" x2="150" y2="150"/>
  <text class="sm" x="60" y="128">Mono backend</text>
  <rect class="box" x="40" y="150" width="220" height="40"/>
  <text class="lbl" x="58" y="175">Ship obfuscated .dll in player</text>
  <!-- IL2CPP branch -->
  <line class="arr" x1="240" y1="86" x2="470" y2="86"/>
  <text class="sm" x="300" y="78">IL2CPP backend</text>
  <rect class="box" x="470" y="66" width="250" height="40"/>
  <text class="lbl" x="488" y="91">il2cpp.exe → C++ → native</text>
  <line class="arr" x1="595" y1="106" x2="595" y2="150"/>
  <rect class="box" x="470" y="150" width="250" height="40"/>
  <text class="lbl" x="488" y="168">native lib + </text>
  <text class="lbl" x="488" y="184">global-metadata.dat (names/strings)</text>
  <text class="sm" x="300" y="228">Obfuscating the IL first means the metadata IL2CPP bakes in is already renamed &amp; string-encrypted.</text>
</svg>

## What Unity resolves by name — and will break if you rename it

The editor and runtime both do a surprising amount of string- and name-based binding. Each of these is a place where blind renaming corrupts the game.

- **Serialized fields.** Every `public` field and every `[SerializeField] private` field on a `MonoBehaviour` or `ScriptableObject` is saved into scenes, prefabs, and asset files keyed by its exact name. Rename the field and the saved value no longer loads.
- **Script type references.** A scene stores "this GameObject has a component of type `PlayerController`" by type (and assembly) identity. Renaming the class detaches every instance.
- **UnityEvents.** Inspector-wired `UnityEvent` listeners (your button `OnClick`, etc.) persist the *target method name* as a string. Rename the method and the button does nothing.
- **String-dispatched calls.** `SendMessage`, `BroadcastMessage`, `Invoke`, `InvokeRepeating`, and `AnimationEvent` all call methods by string name.
- **Lifecycle & attribute hooks.** Unity finds `Awake`, `Start`, `Update`, `OnEnable`, etc. by name via reflection; it also scans for `[RuntimeInitializeOnLoadMethod]`, `[MenuItem]`, and similar. Magic methods are usually on a built-in keep list, but custom reflection is not.
- **Addressables / asset bundles.** Anything you load by type name or by string address at runtime.

## A concrete example

Here is a perfectly normal component that renaming would wreck in three different ways:

```csharp
using UnityEngine;
using UnityEngine.Events;

public class TurretController : MonoBehaviour   // (1) type name is in every scene/prefab using it
{
    [SerializeField] private float fireRate = 0.5f;   // (2) field name is saved in the asset
    [SerializeField] private UnityEvent onFire;        // (3) persistent listeners store a method name

    // Called by name from an AnimationEvent on the fire clip:
    public void SpawnProjectile() { /* ... */ }         // (4) resolved by string, not a direct call
}
```

If Nebula renames `TurretController` → `a`, `fireRate` → `b`, and `SpawnProjectile` → `c`, then every turret prefab loses its fire-rate value, every scene loses its turret components, and the fire animation stops spawning projectiles — with no compiler error, because none of these bindings are checked at compile time.

## The keep-rules that fix it

The goal is *not* "exclude MonoBehaviours from obfuscation." That would leave most of your code in the clear. The goal is to keep **only the names Unity binds to**, and obfuscate everything behind them — method bodies, private helpers, algorithms, and string literals.

Nebula expresses this through its keep-list / rename-exclusion rules (the same mechanism covered in [keep-lists and rename exclusions](https://delta1labs.com/blog/nebula-keep-list-rename-exclusions/)). A practical Unity rule set keeps:

- the **names of all types** deriving from `UnityEngine.MonoBehaviour` and `UnityEngine.ScriptableObject`;
- all **serialized members** on those types (public fields and `[SerializeField]` fields, and `[field: SerializeField]` auto-properties);
- any **method targeted by a UnityEvent or called via `SendMessage`/`Invoke`/`AnimationEvent`**.

Expressed against members, that is "keep the name of any field marked `[SerializeField]` or public on a `UnityEngine.Object` subclass, and keep the type name of any `MonoBehaviour`/`ScriptableObject` subclass." Method **bodies** are never affected by name-keep rules, so control-flow flattening and [string encryption](https://delta1labs.com/blog/string-encryption-dotnet/) still apply to `SpawnProjectile` even though its *name* is preserved — the logic is protected, only the identifier survives.

Everything else — your pure C# domain logic, networking, save-game format, anti-cheat, and all the string literals in them — gets the full treatment.

## Fitting it into the build

Keep obfuscation out of the editor and off developer machines; run it on the artifact. For the **Mono** backend, add a post-build step (a `BuildPipeline` post-process or a CI stage) that obfuscates `Assembly-CSharp.dll` and your other game assemblies in the player's `Managed` directory, then repackages. For **IL2CPP**, obfuscate the managed assemblies before the IL2CPP conversion so the generated C++ and `global-metadata.dat` inherit renamed symbols and encrypted strings. Running protection in CI (see [obfuscating on the build server only](https://delta1labs.com/blog/obfuscate-on-build-server-only/)) means your team keeps debugging clean, readable code and only shipped builds are hardened.

## Doing this with Nebula

[Nebula](https://delta1labs.com/nebula) obfuscates the managed assemblies a Unity game is built from — renaming, control-flow flattening, and string encryption — with a keep-list model precise enough to preserve exactly the serialized fields, component type names, and event-target methods Unity binds by name, while still hardening every method body behind them. It runs as a post-build or CI step, so your editor stays fast and only the shipped player is protected. Nebula **Pro** adds [code virtualization](https://delta1labs.com/blog/nebula-1-1-code-virtualization/) for the handful of routines that matter most — license checks, anti-cheat, purchase validation.

Start with the [free edition](https://delta1labs.com/blog/free-dotnet-obfuscator/) to harden your first build, or see the [plans](https://delta1labs.com/pricing?product=nebula) when you are ready for virtualization.

