Safe ways to use defines

Andrey Vishnitskiy
Hypemasters
Published in
4 min readApr 17, 2023

This article is written to help any developer faced with errors related to defines in the code. Namely, for those of you who’ve performed some renaming via IDE, and suddenly, have a compilation time error (CTE) on the release build. After reading this article, you will use methods that are less error-prone when using defines, enabling you to face fewer cases of CTEs.

At Hypemasters, our engineer corps is working tirelessly to make the best RTS games ever

Simple use define

Let’s take a look at a simple case of using

using UnityEngine;
using UnityEditor;
//...
private void Start()
{
DoSmthEditor();
}

private void DoSmthEditor()
{
#if UNITY_EDITOR
AssetDatabase.SaveAssets();
#endif
}

Issues

The above example demonstrates how we cannot compile that code in any build. This code will only work in Editor.

This method uses the AssetDatabase class, which was declared in UnityEditor. Of course, to use it, we have to declare using UnityEditor at the top of the file.

Here, our main problem is a using, what your IDE usually imports into a file. This may work well in regular cases, but not if you are using defines. In this particular case, we imported UnityEditor into a file, which does not exist in builds. That’s the reason why we cannot compile this example to any build, mobile, console, pc, etc.

Option to solve

Now that we understand the issues, it’s time to resolve them.

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
//...
private void Start()
{
DoSmthEditor();
}

private void DoSmthEditor()
{
#if UNITY_EDITOR
AssetDatabase.SaveAssets();
#endif
}

It compiles, but here, you have one more define using. If you will implement any additional using, then it will still automatically be placed out of the define, and will lead to CTE once more.

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using Reimporter.AvailableOnlyInEditor;

//...
private void DoSmthEditor()
{
#if UNITY_EDITOR
AssetDatabase.SaveAssets();
Reimporter.ReimportAllFolders();
#endif
}

Clearly, this potential solution is not so safe. How can we improve it?

Case with full namespace

using UnityEngine;
//...

private void Start()
{
DoSmthEditor();
}

private void DoSmthEditor()
{
#if UNITY_EDITOR
UnityEditor.AssetDatabase.SaveAssets();
#endif
}

In this example, we use the fully qualified class name.

Do not leave the using namespace on top of the file in the way it was suggested by your IDE. Just use the full name of the classes inside your defines to avoid having the CTE in that case.

Issues

This approach has almost no issues except for the following:

1) In cases with over 10–20 lines, it can be hard to maintain that rule

2) It still won’t rename it safely. This is because if the current context has disabled defines, then the code under disabled defines is not active. It won’t invoke an IDE; it’s just a text like a comment. Once that code is activated by enabling a define, you will find yourself with a CTE.

Conditional

Can this last case be resolved as well? Yes.

using UnityEngine;
//...
private void Start()
{
DoSmthEditor();
}

[Conditional("UNITY_EDITOR")]
private void DoSmthEditor()
{
UnityEditor.AssetDatabase.SaveAssets();
}

When using a [Conditional()] attribute, you will have always code available in IDE, even when your defines are not active.

That way, it is easier for your to analyze code, display warnings, make suggestions, and perform the refactoring. This is true in the case of renaming as well.

Issues

This approach can be very helpful and preferable in most cases, but it has limitations as well:

- The method must return void

- It doesn’t support ‘or’ operations like ‘UNITY_EDITOR || UNITY_IOS’

Bonus — logger tip

I think most projects have some level of logging, and we should enable and disable this function on release and development builds.

We can easily do this as follows:

public void LogTrace(string message)
{
#if LOGLEVEL_TRACE
Log(LogLevel.Trace, message)
#endif
}

It works, but it also presents an issue. The LogTrace method still compiles and executes with strings created from invocation. And strings to make a log that is still creating for each not-enabled log generate garbage in a heap. i.e. any string concatenations or formatting would still take place.

Just use a [Conditional] attribute to improve the performance of your project as follows:

[Conditional("LOGLEVEL_TRACE")]
public void LogTrace(string message)
{
Log(LogLevel.Trace, message)
}

Project limitation

A final approach would be to limit your code by defines in .asmdef. In the case of mismatched defines, you would simply cut the whole assembly. This method works well for tests, plugins, and packages and presents no disadvantages, but your architecture may require you to split it by different assemblies.

For additional information, check out Unity’s documentation.

Conclusion

It may seem like a tiny theme, but when you use defines, you will be faced with more and more CTE. Some will occur because of renaming or refactoring, while others will result from inaccurate or insufficient experience with defines. Without implementing best practices and additional code checks, you will find yourself experiencing more and more broken develop compilations.

All these approaches have their own pros and cons. Some will be better for some cases, while others are more optimal in others.

It’s up to you to try and decrease your defines count. You can do this using various interface implementations through dependency injection. This is safe, and transparent, and enables you to achieve a lower repeatable code count (using define).

For example,

public class LoggerProvider : ILoggerProvider
{
public ILogger GetLogger(string category)
{
#if LOGS_ENABLES
return new Logger(category);
#endif
return new NullLogger();
}
}

Or, you can only use one define and have a transparent logic in its dedicated class:

void InstallBindings()
{
#if LOGS_ENABLED
Container.BindAllInterfacesTo<LoggerProvider>().AsSingle();
#else
Container.BindAllInterfacesTo<NullLoggerProvider>().AsSingle();
#endif
}

public class NullLoggerProvider : ILoggerProvider
{
public ILogger GetLogger(string category)
{
return new NullLogger();
}
}

public class LoggerProvider : ILoggerProvider
{
public ILogger GetLogger(string category)
{
return new Logger(category);
}
}

Chat to us if you are passionate about creating games and want to experience one of the most dynamic cultures in gaming today join@hypemasters.com

--

--

Hypemasters
Hypemasters

Published in Hypemasters

Hypemasters (est. 2019) is an international game development studio with offices in Europe and the UAE. We are on a mission to build the top mid-core PvP gaming company in the world, one that disrupts and defines gaming genres.

Andrey Vishnitskiy
Andrey Vishnitskiy

Written by Andrey Vishnitskiy

Technical Director at Hypemasters