Unity: Implementing Dark mode

Pudding Entertainment
8 min readApr 9, 2020

Dark mode as it is called on desktop or Dark theme as it is called on mobile devices is today’s trend. If you want your game to look modern and make users happy, having a possibility to switch between modes is a must-have feature. In this tutorial, I’ll show you how to add Dark mode support with relatively few changes to the existing icons and images.

Prerequisites

I’m using Unity 2019.3 here, but the solution will work in any other versions as well. I’ve implemented it for one of my games — Zen Jigsaw — find it in action there.

As always, source code is available at GitHub, and the link is at the end of the article.

This tutorial is logically divided into two parts: images preparation and implementation. The described approach might require basic image editing skills (I’m a Photoshop user). Ask your designer or leave a comment below, I will add a Photoshop part if there is a demand!

The final result will look like this:

Light (left) and Dark (right) modes

Note! While designing the Dark theme I stumbled upon this article with lots of valuable advice on colors, shapes and ideology in general. As it stated there “The recommended dark theme surface color is #121212” . #121212 is a color in the RGB model. I’ll be referring to it as DarkGrey.

Part 1. Adjusting images

It is important to keep all project’s assets in either Dark or Light mode. Don’t mix them up! Another point worth mentioning here is due to the minimalist design of the game there are only two colors used throughout the whole project— DarkGrey and White. However, the given solution, with a couple of tweaks, is applicable for a wider range of colors.

There are 3 different cases to implement, I will walk you through them one by one.

  1. Icons that differ in color only
Each icon is a separate png file with a transparent background.

You might be wondering, how come in the above picture the icons color is white whereas on the previous final result screen in the Light mode the color is black. I didn’t want to duplicate all icons in two forms and figured out that there is only one difference — color. Hence you can have all the icons in white by default and change them directly in Unity.

Making white icons DarkGrey

This of course applies only to icons with one solid color. If that is not the case then the next point is a solution.

2. Images that will have different sprites in each mode

My company's logos

This case is relatively straight-forward, you just need to have two versions of the same image. Consider looking into one of my previous articles for tips on how to reduce the size of a final apk/aab file.

3. Images that will remain the same regardless of the selected mode

Taken directly from Zen Jigsaw!

It plays no special role in this tutorial, just an image that won’t be changed.

As for scene setup, it is done in Light mode with a couple of images and text, everything is coming from the standard Unity UI package.

Scene setup

Now when the scene and images are prepared we can move on and implement logic around it.

Part 2. Resolving a mode

If you read any of my previous articles you already know that I’m a huge fan of clean code and separation of concerns. This tutorial is no different. Let’s start with the most important script here — UIModeResolver. This class will be responsible for everything related to UI mode resolution. As it is stated in the previous part we have 3 different cases to implement.

First of all, UIModeResolver should be able to find all white icons and texts on a scene and change their color depending on selected mode.

public class UIModeResolver : MonoBehaviour
{
private const float DarkGrey = 0.07f;
private readonly Color _darkGrey = new Color(DarkGrey, DarkGrey, DarkGrey, 1);
public void ResolveMode()
{
foreach (var text in GetComponentsInChildren<Text>(true))
{
text.color = text.color == Color.white ? _darkGrey : Color.white;
}
foreach (var image in GetComponentsInChildren<Image>(true))
{
image.color = image.color == Color.white ? _darkGrey : Color.white;
}
}
}

Note! DarkGrey color value is in RGB 0–1.0 format.

The idea here is to decide how to change an element based on its current color and turn it into opposite one. We have to rely on color instead of some boolean flags because assets on a scene can be of both colors in the same mode. For instance, if you decide to have a text of DarkGrey color and icons of white color in the Light mode, then in the Dark mode text will be white and icons will be DarkGrey.

Since we use the GetComponentsInChildren method this script should be attached to the parent object of the whole UI — Canvas.

As we don’t call it from anywhere yet, therefore let’s create another script responsible for scene UI handling in general and attach it to the Panel object:

public class UIController : MonoBehaviour
{
private UIModeResolver _modeResolver;

private void Awake()
{
_modeResolver = FindObjectOfType<UIModeResolver>();
}
public void ToggleUIMode()
{
_modeResolver.ResolveMode();
}
}

Last missing piece here is to call the ToggleUIMode method on a button press:

Now start the game, press the UIMode button and you will see:

Icons and text are perfectly aligned with the Dark mode!

Next, let’s implement the second case where one image has different sprites depending on the mode.

public class UIModeElement : MonoBehaviour
{
public Sprite LightModeSprite;
public Sprite DarkModeSprite;
public void ResolveImage()
{
GetComponent<Image>().sprite = PrefsHolder.IsDarkMode() ? DarkModeSprite : LightModeSprite;
}
}

It is a very small script but plays a crucial role in the whole architecture! Mostly, because it encapsulates mode resolution logic and narrows it down to just two sprites.

There is the PrefsHolder class that you might have seen in my previous tutorials. It is a simple static utility class, basically, a wrapper around Unity’s PlayerPrefs which will be responsible for storing whether Dark mode has been selected and returning its boolean value:

public static class PrefsHolder
{
private const string DarkMode = "DarkMode";
public static void SaveDarkMode(bool on)
{
PlayerPrefs.SetInt(DarkMode, on ? 1 : 0);
}
public static bool IsDarkMode()
{
return PlayerPrefs.GetInt(DarkMode, 0) == 1;
}
}

In big projects, it is quite convenient to have such wrappers instead of scattering properties around.

Going back to UIModeElement — let’s attach it to the Logo image and select sprites accordingly.

The last step is to wire it into UIModeResolver with only one change for image handling loop in the ResolveMode method:

public void ResolveMode()
{
...
foreach (var image in GetComponentsInChildren<Image>(true))
{
var uiModeElement = image.GetComponent<UIModeElement>();
if (uiModeElement)
{
uiModeElement.ResolveImage();
}
else
{
image.color = image.color == Color.white ? _darkGrey : Color.white;
}
}
}

Before switching back to Unity I propose we cover the third case where a sprite don’t need to be changed in different modes. Solution here is quite simple — UIModeElement can be reused with one small amendment:

public void ResolveImage()
{
if (LightModeSprite)
{
GetComponent<Image>().sprite = PrefsHolder.IsDarkMode() ? DarkModeSprite : LightModeSprite;
}
}

Did you notice that if statement? It is all we need! The idea here is if no sprite was specified nothing should be changed. Let’s attach it to the Puzzle image

It is time to see if everything is working as expected. Now start the game and press the UIMode button to switch between the modes:

Nearly final result

Part 3. Saving a state

As you can see from the screen above the UIMode button sprite is same in both modes which obviously is not correct. Also, selected state is still not persisted, therefore if a player selects the Dark mode and restarts the game he will see the Light mode instead. Let’s fix those two flaws!

We will start with a proper button icon image per mode. In current implementation the UIController script is a perfect place for such functionality since it is already responsible for toggling the modes.

public class UIController : MonoBehaviour
{
public Sprite[] UIModeImages;

private UIModeResolver _modeResolver;
private Button _uiMode;

private void Awake()
{
_modeResolver = FindObjectOfType<UIModeResolver>();
_uiMode = GetComponentInChildren<Button>();
_uiMode.image.sprite = UIModeImages[PrefsHolder.IsDarkMode() ? 0 : 1];
}
public void ToggleUIMode()
{
var state = !PrefsHolder.IsDarkMode();
PrefsHolder.SaveDarkMode(state);
_uiMode.image.sprite = UIModeImages[state ? 0 : 1];
_modeResolver.ResolveMode();
}
}

Three changes here are:

  • A public field with button sprites for both modes has been added
  • On Awake a proper button icon is resolved depending on the previously selected mode
  • In the ToggleUIMode method selected mode is saved and an icon sprite is changed accordingly

The last step left is to resolve Dark mode if it was previously selected and set up all the sprites. As we’ve designed the scene (and the whole game) in Light mode it is needed only if saved mode is the Dark one. This change will reside in the UIModeResolver script.

public class UIModeResolver : MonoBehaviour
{
...
private void Awake()
{
ResolveDarkMode();
}
private void ResolveDarkMode()
{
if (!PrefsHolder.IsDarkMode()) return;
ResolveMode();
}
...
}

As easy as it looks, on Awake we are checking the saved mode and, if needed, calling the ResolveMode method created previously.

Afterwards

Well done finishing the tutorial! Should you have any questions please leave them in the comments section below.

The project source files can be found at this GitHub repository.

The game I was referring to is available here Zen Jigsaw.

Support

If you like the content you read and want to support the author — thank you very much!
Here is my Ethereum wallet for tips:
0xB34C2BcE674104a7ca1ECEbF76d21fE1099132F0

--

--

Pudding Entertainment

Serious software engineer with everlasting passion for GameDev. Dreaming of next big project. https://pudding.pro