UNITY_64 and DEVELOPMENT_BUILD deprecation and Managed Code Variants

Hi everyone,

In Unity 6.6, we’re deprecating the UNITY_64 and DEVELOPMENT_BUILD scripting symbols and introducing a new Player Setting, Managed Code Variant, to take the latter’s job. If you’ve ever guarded code with #if UNITY_64 or #if DEVELOPMENT_BUILD, you’ll need to update it eventually. In this post I’ll explain why these symbols were never as reliable as they looked, what’s replacing them, and how to migrate your code, including a behavior change that affects you even if you’ve never typed either symbol yourself.

Why we’re doing this

Here’s the uncomfortable truth: UNITY_64 and DEVELOPMENT_BUILD have never been reliable C# defines.

Whether a build is 32-bit or 64-bit, and whether it is a development or non-development build, is decided solely by the native engine binary (for example, UnityPlayer.dll on Windows or libunity.so on Android). Unity compiles your C# once when you build your project, but the resulting managed assemblies can then be reused across several different native binaries. A few examples:

  • On Android, the 32-bit and 64-bit builds are built from the same managed code, compiled once. UNITY_64 simply loses its meaning.
  • With the Generate Visual Studio Solution build setting on Windows, you choose development/non-development configurations and x86/x64/Arm64 architectures later, inside Visual Studio. At the point you build your project from Unity, it hasn’t even been decided yet what kind of build(s) you’ll end up with.
  • Modders routinely drop the development UnityPlayer.dll inside shipped games to attach a profiler/debugger, or to get extra diagnostic information. Same managed code, suddenly a “development” build.

On top of that, UNITY_64 was simply never defined on iOS, Android, UWP, QNX, Embedded Linux (and a few others), so any code relying on it was already broken on more than half of the platforms we support. And DEVELOPMENT_BUILD was overloaded: some code used it for extra safety checks, some for profiler instrumentation, and some to (incorrectly) detect a development native binary. One symbol was being asked to mean three different things.

What’s changing

1. A new “Managed Code Variant” Player Setting (landed in 6000.6.0a5)

You can find it in Player Settings -> Other Settings -> Managed Code Variant, right below Managed Stripping Level. It decides which diagnostic symbols get baked into your C#, completely independently of the native binary. There are four levels, and each one is inclusive of the next:

Variant Defines Optimized?
Debug DEBUG, UNITY_ASSERTIONS, UNITY_ENABLE_CHECKS, UNITY_INCLUDE_INSTRUMENTATION, ENABLE_PROFILER No
Checked UNITY_ASSERTIONS, UNITY_ENABLE_CHECKS, UNITY_INCLUDE_INSTRUMENTATION, ENABLE_PROFILER Yes
Instrumented UNITY_INCLUDE_INSTRUMENTATION, ENABLE_PROFILER Yes
Release (none of the above) Yes

The way to think about it, this splits the old DEVELOPMENT_BUILD into two clearer ideas:

  • UNITY_ENABLE_CHECKS -> heavyweight safety checks, assertions and validation (the “Checked” bucket).
  • UNITY_INCLUDE_INSTRUMENTATION -> lightweight profiling, instrumentation and diagnostic logging (the “Instrumented” bucket).

You can also read and set the variant from build scripts via PlayerSettings.SetManagedCodeVariant(...).

2. UNITY_64 and DEVELOPMENT_BUILD are now deprecated (warnings landed in 6000.6.0a6)

Using either one in an #if or a [Conditional(...)] attribute now produces a Roslyn analyzer warning (UAC0008 for UNITY_64 and UAC0009 for DEVELOPMENT_BUILD) in the Console and in your IDE. In 6.6 the symbols still work, you’ve just got warnings nudging you to migrate. If you compile with warnings-as-errors, though, you’ll need to update the code now.

3. Coming in Unity 6.8: full removal. The symbols will stop being emitted and using them will become a hard compile error (we’re not dead set on this last step: depending on your feedback, we might reconsider and keep them as warnings even after removing them). Either way, 6.8 is the real deadline, so it’s worth fixing your code during the 6.6/6.7 window while the old and new symbols still coexist.

Documentation:

This affects you even if you never used these symbols

A lot of Unity’s own packages used DEVELOPMENT_BUILD internally, and we’ve moved all of them onto the new variant symbols. So their availability is now controlled by the Managed Code Variant, not by the Development Build checkbox, and Managed Code Variant defaults to Release, which turns all of them off. Affected engine features include:

  • Scriptable Render Pipeline (URP/HDRP/Core): debug overlays, the Rendering Debugger runtime resources, Render Graph Viewer, render-graph validation, the magenta “incompatible objects” pass, Frame Debugger support, and the Volume panel are now gated by UNITY_ENABLE_CHECKS (Debug/Checked). Render Graph profiling samplers, URP’s per-pass ScriptableRenderPass.profilingSampler, and HDRP’s dynamic-resolution overlay are gated by UNITY_INCLUDE_INSTRUMENTATION (Debug/Checked/Instrumented). Build-time stripping of debug shaders and rendering-debugger resources now follows the variant too, so a non-development Checked build keeps them and a Release build strips them.
  • Unity Physics: simulation integrity checks are now compiled in for the Debug/Checked variants (still overridable with UNITY_PHYSICS_DISABLE_INTEGRITY_CHECKS).
  • Entities: Entities Journaling (the data behind the Entities Journaling window) is now compiled in for Debug/Checked/Instrumented variants (still overridable with DISABLE_ENTITIES_JOURNALING).
  • Adaptive Performance: Apple and Android provider logging is now available in Debug/Checked/Instrumented variants.

Packages that ship independently of the Unity editor change the same way on Unity 6.6 and newer:

  • AR Foundation: provider-returned trackable-change validation and misconfiguration warnings (for example, the warning shown when no reference image library is assigned) are now compiled in Debug/Checked variants.
  • PolySpatial: assertions and the runtime scene validator are compiled in Debug/Checked variants, while statistics, debug object names, and verbose logging are compiled in Debug/Checked/Instrumented variants.

So if you make a Development Build and leave the variant at the default Release, you’ll be missing diagnostics you used to get for free. To get the old behavior back, set Managed Code Variant -> Checked in Player Settings; Checked turns on both the check and the instrumentation paths. Pick Debug instead if you also want DEBUG and unoptimized code for stepping through a debugger, or Instrumented if you want only informational logging or are trying to profile the builds.

There’s a small upside to the same change, too: non-development Release builds no longer drag in some URP debug-display shader variants they used to, so you may notice slightly smaller builds and lower shader-variant counts.

How to fix your code

Let’s start with UNITY_64. Since the bitness isn’t known when your scripts are compiled, the fix is to stop asking at compile time and check it at runtime instead, with IntPtr.Size:

// Before
#if UNITY_64
    UseWidePointers();
#endif

// After
if (IntPtr.Size == 8)
    UseWidePointers();

DEVELOPMENT_BUILD takes a little more thought, because it was doing more than one job. The first step is to work out which of its meanings your code actually relied on. If you were gating heavyweight safety checks, validation or assertions, that’s the “Checked” bucket, so use UNITY_ENABLE_CHECKS. If you were gating profiler instrumentation or diagnostic logging, that’s the “Instrumented” bucket, so use UNITY_INCLUDE_INSTRUMENTATION (or put it on a [Conditional] method):

// Safety checks / assertions / validation:
#if UNITY_ENABLE_CHECKS
    SafetyChecks.Validate(data);
#endif

// Profiler instrumentation / debug logging:
#if UNITY_INCLUDE_INSTRUMENTATION
    Profiler.BeginSample("MyThing");
#endif

// ...or on a method:
[Conditional("UNITY_INCLUDE_INSTRUMENTATION")]
static void LogDiagnostic(string msg) { ... }

There’s a third case worth calling out. Sometimes you genuinely want to know, at runtime, whether you’re running in a development build and here the variant symbols are not a substitute. Remember that a non-development builds can still be built with the Checked variant, so a #if UNITY_ENABLE_CHECKS block could end up compiling something into a shipping build that you never wanted there (logging that leaks auth tokens is the classic example). For that, reach for the runtime property Debug.isDebugBuild:

// Before
#if DEVELOPMENT_BUILD
    Debug.LogError($"Rejecting connection: {payload}");
#endif

// After
if (Debug.isDebugBuild)
    Debug.LogError($"Rejecting connection: {payload}");

Finally, if you were using BuildOptions.ForceEnableAssertions, that’s deprecated too: replace it with PlayerSettings.SetManagedCodeVariant(target, ManagedCodeVariant.Checked).

Package / Asset Store authors

If your code has to compile against Unity versions older than 6.6, the new symbols won’t always exist, so you can’t rely on them unconditionally. The pattern we used across our own packages is to gate on UNITY_6000_6_OR_NEWER and fall back to a runtime Debug.isDebugBuild check on older versions, keeping the diagnostic body written exactly once:

#if !UNITY_6000_6_OR_NEWER || UNITY_ENABLE_CHECKS
#if !UNITY_6000_6_OR_NEWER
    if (Debug.isDebugBuild)
#endif
    {
        if (library == null)
            Debug.LogWarning("No library assigned.");
    }
#endif

And for [Conditional] methods, pick the attribute per version:

#if UNITY_6000_6_OR_NEWER
[Conditional("UNITY_INCLUDE_INSTRUMENTATION")]
#else
[Conditional("DEVELOPMENT_BUILD")]
#endif
static void LogDiagnostic(...) { ... }

TL;DR

  • UNITY_64 and DEVELOPMENT_BUILD are deprecated now (6.6) and will be removed in 6.8.
  • Replace UNITY_64 with a runtime IntPtr.Size check.
  • Replace DEVELOPMENT_BUILD with UNITY_ENABLE_CHECKS (checks), UNITY_INCLUDE_INSTRUMENTATION (profiling/logging), or Debug.isDebugBuild when you really need a runtime development-build test.
  • The new Managed Code Variant setting drives all of this, and it defaults to Release so set it to Checked to get development-build-style diagnostics back.

I’m eager to hear your feedback and questions on this!

Someone should add the official tag to this.

I really don’t understand how they can add 4 tags but often forget about the most important one :person_facepalming:, found this discussion by accident.

There is one thing I don’t fully understand, will we still have the Development Build checkbox? is Debug.isDebugBuild linked to that?

That will be done on Monday, by the community team and also pinned. I didn’t have the permissions to add that one.

Yes and yes. That isn’t changing, and it will still control which native binary you get (that’s what it always did). Some platforms don’t have that checkbox and instead replace it with a drop down with configs like “debug”/“release”/“master” instead but that’s not new.

Hey, thanks for the explanation of UNITY_64 and DEVELOPMENT_BUILD.
your explanation did clear up why UNITY_64 was mostlikely “useless” , and for DEVELOPMENT_BUILD it is more like an “unintended MACRO” that is keep being abused by modders or debuggers.

While I agree that it’s best to remove them, I vote for “keep symbols warning instead of error” even after full removal in Unity6.8.
The reason is the macro is just too ancient and has been existing and working for years in Unity.
so this will break and cause problems especially for big projects who’s been developed for years using old Unity and decided to upgrade to new versions,
or for Developers who are not informed about this.
( seeing how many likes this post have, I doubt that many people check and know about this information )

so we’ll be thankful if you keep them as warning.
Again full removal is okay, the macro doesn’t need to do anything once it’s removed, just making it a compile error is what I’m against.

Sometimes I use defines for conditionally doing stuff like implementing interfaces, or overriding interface members with explicit implementations. Sadly, this pattern doesn’t work for those cases.

I’d like to propose that the Roslyn Analyzer should skip warnings for cases where both old and new defines are used. For example, it could allow #if DEVELOPMENT_BUILD || UNITY_ENABLE_CHECKS. This would be a much easier pattern to use in packages, less mistake-prone, and it’d support more cases.

So I found a workaround in case it helps someone in the future:

DEBUG is mostly equivalent to UNITY_EDITOR || DEVELOPMENT_BUILD before 6000.6, and it won’t trigger analyzer warnings. One could use something like #if (!UNITY_EDITOR && DEBUG) || UNITY_ENABLE_CHECKS to replicate #if DEVELOPMENT_BUILD in a way that works across different versions.

All of my actual cases need the conditionally compiled code to run in the Editor. So I’m using #if UNITY_EDITOR || DEBUG or #if UNITY_EDITOR || DEBUG || UNITY_ENABLE_CHECKS, depending on the kind of code. These directives work across different Unity versions. UNITY_EDITOR isn’t really needed because it overlaps with DEBUG, but I feel it’s clearer and more future-proof this way.

Thanks for feedback! I am leaning towards that right now.

The problem is that DEVELOPMENT_BUILD is broken, even on older versions of Unity where we don’t emit a warning. For your own projects, you might be not using Unity in a way that exposes that, but most packages (including ones that come from the Asset Store) that aim to support multiple versions of Unity don’t really know how the projects that embed them end up using Unity, which will cause it to break, often and silently. The correct way to fix it in older versions is to switch to either runtime checks, or remove the condition altogether. I am hesitant silencing Roslyn warning in those cases as we really want to eliminate the usage of this preprocessor symbol. I do agree that the pattern I showed is not pretty at all, though. DEBUG sort of has the same issues in older versions of Unity but in Unity 6.8+ will only be defined for the debug managed code variant. The difference is that it a standard C# define and has a clear meaning outside of Unity, which we will adopt.

Even if it’s “broken” in older versions, not only it doesn’t emit warnings: Those directives are the only way to detect it’s a non-release version in order to do some extra checks or Log some extra warnings. Well, yes, one can use C# conditions in some cases, but it makes code less performant. And sometimes the extra checks require extra fields and data structures that I’d rather remove when they aren’t needed (e.g. a call count to detect infinite circular calls).

Usually, when there’s a breaking change, package developers can easily support multiple versions by using preprocessor directives, but here you’re breaking that mechanism itself. And we can’t even use pragmas to ignore those warnings.

The proposed way to handle it in multiple Unity versions doesn’t work for every case, and it’s very verbose and error-prone. So I thought since DEVELOPMENT_BUILD is the only way to do this in those older versions, and it’ll do nothing in future versions, allowing it when it’s used together with the new directives could be good enough. It lets you know that whoever is using the deprecated directives is aware of their peculiarities, and is not using them to detect a particular Player dll.

I think understand. To be honest, I’ve never taken DEVELOPMENT_BUILD to mean “using the development UnityPlayer.dll”. I’ve just understood it kinda like “we’re in a non-released temp build to test something, so do some extra checks and Log some extra warnings”. I don’t think I’ve ever seen someone use it to do things that depend on what kind of Player dll is loaded, though I don’t doubt that happens.

I think I understand the risks you are trying to remove from developers. I just think that allowing old directives when used with new ones wouldn’t be more risky in practice, because it’d indicate devs aren’t using the old directives in a risky way.

That said, the DEBUG workaround works well enough for me, so even without allowing that, I’ll be fine. I really appreciate you taking your time to explain your perspective. Thank you :slightly_smiling_face:.

Can I simply replace the DEVELOPMENT_BUILD symbol with the DEBUG symbol? Does this also work in older Unity versions?

That will break down if those checks are there for profiling and you want to use instrumented managed code variant. If you never use instrumented managed code variant, or if you don’t have any profiling related code guarded by it, it should be fine.

Unless I’m missing something, Debug.isDebugBuild doesn’t fix that, arguably it makes things worse. If somebody wanted to have code that shouldn’t be leaked in dev builds (let’s ignore for now whether that’s a good idea), they cannot do it now, as now the code will always be there.

And if a third party uses the development version of the native binary, any debug code will be executed, which, in most cases, using DEVELOPMENT_BUILD would prevent, even if it’s broken in some cases.

Something that can be quite confusing is that the DEBUG symbol means something quite different to Debug.isDebugBuild, and you can make builds with any combination of them. I understand there’s no easy fix, but there has to be a way to make things make more sense.

One of my uses for DEVELOPMENT_BUILD was to add debugging tools, marks, etc., to development builds without having the code or assets in regular builds (like indicators on correct answers, buttons to execute debug actions, console/hierarchy viewer assets, and things like that). So I guess I would have to use DEBUG now, but then they could go unnoticed into non-development builds.

I haven’t given it much thought, and I don’t know what problems it could cause or what can prevent doing it, but why not remove the development build option, and just force non-Release managed code variants to create/use development build native binaries?

The DEVELOPMENT_BUILD define unfortunately gave a false sense of security. It did not prevent such code from going to non-development build in many cases.

Can you explain this use case a little bit?

To manage additive functionality that isn’t related to diagnostics/profiling, the recommended workflow is to have a separate build profile that enables these things by defining a custom feature define and using that in your code. You can change any kind of things then, including development build, any player setting, scene list, etc, and the only way you’d accidentally ship it is if you built from the wrong build profile.

That just reintroduces the problem that separating development builds and managed code variants is solving. I tried explaining it in the original post but the gist of it is that in some scenarios, developmentness is not determined the moment you press build in the editor, and instead can be flipped after the build is done. So Unity editor is unable to force anything like that.

DEVELOPMENT_BUILD should prevent the code from being compiled in the vast majority of cases, shouldn’t it? Unless I’m missing something, whenever you generate a build with the development build option enabled, that code is gone from the compiled files, no matter if Unity generates the final build or a Visual Studio project. Whether the native libraries end up being the development or non-development ones, it wouldn’t matter for this.

So, unless you did something weird (which you can also do using managed code variants, arguably more easily), I’d say it would be rather difficult to screw up.

In any case, my main issue with the specific quote I commented on is that it seems to imply that Debug.isDebugBuild is a substitute for #if when you don’t want something leaked, which is not the case (somebody can just drop the development native library in there to get the logs, or just decompile the code).

That comment was just a continuation of the previous paragraph about code leaking, not really any specific use case, other than you wanting code to not execute and not even exist on a build when building with development build enabled.

Yes, but it’s more inconvenient in some situations, plus I may want to have those features in a package and not the projects that use it, without having to force the projects to use any specific settings. DEVELOPMENT_BUILD provides a built-in standard I can rely on, and now DEBUG seems to be more or less the closest alternative.

I understand that, “force” was probably not the best word, what I meant was just pairing the development native library with non-release managed code variants when building, even if the developer or player can later change it (hopefully knowing what they are doing). For example, when creating a Visual Studio project, there are already different UnityPlayer.dll in Master and Debug/Release (it may be development/non-development versions, or something else).

If you want to give complete control from the build-creation point for users to mix-and-match native and managed versions, then of course there’s no point in doing what I said, it’s just that I don’t understand why you would want to mix-and-match in most cases, to me it seems more confusing and prone to mistakes than helpful, but of course my use cases are not representative of everyone’s.

The thing is, until reading this thread, I always saw the development build option as something that mostly affected the managed code side, adding debug symbols and whatnot, so now I don’t fully understand what it actually does, other than “creates a development native binary, maybe?” and “builds the managed code with DEVELOPMENT_BUILD”, but everything else is now a mystery to me… Is there some more in-depth explanation of what happens with the native and managed sides, and how that affects the runtime?

In any case, as a feature request, I’d ask for an option (I’d argue enabled by default) to make non-release managed builds clearly identified at runtime if that’s possible, just like you get a “DEVELOPMENT BUILD” text in the bottom-right corner. I think it really helps prevent actual development builds to be released to the wild, and it should be even more helpful when using managed code variants, as now it wouldn’t matter what native library is used, when testing the build you’d instantly know whether you screwed up or it’s a safe build to release. I will probably add my own implementation of that in my tools, but it would be nice to have it built-in.

The point of that example wasn’t to hide that information from the user: it was to hide it from whoever they send their logs to (maybe to you!) as they may contain private or personal information. If a build is running on their machine, they can always attach a debugger if they want to figure out what values variables hold so that scenario is not particularly useful to protect from. If they swap to a development build binary themselves, they reenable development logging gated behind Debug.isDebugBuild, which would be intended in this case.

The difference between development and non-development build is which engine binary (on Windows that’s UnityPlayer.dll) is used. These binaries ship precompiled in Unity editor installation directory. You can look in <UNITY_INSTALL_DIR>\Editor\Data\PlaybackEngines\windowsstandalonesupport\Variations. This determines a lot of things: whether the profiler exists, whether memory leak detector is enabled, whether certain errors and warnings get surfaced, etc. There are over 1200 places in the engine source code that behave differently based on whether it is a development build binary or not. This is only for native engine source code: both development and non-development builds share the same exact managed engine code DLLs, which we also ship precompiled.

The development build checkbox in the editor does two things:

  • For builds where the editor decides which native binary to put in the output folder, it ends up being the definitive choice between whether the resulting build is a development build.
  • (For now) it adds DEVELOPMENT_BUILD define to script compilation. This only affects code and packages in your project - not engine code.

This is one of the cases where the whole thing was broken: if you checked the development build checkbox, the only thing that the editor build process would do is add that managed define to script compilation. It has no power to choose whether the build ends up being a development build or not: you make that choice in Visual Studio. If you choose Debug or Release configuration, you end up with a development build. If you use Master configuration, you end up with a non-development build (which means you get scripts thinking it’s a development build while it’s not). Thinking about a single development build dimension that affects both native and managed code is incorrect, and was actually causing real serious bugs both internally at Unity, in Unity user projects and Asset Store packages.

This is actually a great suggestion! Thanks, I’ll see what we can do there.

I’ve been using this for a while and I like the separate instrumental vs checks so i think it’s a good improvement, but i really feel like it needs be a separate option in build profiles that isn’t dependent on with player settings

having to overload the entire player settings per platform for just this dropdown is super annoying and bug inducing. I’m always going to have at least 2 profiles, 1 for testing 1 for release so I’m always going to need to override this value at least once on a profile and be forced to use a player settings overload.

Thanks for the feedback. I agree with that, overloading whole player settings is a hassle. I’ll see what we can do here.