Sea otter in a circleSea Otter Games

#3 - Setting a mood with a Day/Night cycle

By Hugo on 2023-12-03
Game Development
Unity
C#
Java

My game project is many things but set in stone. Currently, anything could be subject to change and being my first real-scale video game project, I think it is for the best. One thing I was sure I wanted however was a day/night cycle.

I like a game that immerses its player into a great atmosphere and day/night cycles are a great way to do that. During the day, the game behaves as you would expect any game to, but it is during the night phase that designers can express their art to convey specific atmospheres.

A screenshot of the game kingdom showing the center of the village at night.
Screenshot from the game Kingdom - Here the night is mostly stressful but can feel almost cozy once hidden behind large walls and armies.
A screenshot of the game moonlighter showing the center of the village at night.
Screenshot from the game Moonlighter - The atmosphere of the village at night is so peaceful. It is a beautiful contrast from the chaos of the dungeons you are about to face.

I could keep pumping tons of screenshots night phases from several games that left me with durable memories: my first night in a dirt hut in Minecraft, fending off your first alien attack at nighttime in Factorio or simply sailing through the Ancient Isles under the moon of Sea of Thieves are only a few more examples 😊.

How to implement a simple day/night cycle in Unity

Day/night cycles are essentially a glorified loop. Instinctively, it should include two phases, day and night, but you could easily add more for more precise control: dawn, twilight or any recurring event such as a blood moon or a daily town gathering for instance. For my game, I kept it simple (for now 🙂) with simply day and night. Being a background loop, the go-to system in Unity is coroutines, however, you could probably also make it work with asynchronous methods.

1private void InitializeCycle() 2{ 3 phase = DayNightCyclePhases.Day; 4 Start(DayNightCyclePhases.Day); 5} 6 7private void Start(DayNightCyclePhases phase) 8{ 9 StartCoroutine(StartCycleCoroutine(phase)); 10} 11 12private IEnumerator StartCycleCoroutine(DayNightCyclePhases phase) 13{ 14 OnCyclePhaseStart.Invoke(phase); 15 16 float timer = 0; 17 float phaseDuration = phasesDurations.At(phase); 18 while (timer < phaseDuration) 19 { 20 timer += Time.deltaTime; 21 yield return null; 22 } 23 24 OnCyclePhaseEnd.Invoke(phase); 25 DayNightCyclePhases nextPhase = phase.Next(); 26 Start(nextPhase); 27}

💡For beginners, a coroutine is a type of method described by the IEnumerator return type. It is called using the method StartCoroutine(). The yield return instructs to re-enter the loop with the current variables which allows to process loops over multiple frames.

This system revolves around a generic coroutine which loops for the duration of the current phase and eventually calls itself again for the next phase. My phases are described in a simple Enum.

1public enum DayNightCyclePhases 2{ 3 Day, 4 Night 5}

You may have noticed the line 25 DayNightCyclePhases nextPhase = phase.Next(); and thought something like: “Calling a method on an enumeration instance ? WTF??” or maybe “Enums are not meant to represent an order in elements! Yuck!” or maybe nothing at all 😃. Truth is, I am used to working with Java at work. And, in Java, Enums are such powerful tools, allowing you to store anything in them to the point of using them as highly maintainable maps. I wish this was available in C# be it just to store the duration of each phase in the Enum.

In Java, this is how my implementation would look like. This would even have allowed us to store class instances for each phase to call methods shared by all phases later on.

1public enum DayNightCyclePhases { 2 3DAY(10), 4NIGHT(10); 5 6private final int phaseDuration; 7 8DayNightCyclePhases(final int phaseDuration) { 9 this.phaseDuration = phaseDuration; 10} 11 12public int getPhaseDuration() {...} 13}

However, we are stuck with C#. Do not get me wrong: I love C# and I wish some of its tools were in Java as well. But for now, we will have to make this lame enums work. As such, I need a few extensions, namely:

The Next() method (source).

1public static T Next<T>(this T src) where T : struct 2{ 3 if (!typeof(T).IsEnum) throw new ArgumentException(String.Format("Argument {0} is not an Enum", typeof(T).FullName)); 4 5 T[] Arr = (T[])Enum.GetValues(src.GetType()); 6 int j = Array.IndexOf<T>(Arr, src) + 1; 7 return (Arr.Length == j) ? Arr[0] : Arr[j]; 8}

The serializable dictionary.

1public class SerializableDictionary<TKey, TValue> 2{ 3 [System.Serializable] 4 public class DictionaryItem 5 { 6 public TKey key; 7 public TValue value; 8 9 public DictionaryItem(TKey key, TValue value) 10 { 11 this.key = key; 12 this.value = value; 13 } 14 } 15 16 public List<DictionaryItem> elements = new List<DictionaryItem>(); 17} 18 19// At(), Remove(), Count() methods are implemented below

The loop loops, what now?

Our loop loops but it is essentially useless. We need it to do things. To keep it tidy, the best would be to avoid directly calling methods from the coroutine’s body. One solution would be to have several classes for each phase, a way to get these class instances from the phase enum element and then call the methods they have in common. This implementation, again, takes inspiration from Java’s way of doing things, let’s do something else! Why not some event?

To do so, simply declare a delegate and a few events for each phase during which you want to do things.

1public delegate void cyclePhaseHandler(DayNightCyclePhases phase); 2public static event cyclePhaseHandler OnCyclePhaseStart; 3public static event cyclePhaseHandler OnCyclePhaseEnd;

These are the events that are called before and after the loop I presented above. Throughout your components, you can now subscribe methods to these events during initialization and they will be executed at the right time in your cycle.

1private void Awake() 2{ 3 OnCyclePhaseStart += OnPhaseStart; 4 OnCyclePhaseEnd += OnPhaseEnd; 5} 6private void OnPhaseStart(DayNightCyclePhases phase) 7{ 8 Debug.Log("Phase " + phase.ToString() + " has started.); 9} 10private void OnPhaseEnd(DayNightCyclePhases phase) 11{ 12 Debug.Log("Phase " + phase.ToString() + " has ended."); 13}

Let’s use our cycle to light our world

In this part, I will show you how to use dynamic lights to light your world throughout the phases of your cycle. We will use a day/night cycle to keep it simple.

Disclaimer: In this part, you will need the Universal Render Pipeline (URP) which can be quite the hassle to set up (I underestimated that part and it took me a couple of painful hours…). Hopefully, I wrote another article to guide you through the process of installation! Everything will revolve around one component: the mighty Light2D component!

A screenshot of the light2d component in Unity. Amongst other it has an intensity and a colour parameter.
The Light2D component

Used as “Global” this will set a base lighting on all your sprites. Note that you can make it ignore some sprites by playing around with the sorting layers. This is the base element if you want to set a mood using dynamic lighting. Essentially, what we will be doing here is modifying its colour and intensity throughout the day: during the day warm colours with an intensity close to 1 at midday and during the night cold colours with low intensity.

Colours

For each phase, I need a colour gradient. This will allow me to have a corresponding hue for every moment of the day. To ensure the correspondency of the gradient with each phase, I used my Serializable dictionary:

1[Header("Global lighting colors")] 2[SerializeField] private SerializableDictionary<DayNightCyclePhases, Gradient> phaseColorGradients;
A screenshot of the colour dictionnary.
Each phase is associated with a gradient which represents the colours the sky will take.

Using this, you can simply add elements to your dictionary and set the Gradients manually. For the anecdote, I did not know what colours to use. For the night, I improvised with palettes I found online. For the day, however, I used the gradient of the hues of the Planckian locus according to temperature which I then mirrored horizontally. In physics, this gradient is used to describe the colour of incandescent bodies.

A color gradient illustrated the Plackian locus.

Intensity

For the intensity, it works similarly. However, we will not use Gradients but rather AnimationCurves which simply allow us to create a curve and evaluate its value.

1[Header("Global lighting intensities")] 2[SerializeField] private SerializableDictionary<DayNightCyclePhases, AnimationCurve> phaseIntensityGradients;
A screenshot of the intensity dictionnary.
During the day, light intensity increases to reach 0.8 by midday and starts to drop at the end of the afternoon. During the night, it's mostly dark 😌 (Yeah, you can quote me on that)

💡 Try as much as possible to create a smooth transition between phases: the last colour of the day should be the same as the first colour of the night. The same applies to the intensity value.

In the end, all there remains to do is to update your Light2D component at every step of your loop.

Methods to call from within your loop.

1private void UpdateGlobalLightColor(DayNightCyclePhases currentPhase, float phaseProgress) 2{ 3 Gradient phaseColorGradient = phaseColorGradients.At(currentPhase); 4 if (phaseColorGradient == null) 5 { 6 Debug.LogError("Error : the phase color gradient does not exist for phase " + currentPhase); 7 return; 8 } 9 globalLight.color = phaseColorGradient.Evaluate(phaseProgress); 10} 11 12private void UpdateGlobalLightIntensity(DayNightCyclePhases currentPhase, float phaseProgress) 13{ 14 AnimationCurve phaseIntensityCurve = phaseIntensityGradients.At(currentPhase); 15 if (phaseIntensityCurve == null) 16 { 17 Debug.LogError("Error : the phase intensity curve does not exist for phase " + currentPhase); 18 return; 19 } 20 globalLight.intensity = phaseIntensityCurve.Evaluate(phaseProgress); 21}
A gif of the day/night cycle in motion.
Here is the result!

Once again, thank you for reading to the end! I wanted to do two pieces on UI Toolkit but I would rather write about something I did more recently. I hope to see you in the next post. 🦦

© Copyright 2024 - 🦦 Sea Otter Games