| |

Coding a Weather System [PART 1]

Hello everybody! Lately it came to my mind to create a small weather system for my game in Unity Engine, as I want it to be dynamic for each day and depending on the season. On a playable level it will affect the gameplay, but for that there is still a lot of time and planning to do.

But, creating the system that I want to change the weather I could do it now without problem, since it is practically all logic and not touching much in the editor part, or at least, in this first part of the tutorial.

As always, this code can be improved, for sure, but at the moment this is my way of doing it and can be updated in the future. Here we go.

[System.Serializable]
public enum Season
{
  Spring = 0,
  Summer = 1,
  Autumn = 2,
  Winter = 3
}

[System.Serializable]
public enum Weather
{
  None,
  Sun,
  SunAndClouds,
  Clouds,
  Fog,
  FogLightRain,
  LightRain,
  NormalRain,
  HeavyRain,
  ElectricStorm,
  RainStorm,
  LightSnow,
  Snow
}

[System.Serializable]
public enum DayType
{
  None,
  Sunny,
  Cloudy,
  Foggy,
  Rainy,
  Snowy,
  Stormy
}

[System.Serializable]
public enum RainProbability
{
  None,
  ExtremelyLow,
  VeryLow,
  Low,
  Medium,
  High,
  VeryHigh,
  ExtremelyHigh
}

To begin with, I have created the above enums with the following scheme in mind:

  • Season: Each season will have a series of probabilities for its day to be Sunny, Rainy, etc. Since it is not the same the amount of days that it can rain in a summer month than in an autumn month.
  • DayType: Normally, days are divided into sunny, rainy, cloudy, stormy‚Ķ. And this will be the most common in each day, but that does not mean that if a day is sunny, it can not rain in the afternoon in the form of thunderstorms or light rain, because in each season, the weather can change.
  • RainProbability: Depending on the Season and the time of day, there will be a probability of rain. Normally, in summer, thunderstorms may occur in the afternoon or evening, but it is more rare that they occur at noon or in the morning. The enum of rain probability will be distributed in 24 hours, with different types of probability per Season.
  • Weather: I have put all the types of weather I could think of (rather, the ones I wanted to be in the game), and these can be distributed by Season, by days and finally by hours. This enum will follow some rules that you will see in the code, through dictionaries.

In order to relate Season with DayType, Rain Probability with Season and the DayType with Weather, I have created a Dictionary for each type. At first I thought it could be done with a well assembled system of Scriptable Objects, but as it is designed so that once finished, there will be no major changes or add new things, I have done it all in code and in the same class (or at least, in this first part is so).

private Dictionary<Season, Dictionary<DayType, int>> seasonProbabilities = new Dictionary<Season, Dictionary<DayType, int>>()
  {
      { Season.Spring, new Dictionary<DayType, int>
          {
              { DayType.Sunny, 40 },
              { DayType.Rainy, 23 },
              { DayType.Cloudy, 16 },
              { DayType.Stormy, 12 },
              { DayType.Foggy, 9 }
          }
      },
      { Season.Summer, new Dictionary<DayType, int>
          {
              { DayType.Sunny, 70 },
              { DayType.Stormy, 18 },
              { DayType.Cloudy, 7 },
              { DayType.Rainy, 5 }
          }
      },
      { Season.Autumn, new Dictionary<DayType, int>
          {
              { DayType.Rainy, 35 },
              { DayType.Stormy, 25 },
              { DayType.Sunny, 20 },
              { DayType.Cloudy, 15 },
              { DayType.Foggy, 5 }
          }
      },
      { Season.Winter, new Dictionary<DayType, int>
          {
              { DayType.Snowy, 40 },
              { DayType.Foggy, 30 },
              { DayType.Sunny, 20 },
              { DayType.Cloudy, 10 } 
          }
      }
  };

In the seasonProbabilities dictionary, along with the DayType, a percentage is assigned from highest to lowest probability. In my case, the sum of all the DayType will not exceed 100. In the case in which I program the time, it would not matter how you want to put the percentages (in terms of numbers), but the way I will program it, I put them from highest to lowest probability, later you will see why.

private Dictionary<DayType, Dictionary<Weather, int>> weatherProbabilities = new Dictionary<DayType, Dictionary<Weather, int>>()
    {
        { DayType.Sunny, new Dictionary<Weather, int>
            {
                { Weather.Sun, 75 },
                { Weather.SunAndClouds, 25 }
            }
        },
        { DayType.Cloudy, new Dictionary<Weather, int>
            {
                { Weather.Clouds, 60 },
                { Weather.LightRain, 30 },
                { Weather.SunAndClouds, 10 }
            }
        },
        { DayType.Rainy, new Dictionary<Weather, int>
            {
                { Weather.NormalRain, 45 },
                { Weather.HeavyRain, 30 },
                { Weather.LightRain, 13 },
                { Weather.SunAndClouds, 7 },
                { Weather.Clouds, 5 } 
            }
        },
        { DayType.Foggy, new Dictionary<Weather, int>
            {
                { Weather.Fog, 50 },
                { Weather.LightRain, 20 },
                { Weather.LightSnow, 15 },
                { Weather.Clouds, 10 },
                { Weather.SunAndClouds, 5 },
            }
        },
        { DayType.Snowy, new Dictionary<Weather, int>
            {
                { Weather.Snow, 50 },
                { Weather.LightSnow, 30 },
                { Weather.Clouds, 10 },
                { Weather.Fog, 10 }
            }
        },
        { DayType.Stormy, new Dictionary<Weather, int>
            {
                { Weather.RainStorm, 45 },
                { Weather.ElectricStorm, 25 },
                { Weather.SunAndClouds, 20 },
                { Weather.NormalRain, 5 },
                { Weather.Clouds, 5 }
            }
        },
    };

For the Weather that will make each day, we do the same as with the previous Dictionary.

To put the probabilities for each hour, I have made an approximation of what can really happen in each station, but apart from that, I have wanted that normally, in the central hours is where the weather is more stable with less chance of precipitation.

private Dictionary<Season, List<RainProbability>> hourProbabilities = new Dictionary<Season, List<RainProbability>>
{
  { Season.Winter, new List<RainProbability> 
    {
      RainProbability.Medium,
      RainProbability.High,
      RainProbability.High,
      RainProbability.High,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Low,
      RainProbability.Low,
      RainProbability.Low,
      RainProbability.VeryLow,
      RainProbability.ExtremelyLow,
      RainProbability.None,
      RainProbability.None,
      RainProbability.None,
      RainProbability.ExtremelyLow,
      RainProbability.ExtremelyLow,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.High,
      RainProbability.High,
      RainProbability.Medium,
      RainProbability.Medium
    }
  },
  { Season.Spring, new List<RainProbability>
    {
      RainProbability.VeryLow,
      RainProbability.VeryLow,
      RainProbability.Low,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.VeryLow,
      RainProbability.ExtremelyLow,
      RainProbability.VeryLow,
      RainProbability.ExtremelyLow,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.VeryLow,
      RainProbability.VeryLow,
      RainProbability.Medium,
      RainProbability.High,
      RainProbability.VeryHigh,
      RainProbability.Medium,
      RainProbability.Low,
      RainProbability.VeryLow
    }
  },
  { Season.Summer, new List<RainProbability>
    {
      RainProbability.VeryLow,
      RainProbability.ExtremelyLow,
      RainProbability.ExtremelyLow,
      RainProbability.Low,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Low,
      RainProbability.ExtremelyLow,
      RainProbability.ExtremelyLow,
      RainProbability.None,
      RainProbability.None,
      RainProbability.None,
      RainProbability.None,
      RainProbability.None,
      RainProbability.None,
      RainProbability.ExtremelyLow,
      RainProbability.Low,
      RainProbability.Low,
      RainProbability.Low,
      RainProbability.Medium,
      RainProbability.High,
      RainProbability.ExtremelyHigh,
      RainProbability.High,
      RainProbability.Medium
    }
  },
  { Season.Autumn, new List<RainProbability>
    {
      RainProbability.High,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Low,
      RainProbability.VeryLow,
      RainProbability.Medium,
      RainProbability.High,
      RainProbability.ExtremelyHigh,
      RainProbability.High,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.Low,
      RainProbability.VeryLow,
      RainProbability.Low,
      RainProbability.Low,
      RainProbability.Low,
      RainProbability.Medium,
      RainProbability.Medium,
      RainProbability.High,
      RainProbability.VeryHigh,
      RainProbability.ExtremelyHigh,
      RainProbability.ExtremelyHigh,
      RainProbability.High,
      RainProbability.High
    }
  }
};

For each Season, we make a list of 24 RainProbability, one for each hour of the day (depending on how you manage the weather in your game, you can change it as you like). As a general rule, I have made that in the central hours of the day, as usual, there is a lower probability of rain, whatever the season is, although with variations.

private Dictionary<RainProbability, int> rainProbabilityMappings = new Dictionary<RainProbability, int>()
{
  { RainProbability.None, 0 },
  { RainProbability.ExtremelyLow, 5 },
  { RainProbability.VeryLow, 10 },
  { RainProbability.Low, 30 },
  { RainProbability.Medium, 50 },
  { RainProbability.High, 65 },
  { RainProbability.VeryHigh, 75 },
  { RainProbability.ExtremelyHigh, 100 }
};

And finally, the RainProbability type I put it differently in terms of percentages because I check it differently, as we will see later. Now we will see the properties that I have created for this first part:

DayType currentDayType = DayType.None;
DayType previousDayType = DayType.None;

Weather currentWeather = Weather.None;
Weather previousWeather = Weather.None;

Queue<Weather> weatherQueue; // Where the Weather for each hour will be saved

First, we will initialize weatherQueue. Provisionally in this first part we will put it in the Start function:

private void Start()
{
  weatherQueue = new Queue<Weather>();
  GenerateWeatherDay(Season.Autumn); // A example season for testing.
  Debug.Log("Today will be a " + currentDayType.ToString() + " day!");
}

GenerateWeatherDay

There will be two main functions for this first part, GenerateWeatherDay (which will generate what kind of day it will be) and GetWeatherPerHour (which, given a certain hour, will generate the weather). The first one is the simplest one:

void GenerateWeatherDay(Season season)

To this, the current Season will be passed, and from there it will get the type of day, taking into account what type of day it has had previously, because if the previous day has been sunny, I do not want the next one to be rainy, but at most, cloudy. The same goes the other way around, if the previous day was rainy, I don’t want the current day to be sunny. I don’t want the weather changes to be abrupt.

Dictionary<DayType, int> tempProbabilities = new Dictionary<DayType, int>(seasonProbabilities[season]);

tempProbabilities.Remove(DayType.None);

if (previousDayType == DayType.Sunny)
{
  tempProbabilities.Remove(DayType.Rainy);
  tempProbabilities.Remove(DayType.Snowy);
}
else if(previousDayType == DayType.Rainy || previousDayType == DayType.Snowy)
{
  tempProbabilities.Remove(DayType.Sunny);
}

To save a copy of the part of the dictionary that we want, we have to create a new dictionary, because if we want to remove elements from it, as it is in my case, you will remove it directly from the main dictionary. Therefore, a new Dictionary is created to be able to manipulate the data in the way we want.

float totalProbability = 0;
foreach (var day in tempProbabilities)
{
  totalProbability += day.Value;
}

// Select what type of weather will be
float randomValue = UnityEngine.Random.Range(0,totalProbability);
float cumulativeProbability = 0;
DayType selectedDayType = DayType.None;

foreach (var kvp in tempProbabilities)
{
  cumulativeProbability += kvp.Value;

  // If the random value is less than the cumulative probability, select this day type
  if (randomValue < cumulativeProbability)
  {
    selectedDayType = kvp.Key;
    break;
  }
}

if (selectedDayType == DayType.None)
{
  selectedDayType = previousDayType;
}

currentDayType = selectedDayType;
previousDayType = currentDayType;

Do you remember the probabilities I told you about? After removing from the dictionary the days that we do not want it to come out for today, we have to add up the total probabilities of the remaining days, since now they will not add up to 100 as they did when we first created it.

Then, we draw a Random between 0 and the total probability. Next, in the foreach loop the variable kvp (KeyValuePair) will be the one that runs through the tempProbabilities dictionary. At the beginning of the loop we add the cumulative sum of the values of each day type. If the random number is LESS than the probability of the day, this will be the chosen day and we exit the loop. If it is not, we add again the possibility of the next day, until we reach the last day, which is the day with less probability.

To be understood, we have the following days:

{ DayType.Sunny, 40 },
{ DayType.Rainy, 23 },
{ DayType.Cloudy, 16 },
{ DayType.Stormy, 12 },
{ DayType.Foggy, 9 }

As you can see, the probabilities go from highest to lowest, and the numbers cannot be equal with this method (now you will see why and a solution if you want to have days with the same probability).

If the number Random rolls a 70, he first checks Sunny, who has 40, so he cannot choose it and continues to the next one, adding his probability 40+23 = 63. Now Rainy is as if he has a 63, but it is still a smaller number than Random, so he goes to the next one, which would be Cloudy: 63+16 = 79. 79 is now already a smaller number than Random, so the chosen day would be Cloudy.

Now you may be wondering, “What if I want several days to have the same probability? With this method it will always come out the first one with that probability“. And you’re right. To do this, you can create an array before entering the foreach loop and go adding the days is that array (without breaking and exiting the loop), checking that the days inserted in the array have the same probability. Once you have them, out of the loop you could choose them by doing a Random between 0 and the count of elements of that array.

It has a few extra steps, so I decided not to repeat numbers in the dictionary, but now you know you can do it without any problem. Let’s continue.

PopulateForecast(season);

At the end of the function we call the PopulateForecast function, which will call the function for each desired hour.

PopulateForecast

weatherQueue?.Clear();

for (int i = 0; i < weatherQueueSize; i++)
{
  weatherQueue.Enqueue(GetWeatherPerHour(season, i));
}

It is a very simple function. We check that weatherQueue is not NULL, and if it is not, we remove all the possible Weather that could have been left from previous repetitions (in case it has been called previously).

You know that, in Unity C#, instead of:

if(myGameObject != null)
{
  myGameObject.DoThings();
}

You can add a “?” at the end of an element and then the method, and it’s the same. It’s very useful for code reading.

myGameObject?.DoThings();

weatherQueueSize should be the desired number of hours (in my case, 24), and with that time, we pass it to the function GetWeatherPerHour, that depending on the desired time and the Season, will choose the weather.

GetWeatherPerHour

As it is a function that is called by a Weather Queue, it must return a Weather:

Weather GetWeatherPerHour(Season season, int hour)

As we did in the day selection function, we create a new dictionary by selecting the data we want:

Dictionary<Weather, int> tempProbabilities = new Dictionary<Weather, int>(weatherProbabilities[currentDayType]);

And now, I have split the function into 4 regions:

  1. Season weather discards: We discard, per season, the possible weather from the dictionary.
  2. Next weather discards: Similar to what we did in the selection of the day, depending on the weather in the previous hour, we remove from the dictionary the possible remaining weather so that the change of weather is more progressive and does not go directly from heavy rain to sunny, and vice versa.
  3. Rain probability selection: Depending on the probability of rain for that particular Season and weather, we will discard the necessary Weather, to select them in the final step.
  4. Select weather between last posibilities: We will do the same as we did in the final step of selecting the day type: From the remaining available Weather, we will sum the probabilities and loop through them with a foreach loop.
tempProbabilities.Remove(Weather.None);
switch (season)
{
  case Season.Spring:
    tempProbabilities.Remove(Weather.Snow);
    tempProbabilities.Remove(Weather.LightSnow);
    tempProbabilities.Remove(Weather.HeavyRain);
    break;
  case Season.Summer:
    tempProbabilities.Remove(Weather.Snow);
    tempProbabilities.Remove(Weather.FogLightRain);
    tempProbabilities.Remove(Weather.LightSnow);
    tempProbabilities.Remove(Weather.HeavyRain);
    break;
  case Season.Autumn:
    tempProbabilities.Remove(Weather.Snow);
    tempProbabilities.Remove(Weather.LightSnow);
    break;
  case Season.Winter:
    tempProbabilities.Remove(Weather.HeavyRain);
    tempProbabilities.Remove(Weather.NormalRain);
    tempProbabilities.Remove(Weather.ElectricStorm);
    tempProbabilities.Remove(Weather.RainStorm);
    break;
}

if(currentWeather != Weather.None)
{
  previousWeather = currentWeather;
}
else
{
  previousWeather = Weather.None;
}

Depending on the current season, we will remove the possible Weather that may exist, and prepare the currentWeather and previousWeather variable, in case a previous Weather does not exist (because it is the first Weather to be created).

bool canRain = true;
if (previousWeather == Weather.None)
{
  List<Weather> randomWeather = tempProbabilities.Keys.ToList();
  int randomIndex = UnityEngine.Random.Range(0, randomWeather.Count);
  previousWeather = randomWeather[randomIndex];
}
  // Due to previous weather, remove possibilities from the next weather
switch (previousWeather)
{
  case Weather.Sun:
    tempProbabilities.Remove(Weather.NormalRain);
    tempProbabilities.Remove(Weather.HeavyRain);
    tempProbabilities.Remove(Weather.ElectricStorm);
    tempProbabilities.Remove(Weather.RainStorm);
    tempProbabilities.Remove(Weather.LightSnow);
    tempProbabilities.Remove(Weather.Snow);
    canRain = false;
    break;
  case Weather.SunAndClouds:
    tempProbabilities.Remove(Weather.RainStorm);
    tempProbabilities.Remove(Weather.Snow);
    canRain = false;
    break;
  case Weather.Clouds:
    tempProbabilities.Remove(Weather.Sun);
    break;
  case Weather.LightRain:
    tempProbabilities.Remove(Weather.Sun);
    break;
  case Weather.NormalRain:
    tempProbabilities.Remove(Weather.Sun);
    tempProbabilities.Remove(Weather.SunAndClouds);
    break;
  case Weather.HeavyRain:
    tempProbabilities.Remove(Weather.Sun);
    tempProbabilities.Remove(Weather.SunAndClouds);
    tempProbabilities.Remove(Weather.Fog);
    break;
  case Weather.ElectricStorm:
    tempProbabilities.Remove(Weather.Sun);
    break;
  case Weather.RainStorm:
    tempProbabilities.Remove(Weather.Sun);
    break;
  case Weather.Snow:
    tempProbabilities.Remove(Weather.Fog);
    break;
}

A variable canRain is created. Depending on the weather of the previous hour, the possible Weather is removed from the dictionary and canRain is set to false, if it is the case.

You may have seen that in the dictionary I am using Remove() to remove elements from it without knowing if that element is present in the dictionary or not. The Remove method checks if that element exists, and if it does, it removes it.

if (canRain)
{
  float probabilityRandom = UnityEngine.Random.Range(0, 100);
  int rainProbability = rainProbabilityMappings[hourProbabilities[season][hour]];
  tempProbabilities.Remove(Weather.None);

  if (probabilityRandom <= rainProbability)
  {
    // Will rain
    tempProbabilities.Remove(Weather.Sun);
    tempProbabilities.Remove(Weather.SunAndClouds);
    tempProbabilities.Remove(Weather.Clouds);
    tempProbabilities.Remove(Weather.Fog);
  }
  else
  {
    // Will NOT rain
    tempProbabilities.Remove(Weather.LightRain);
    tempProbabilities.Remove(Weather.NormalRain);
    tempProbabilities.Remove(Weather.HeavyRain);
    tempProbabilities.Remove(Weather.LightSnow);
    tempProbabilities.Remove(Weather.Snow);
    tempProbabilities.Remove(Weather.ElectricStorm);
    tempProbabilities.Remove(Weather.RainStorm);
    tempProbabilities.Remove(Weather.FogLightRain);
  }
}
else
{
  tempProbabilities.Remove(Weather.LightRain);
  tempProbabilities.Remove(Weather.NormalRain);
  tempProbabilities.Remove(Weather.HeavyRain);
  tempProbabilities.Remove(Weather.LightSnow);
  tempProbabilities.Remove(Weather.Snow);
  tempProbabilities.Remove(Weather.ElectricStorm);
  tempProbabilities.Remove(Weather.RainStorm);
  tempProbabilities.Remove(Weather.FogLightRain);
}

If it can rain because of the canRain bool, there are two options: To make the calculation of the probabilities with the Random number and that day it may rain or not (true), or directly it may not rain (false). In both cases, we will remove the corresponding Weather from the temporary dictionary.

float totalProbability = 0;
foreach (var weather in tempProbabilities)
{
  totalProbability += weather.Value;
}

float randomValue = UnityEngine.Random.Range(0f,totalProbability);
float cumulativeProbability = 0;
Weather selectedWeather = Weather.None;

foreach (var kvp in tempProbabilities)
{
  cumulativeProbability += kvp.Value;

  // If the random value is less than the cumulative probability, select this day type
  if (randomValue < cumulativeProbability)
  {
    selectedWeather = kvp.Key;
    break;
  }
}


return selectedWeather;

And finally, as in the choice of the DayType, we calculate the possibilities depending on the sum of the total of possibilities of the current dictionary, and we make a foreach going through it. This way we will already have the Weather selected for the desired time.

We can use the Queue in which all the Weather has been saved to see it all for hours, going through it in a for. This way you will see that everything has gone correctly.

For part 2 of this tutorial, we will make a TimeManager so that the WeatherManager can also integrate and advance the weather, as well as display it by UI in a basic way, both the current Weather, as well as the day and time.

If you have any doubt or possible code improvement, you can contact me through the social networks, see you!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *