Avoid preprocessor directives all over the place

Preprocessor directives come in very handy if you develop a game for multiple platforms. They can include or exclude code based on the platform the app is running. They can also be used to add debugging stuff if the app is executed inside the Unity editor.

#if UNITY_EDITOR
  EnableMockPlacement();
#elif UNITY_IOS
  ARKitSessionSubsystem arKitSession = session.subsystem as ARKitSessionSubsystem;
  arKitSession.SetCoachingActive(true);
#elif UNITY_ANDROID
  EnableCustomAndroidCoaching();
#endif

Preprocessor directives are even more important for frameworks, because they have to support multiple Unity versions. Deprecated Unity API has to be hidden based on the current Unity version.

#if UNITY_2022_1_OR_NEWER
#else
#endif

What’s the problem with preprocessor directives?

Problems arise if preprocessor directives are scattered all over the place.The easiest way for many issues is to just add a preprocessor directive in any piece of code. E.g. if a UI uses different copy for iOS and Android. Or a compile error for an older Unity version needs to be fixed.

Hidden code within preprocessor directives behaves like it is not there at all. It’s not possible to find references to this hidden code. IDEs like Visual Studio and Rider won’t recognize it correctly. And also inside the Unity Editor this code is just not there.

This makes refactoring very dangerous. Renaming a variable won’t work for the hidden code of another platform. Removing an unused script might break the code for older Unity versions.

Clean separation with a factory

The solution is to separate between different code parts. It should be clear which code is used by all parts of the project and which code is only used for a specific platform.

A well-known solution is the factory method design pattern. The platform-dependent code is decoupled with an interface. As an example, I use an AR coaching overlay.

public interface IARCoaching
{
  void ShowCoachingOverlay();
}

The implementations are platform-dependent.A preprocessor directive encapsulates the whole file. Or even better, the file is added to a platform-dependent assembly.

public class iosARCoaching : MonoBehaviour, IARCoaching
{
  private ARKitSessionSubsystem arKitSession;

  public void ShowCoachingOverlay()
  {
    arKitSession.SetCoachingActive(true);
  }
}

public class AndroidARCoaching : MonoBehaviour, IARCoaching
{
  public void ShowCoachingOverlay()
  {
    // enable custom game objects for coaching overlay
  }
}

How is the correct implementation retrieved? Usually, the factory design pattern uses a static method to create and return the platform-dependent implementation.

In Unity it’s a bit different, because the implementation usually already exists as a component in the scene.

A simple solution is to add the factory and the implementation to the same game object. Then it’s possible to get the interface without needing to know which implementation it is.

public class ARCoaching : MonoBehaviour
{
  public IARCoaching GetCoaching()
  {
    return GetComponent<IARCoaching>();
  }
}

It’s also possible to allow references to interfaces inside the Unity inspector. Then the implementation can be located in a different game object.

public class ARCoaching : MonoBehaviour
{
  [SerializeField, Interface(typeof(IARCoaching))]
  private UnityEngine.Object arCoachingObject = null;
}

The best solution in my opinion is to use a Service Locator. The implementations register themselves to the service locator. Then, the correct implementation can be easily retrieved where it’s needed.

public class AndroidARCoaching : MonoBehaviour, IARCoaching
{
  void Awake()
  {
    ServiceLocator.Register<IARCoaching>(this);
  }
}

public class ARCoaching : MonoBehaviour
{
  public IARCoaching GetCoaching()
  {
    ServiceLocator.Get<IARCoaching>();
  }
}

Advantages

The platform-dependent code is encapsulated in it’s own file. Code for different platforms is not mixed up. It’s much harder to break things on currently inactive platforms.

It’s easier to test with different environments. For example, you could replace the UNITY_EDITOR preprocessor inside the factory and debug the production code inside the Unity editor.

Prev
Make a simple progress bar with UI images

Make a simple progress bar with UI images

The UI image offers more options than simply showing an image inside a canvas

Next
Spatial anchors

Spatial anchors

Spatial anchors denote a specific pose in the real world