Creating a Farming RPG (like Harvest Moon) in Unity — Part 6: Managing In-Game Time

Creating a Farming RPG (like Harvest Moon) in Unity — Part 6: Managing In-Game Time

This article is a part of the series:
Creating a Farming RPG (like Harvest Moon) in Unity

Ever wanted to create a game like Harvest Moon in Unity? Check out Part 6 of our guide here, where we go through how to set up a day-night cycle and manage in-game time. You can also find Part 5 of our guide here, where we went through how to create an item equipping system.

1. Creating the in-game time system

Unlike most games, we cannot simply use Unity’s built-in Time as we are looking to store more complex information than that. DateTime might be more helpful, but our game’s dating system does not exactly follow real life. Instead of 12 months, there are 4 seasons in a year, spanning 30 days each. Hence, we will set up our own class to represent how in-game time will pass.

a. Setting up our own time class

For starters, we want to record the following information:

  • Year
  • Season
  • Day
  • Hour
  • Minute

Create a new script called GameTimestamp:

There are 4 seasons in a year: Spring, Summer, Fall, Winter. These seasons will be represented as an enumeration (i.e. enum).

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;
    public int day; 
    public int hour;
    public int minute; 

    // Start is called before the first frame update
    void Start()
    {
        
    }


    // Update is called once per frame
    public void Update()
    {
        
    }


    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }


}

As GameTimestamp is a class that will be instantiated whenever we want to record in-game time, notice that we also create a constructor for it. To read more about constructors, check out this article.

b. Updating the Timestamp by the minute

Each of the variables in the class is affected by one another. Our in-game clock will update in increments of the minute. When that happens, the following conversions must also be made:

  • 60 minutes to 1 hour
  • 24 hours to 1 day
  • 30 days to 1 season
  • 4 seasons to 1 year

To this end, let’s make a function to handle this updating of the clock:

Note: The days start from 1 because there are no ‘Day 0s’ in a calendar month.

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;
    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

}

c. Handling conversions

A lot of the game’s mechanics are affected by time. This is especially necessary for our core farming system. Consequently, we will have to convert to different units to compare between two timestamps often. To make this process easier, we can set up some static functions to do the calculations ahead of time.

Let’s do the straightforward calculations first:

  • 1 hour = 60 minutes
  • 1 day = 24 hours
  • 1 year = 30 days * 4 seasons

Converting seasons to days might be a bit more challenging. After all, we have represented them with enums, so how are the calculations going to be done?

  • At Spring: 0 seasons have passed × 30 days in a season = 0 days
  • At Summer: 1 season has passed × 30 days in a season = 30 days
  • At Fall: 2 seasons have passed × 30 days in a season = 60 days
  • At Winter: 3 seasons have passed × 30 days in a season = 90 days

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;
    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }



}

d. Calculating the days of the week

In the Status Bar of the Player’s HUD, the date includes the season, day, and day of the week.

status bar mockup
The Status Bar

The days of the week will be represented as an enum in GameTimestamp:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
    {
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday,
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }



}

Now comes the tricky part: how do we figure out which day of the week it is depending on the day?

To find out, we look to a calendar in real life.

Do you notice that if you were to divide all the dates under a certain day of the week by 7, it would consistently return the same remainder every time? Likewise, to find the day of the week in our game, we just have to calculate the total number of days and perform a modulo operation (with the % operator) on it and find the corresponding day of the week with it.

Therefore, let us create a function to compute the day of the week in GameTimestamp:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
    {
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    public DayOfTheWeek GetDayOfTheWeek()
    {
        //Convert the total time passed into days
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;

        //Remainder after dividing daysPassed by 7
        int dayIndex =  daysPassed % 7;

        //Cast into Day of the Week
        return (DayOfTheWeek)dayIndex;
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }



}

Based on what we have now, Day 1 will begin on a Monday, as our enum assigns Monday to have an index of 1. You can hence determine what day of the week the game will start in by reordering them such that your desired day ends up with an index of 1.

December 2019 Calendar
I chose December 2019 as an reference in particular because it starts on a Sunday.

At least in the case of this tutorial, we want the game’s calendar to begin on a Sunday as it is the first day of the week. For Sunday to have an index of 1, it has to be preceded by Saturday. Thus, the order of the days in DayOfTheWeek enum should be rearranged.

Note: You can also explicitly assign integer values to each of them in the set (e.g. {Sunday=1, Monday=2, Saturday=0}), but simply reordering them keeps it tidier and less confusing to read.

Make the following changes to GameTimestamp:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
        Saturday,
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    public DayOfTheWeek GetDayOfTheWeek()
    {
        //Convert the total time passed into days
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;

        //Remainder after dividing daysPassed by 7
        int dayIndex =  daysPassed % 7;

        //Cast into Day of the Week
        return (DayOfTheWeek)dayIndex;
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }



}

What we just created is just a representation of the in-game time that can be instantiated as and when we need it. However, we have not set up our actual in-game clock.

e. TimeManager

Let’s create a Manager class to instantiate a GameTimestamp for the game’s official clock.

SerializeField forces Unity to serialize a private field, meaning that it will be displayed in the Inspector even though it is set to private.

The game should start at 6am, Spring 1 Year 0.

Hence, create a new script called TimeManager and add the following:

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [SerializeField]
    GameTimestamp timestamp;

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);

    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Attach the TimeManager script to the Manager GameObject.

adding time manager
The declared timestamp variable is not showing up.

Even though we forced Unity to display the in-game clock in the inspector, it just shows an empty component. This is because we have not indicated that the GameTimestamp class can be serialized.

Unity uses Serialization to convert data within a script for display and editing in the Inspector.

Hence, add the following to GameTimestamp:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]

public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
        Saturday,
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    public DayOfTheWeek GetDayOfTheWeek()
    {
        //Convert the total time passed into days
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;

        //Remainder after dividing daysPassed by 7
        int dayIndex =  daysPassed % 7;

        //Cast into Day of the Week
        return (DayOfTheWeek)dayIndex;
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }



}

The timestamp should now be visible in the Inspector.

Timestamp displayed in the Inspector
The field in the Inspector

We need a function that will update the timestamp clock (add 1 minute to the in-game time) and execute all time-based tasks. Create one in TimeManager:

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [SerializeField]
    GameTimestamp timestamp;

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }
   

    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);

    }

    // Update is called once per frame
    void Update()
    {
        
    }

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();
    }


}

The next step is to figure out how to call this function. Update is not a good place to call it, as we do not want the minutes to update every frame. Instead, we will set up a coroutine for the loop:

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }
   

    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
       
    }
    

    // Update is called once per frame
    void Update()
    {
        
    }

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();
    }


}

The speed of the in-game time can be controlled with timeScale. A higher number means the minutes will go by faster, and a lower number means it will go by slower.

2. Creating the day-night cycle

With a working in-game clock in place, we can now change up the lighting to reflect the time of day. This can be controlled by altering the angle of the directional light. This serves as our game’s ‘sun’.

Add a reference to the directional light in TimeManager:

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    //The transform of the directional light (sun)
    public Transform sunTransform;



    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }
   

    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
       
    }
    

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();
    }


}

Assign the Directional Light in the scene to the newly-declared variable.

assigning directional light to the sun transform variable

a. Calculating the angle

movement of the sun
The sun makes one full rotation in a day

We know the sun moves 360° in a day, hence:

  1. 360° ÷ 24 hours = 15° per hour
  2. 15° ÷ 60 minutes = 0.25° per minute

The Gradient: Every time the Tick() function is called, the directional light should move by 0.25°.

The y-intercept: Based on the diagram above, we know the sun will have an angle of -90° at midnight (0:00).

Therefore, the formula to calculate the angle of our directional light will be:

Angle of the Sun = 0.25° × Time in Minutes – 90°

b. Implementing the movement

Before we find the angle of the sun, we must first find the time in minutes. This is where the conversion functions defined in (1c) come in. Simply convert the hours in the timestamp to minutes with GameTimestamp.HoursToMinutes and add the timestamp’s minutes to get the day’s time in minutes.

In TimeManager, create a function to handle the day-night cycle and call it in Tick():

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    //The transform of the directional light (sun)
    public Transform sunTransform;



    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if(Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this; 
        }
    }
   

    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());
    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
       
    }
    

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();

        UpdateSunMovement(); 
    }

    //Day and night cycle
    void UpdateSunMovement()
    {
        //Convert the current time to minutes
        int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;

        //Sun moves 15 degrees in an hour
        //.25 degrees in a minute
        //At midnight (0:00), the angle of the sun should be -90
        float sunAngle = .25f * timeInMinutes - 90;

        //Apply the angle to the directional light
        sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
    }

}

c. Debugging

Playtesting the game should look something like this:

dawn with daynight cycle
What it should look like at 6 A.M.

Since the sun’s angle moves in small increments, you might have to wait a long time before you can observe the full Day/Night cycle. You could always check to see if the sun is positioned correctly by manually changing up the timestamp values in the inspector, but it’s harder to see the movement that way.

Let’s make it so that time is sped up when the player holds a certain key (let’s make it the ] for now). Add the following to PlayerController:

PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    //Movement Components
    private CharacterController controller; 
    private Animator animator;

    private float moveSpeed = 4f;

    [Header("Movement System")]
    public float walkSpeed = 4f;
    public float runSpeed = 8f;


    //Interaction components
    PlayerInteraction playerInteraction; 

    // Start is called before the first frame update
    void Start()
    {
        //Get movement components
        controller = GetComponent<CharacterController>();
        animator = GetComponent<Animator>();

        //Get interaction component
        playerInteraction = GetComponentInChildren<PlayerInteraction>(); 

    }

    // Update is called once per frame
    void Update()
    {
        //Runs the function that handles all movement
        Move();

        //Runs the function that handles all interaction
        Interact();


        //Debugging purposes only
        //Skip the time when the right square bracket is pressed
        if (Input.GetKey(KeyCode.RightBracket))
        {
            TimeManager.Instance.Tick();
        }
    }

    public void Interact()
    {
        //Tool interaction
        if (Input.GetButtonDown("Fire1"))
        {
            //Interact
            playerInteraction.Interact(); 
        }

        //TODO: Set up item interaction
    }

    
    public void Move()
    {
        //Get the horizontal and vertical inputs as a number
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        //Direction in a normalised vector
        Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
        Vector3 velocity = moveSpeed * Time.deltaTime * dir;

        //Is the sprint key pressed down?
        if (Input.GetButton("Sprint"))
        {
            //Set the animation to run and increase our movespeed
            moveSpeed = runSpeed;
            animator.SetBool("Running", true);
        } else
        {
            //Set the animation to walk and decrease our movespeed
            moveSpeed = walkSpeed;
            animator.SetBool("Running", false);
        }


        //Check if there is movement
        if (dir.magnitude >= 0.1f)
        {
            //Look towards that direction
            transform.rotation = Quaternion.LookRotation(dir);

            //Move
            controller.Move(velocity); 
            
        }

        //Animation speed parameter
        animator.SetFloat("Speed", velocity.magnitude); 



    }
}

At this point when you play test, you should be able to speed up time for various tests by holding ].

Note: Remember to disable that block of code in future when releasing the game build, so that your players won’t be able to speed time up.

skipping time
It gets pitch black after the Sun sets.

3. Displaying the time on the HUD

We are almost done with the Time Management system in itself. Now to get UIManager to display the information in the HUD. In making our inventory system, we did the following:

  1. Create a function in UIManager to render the inventory
  2. Call this function from InventoryManager whenever an update to the system happens

This is all well and good as UIManager is a singleton, and serves as the Manager class to inform all affected objects of inventory state changes. However, this will not work in the context of our time system.

This is because many objects, such as the Land and Crops the players will plant in our game are going to be reliant on time changes. The number of objects reliant on time will always be changing throughout the course of the game. If we were to do the same thing as we did with the inventory, we would have to do a FindObjectsOfType() every time we want to get a list of the dependent objects. This will slow down the game a lot as the number of objects increase.

a. Using the Observer Design Pattern

A better approach to this would be using the Observer Design Pattern, a subscription-based system in which:

  1. A blueprint (interface) for observers is defined for objects interested in observing the subject
  2. The objects that will take action on state changes (in this case changes to the time) ‘registers’ as an observer to the subject (TimeManager). In other words, it ‘subscribes’ to the subject to start receiving notifications from it.
  3. The observable subject (TimeManager), automatically notifies all observers through the interface when a change occurs.

This makes the system more flexible as it minimizes interdependency between the subject and the observers. The only thing the subject needs to care about is for its observer to implement the interface. This is what we want to do:

observer pattern diagram
Observer Pattern for our time system

First, let’s set up the blueprint for the Observers. Create a new script called ITimeTracker:

ITimeTracker.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public classinterface ITimeTracker : MonoBehaviour 
{
    void ClockUpdate(GameTimestamp timestamp);
    // Start is called before the first frame update
    void Start()
    {
        
    }


    // Update is called once per frame
    public void Update()
    {
        
    }
}

In TimeManager, declare a list of observers (listeners) and set up functions to register and unregister them:

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [Header("Internal Clock")]
    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    [Header ("Day and Night cycle")]
    //The transform of the directional light (sun)
    public Transform sunTransform;

    //List of Objects to inform of changes to the time
    List<ITimeTracker> listeners = new List<ITimeTracker>();

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }
    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());

    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
       
    }
    

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();

        //Inform each of the listeners of the new time state 
        foreach(ITimeTracker listener in listeners)
        {
            listener.ClockUpdate(timestamp);
        }

        UpdateSunMovement(); 
    }

    //Day and night cycle
    void UpdateSunMovement()
    {
        //Convert the current time to minutes
        int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;

        //Sun moves 15 degrees in an hour
        //.25 degrees in a minute
        //At midnight (0:00), the angle of the sun should be -90
        float sunAngle = .25f * timeInMinutes - 90;

        //Apply the angle to the directional light
        sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
    }

    //Handling Listeners

    //Add the object to the list of listeners
    public void RegisterTracker(ITimeTracker listener)
    {
        listeners.Add(listener);
    }

    //Remove the object from the list of listeners
    public void UnregisterTracker(ITimeTracker listener)
    {
        listeners.Remove(listener);
    }
}

Implement the ITimeTracker interface on the UIManager, and register it as a listener / observer:

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }
    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
        AssignSlotIndexes();

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this);
    }

    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.equippedTool);
        itemHandSlot.Display(InventoryManager.Instance.equippedItem);

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.equippedTool;

        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }

    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        

    }
}

With the ClockUpdate() function implemented, we can finally start rendering the time in the HUD.

b. Displaying the Time

In UIManager, reference the Time UI components from the status bar:

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }
    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Time UI
    public Text timeText;
    public Text dateText; 


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
        AssignSlotIndexes();

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this); 
    }

    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.equippedTool);
        itemHandSlot.Display(InventoryManager.Instance.equippedItem);

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.equippedTool;

        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }

    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        
    }
}

Assign the Text components from the Status Bar accordingly:

We want the time to be displayed as a 12-hour clock in the status bar. However, the GameTimestamp saves the hours in terms of 24 hours. Hence, we have to convert it over in UIManager:

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }
    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Time UI
    public Text timeText;
    public Text dateText; 


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
        AssignSlotIndexes();

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this); 
    }

    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.equippedTool);
        itemHandSlot.Display(InventoryManager.Instance.equippedItem);

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.equippedTool;

        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }

    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Handle the time
        //Get the hours and minutes
        int hours = timestamp.hour;
        int minutes = timestamp.minute; 

        //AM or PM
        string prefix = "AM ";
        
        //Convert hours to 12 hour clock
        if (hours > 12)
        {
            //Time becomes PM 
            prefix = "PM ";
            hours = hours - 12;
            Debug.Log(hours);
        }


        timeText.text = prefix + hours + minutes;


    }
}

The status bar should crudely display the time in 12-hour format with the minutes value right beside:

Time without the proper formatting

We want it to show it in the format of H:MM, where the minutes are displayed as double digits. We can easily do this with the ToString() function:

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }
    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Time UI
    public Text timeText;
    public Text dateText; 


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
        AssignSlotIndexes();

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this); 
    }

    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.equippedTool);
        itemHandSlot.Display(InventoryManager.Instance.equippedItem);

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.equippedTool;

        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }

    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Handle the time
        //Get the hours and minutes
        int hours = timestamp.hour;
        int minutes = timestamp.minute; 

        //AM or PM
        string prefix = "AM ";
        
        //Convert hours to 12 hour clock
        if (hours > 12)
        {
            //Time becomes PM 
            prefix = "PM ";
            hours = hours - 12;
            Debug.Log(hours);
        }

        //Format it for the time text display
        timeText.text = prefix + hours + ":" + minutes.ToString("00");


    }
}

When the game is played now, everything should work fine until you fast forward the time to a certain point:

double digits cutoff
When the hour reaches 2 digits, the time disappears.

This is because the text is too long to fit within its space. Make the size adjustments accordingly in the scene:

shrink time text size
In addition to the font size, we changed the placeholder text to have 2 digits to see how it would look like.

c. Displaying the Date

The date is to be displayed in the following format: Season Day (Day of the Week).

Get the values of each and display them according to the format:

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }
    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Time UI
    public Text timeText;
    public Text dateText; 


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
        AssignSlotIndexes();

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this); 
    }

    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.equippedTool);
        itemHandSlot.Display(InventoryManager.Instance.equippedItem);

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.equippedTool;

        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }

    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Handle the time
        //Get the hours and minutes
        int hours = timestamp.hour;
        int minutes = timestamp.minute; 

        //AM or PM
        string prefix = "AM ";
        
        //Convert hours to 12 hour clock
        if (hours > 12)
        {
            //Time becomes PM 
            prefix = "PM ";
            hours = hours - 12;
            Debug.Log(hours);
        }

        //Format it for the time text display
        timeText.text = prefix + hours + ":" + minutes.ToString("00");

        //Handle the Date
        int day = timestamp.day;
        string season = timestamp.season.ToString();
        string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();

        //Format it for the date text display
        dateText.text = season + " " + day + " (" + dayOfTheWeek +")";

    }
}

When you speed up time, it should look like this:

skipping time with ui feedback
Date and Time working together

4. Implementing a soil-drying system

As of now, when Farmland is Watered, it remains that way indefinitely. We want it to dry up every 24 hours so the player has to water it again. The logic to accomplish this is as follows:

  1. When the Land changes its state to Watered it saves a timestamp of when it was last watered
  2. At every Tick() from TimeManager, the current time is compared with the saved timestamp to find out how much time has passed since then.
  3. If more than 24 hours have passed, the Land should change its state back to Farmland.

a. Saving timestamps

If you noticed, we kept TimeManager's timestamp private in 1e. This is because retrieving the timestamp directly from TimeManager would just pass it as a reference instead of a value. This means that if Land saved the timestamp as a variable at 6am, and when it checks the value at 12pm, the variable would return 12pm.

User-defined objects such as GameTimestamp are passed by reference. We wrote an article explaining the difference between value and reference types in-depth.

To get around this, we should create a get function to return a copy of the game’s clock rather than a reference to it. To this end, we will overload GameTimestamp's constructor that makes a copy of an existing instance:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
    {
        Saturday,
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Creating a new instance of a GameTimestamp from another pre-existing one
    public GameTimestamp(GameTimestamp timestamp)
    {
        this.year = timestamp.year;
        this.season = timestamp.season;
        this.day = timestamp.day;
        this.hour = timestamp.hour;
        this.minute = timestamp.minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    public DayOfTheWeek GetDayOfTheWeek()
    {
        //Convert the total time passed into days
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;

        //Remainder after dividing daysPassed by 7
        int dayIndex =  daysPassed % 7;

        //Cast into Day of the Week
        return (DayOfTheWeek)dayIndex;
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }

}

With this, create a function in TimeManager that returns the copied instance of the timestamp:

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [Header("Internal Clock")]
    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    [Header ("Day and Night cycle")]
    //The transform of the directional light (sun)
    public Transform sunTransform;

    //List of Objects to inform of changes to the time
    List<ITimeTracker> listeners = new List<ITimeTracker>();

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }
    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());

    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
       
    }
    

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();

        //Inform each of the listeners of the new time state 
        foreach(ITimeTracker listener in listeners)
        {
            listener.ClockUpdate(timestamp);
        }

        UpdateSunMovement(); 
    }

    //Day and night cycle
    void UpdateSunMovement()
    {
        //Convert the current time to minutes
        int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;

        //Sun moves 15 degrees in an hour
        //.25 degrees in a minute
        //At midnight (0:00), the angle of the sun should be -90
        float sunAngle = .25f * timeInMinutes - 90;

        //Apply the angle to the directional light
        sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
    }

    //Get the timestamp
    public GameTimestamp GetGameTimestamp()
    {
        //Return a cloned instance
        return new GameTimestamp(timestamp);
    }


    //Handling Listeners

    //Add the object to the list of listeners
    public void RegisterTracker(ITimeTracker listener)
    {
        listeners.Add(listener);
    }

    //Remove the object from the list of listeners
    public void UnregisterTracker(ITimeTracker listener)
    {
        listeners.Remove(listener);
    }
}

In Land, declare a new variable to keep track of when the Land was last Watered and cache the timestamp at its state change:

Land.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Land : MonoBehaviour
{
    public enum LandStatus
    {
        Soil, Farmland, Watered
    }

    public LandStatus landStatus;

    public Material soilMat, farmlandMat, wateredMat;
    new Renderer renderer;

    //The selection gameobject to enable when the player is selecting the land
    public GameObject select;

    //Cache the time the land was watered 
    GameTimestamp timeWatered; 

    // Start is called before the first frame update
    void Start()
    {
        //Get the renderer component
        renderer = GetComponent<Renderer>();

        //Set the land to soil by default
        SwitchLandStatus(LandStatus.Soil);

        //Deselect the land by default
        Select(false);

    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;

        Material materialToSwitch = soilMat; 

        //Decide what material to switch to
        switch (statusToSwitch)
        {
            case LandStatus.Soil:
                //Switch to the soil material
                materialToSwitch = soilMat;
                break;
            case LandStatus.Farmland:
                //Switch to farmland material 
                materialToSwitch = farmlandMat;
                break;

            case LandStatus.Watered:
                //Switch to watered material
                materialToSwitch = wateredMat;

                //Cache the time it was watered
                timeWatered = TimeManager.Instance.GetGameTimestamp(); 
                break; 

        }

        //Get the renderer to apply the changes
        renderer.material = materialToSwitch; 
    }

    public void Select(bool toggle)
    {
        select.SetActive(toggle);
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Check the player's tool slot
        ItemData toolSlot = InventoryManager.Instance.equippedTool;

        //Try casting the itemdata in the toolslot as EquipmentData
        EquipmentData equipmentTool = toolSlot as EquipmentData; 

        //Check if it is of type EquipmentData 
        if(equipmentTool != null)
        {
            //Get the tool type
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe:
                    SwitchLandStatus(LandStatus.Farmland);
                    break;
                case EquipmentData.ToolType.WateringCan:
                    SwitchLandStatus(LandStatus.Watered);
                    break;
            }


        }
    }

   
}

b. Checking how much time has passed

The Land script should check how much time has elapsed at every update of the clock. Hence, it should register as an observer of TimeManager:

Land.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Land : MonoBehaviour, ITimeTracker
{
    public enum LandStatus
    {
        Soil, Farmland, Watered
    }

    public LandStatus landStatus;

    public Material soilMat, farmlandMat, wateredMat;
    new Renderer renderer;

    //The selection gameobject to enable when the player is selecting the land
    public GameObject select;

    //Cache the time the land was watered 
    GameTimestamp timeWatered; 

    // Start is called before the first frame update
    void Start()
    {
        //Get the renderer component
        renderer = GetComponent<Renderer>();

        //Set the land to soil by default
        SwitchLandStatus(LandStatus.Soil);

        //Deselect the land by default
        Select(false);

        //Add this to TimeManager's Listener list
        TimeManager.Instance.RegisterTracker(this);
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;

        Material materialToSwitch = soilMat; 

        //Decide what material to switch to
        switch (statusToSwitch)
        {
            case LandStatus.Soil:
                //Switch to the soil material
                materialToSwitch = soilMat;
                break;
            case LandStatus.Farmland:
                //Switch to farmland material 
                materialToSwitch = farmlandMat;
                break;

            case LandStatus.Watered:
                //Switch to watered material
                materialToSwitch = wateredMat;

                //Cache the time it was watered
                timeWatered = TimeManager.Instance.GetGameTimestamp(); 
                break; 

        }

        //Get the renderer to apply the changes
        renderer.material = materialToSwitch; 
    }

    public void Select(bool toggle)
    {
        select.SetActive(toggle);
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Check the player's tool slot
        ItemData toolSlot = InventoryManager.Instance.equippedTool;

        //Try casting the itemdata in the toolslot as EquipmentData
        EquipmentData equipmentTool = toolSlot as EquipmentData; 

        //Check if it is of type EquipmentData 
        if(equipmentTool != null)
        {
            //Get the tool type
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe:
                    SwitchLandStatus(LandStatus.Farmland);
                    break;
                case EquipmentData.ToolType.WateringCan:
                    SwitchLandStatus(LandStatus.Watered);
                    break;
            }


        }
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        
    }
}

Next, we need a way to calculate the difference between timestamps. This can be done by converting each of the timestamps into hours and subtracting them from one another.

We can do the conversions using GameTimestamps functions like this:

  • Years: Convert to days with YearsToDays and convert the result with DaysToHours
  • Season: Convert to days with SeasonsToDays and convert the result with DaysToHours
  • Day: Convert to hours with DaysToHours

Add all of them together with the hours and you get the total hours elapsed in the timestamp. Once this is done for both timestamps, subtract them from one another and ensure it returns a positive value with Mathf.Abs(). Let’s make a static function to handle this calculation in GameTimestamp:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
    {
        Saturday,
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Creating a new instance of a GameTimestamp from another pre-existing one
    public GameTimestamp(GameTimestamp timestamp)
    {
        this.year = timestamp.year;
        this.season = timestamp.season;
        this.day = timestamp.day;
        this.hour = timestamp.hour;
        this.minute = timestamp.minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    public DayOfTheWeek GetDayOfTheWeek()
    {
        //Convert the total time passed into days
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;

        //Remainder after dividing daysPassed by 7
        int dayIndex =  daysPassed % 7;

        //Cast into Day of the Week
        return (DayOfTheWeek)dayIndex;
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }

    // Calculate the difference between 2 timestamps in hours
    public static int CompareTimestamps(GameTimestamp timestamp1, GameTimestamp timestamp2)
    {
        //Convert timestamps to hours
        int timestamp1Hours = DaysToHours(YearsToDays(timestamp1.year)) + DaysToHours(SeasonsToDays(timestamp1.season)) + DaysToHours(timestamp1.day) + timestamp1.hour;
        int timestamp2Hours = DaysToHours(YearsToDays(timestamp2.year)) + DaysToHours(SeasonsToDays(timestamp2.season)) + DaysToHours(timestamp2.day) + timestamp2.hour;
        int difference = timestamp2Hours - timestamp1Hours;
        return Mathf.Abs(difference);
    }
}

With this function, we can now check for how many hours have passed under ClockUpdate. Add the following to Land:

Land.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Land : MonoBehaviour, ITimeTracker
{
    public enum LandStatus
    {
        Soil, Farmland, Watered
    }

    public LandStatus landStatus;

    public Material soilMat, farmlandMat, wateredMat;
    new Renderer renderer;

    //The selection gameobject to enable when the player is selecting the land
    public GameObject select;

    //Cache the time the land was watered 
    GameTimestamp timeWatered; 

    // Start is called before the first frame update
    void Start()
    {
        //Get the renderer component
        renderer = GetComponent<Renderer>();

        //Set the land to soil by default
        SwitchLandStatus(LandStatus.Soil);

        //Deselect the land by default
        Select(false);

        //Add this to TimeManager's Listener list
        TimeManager.Instance.RegisterTracker(this);
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;

        Material materialToSwitch = soilMat; 

        //Decide what material to switch to
        switch (statusToSwitch)
        {
            case LandStatus.Soil:
                //Switch to the soil material
                materialToSwitch = soilMat;
                break;
            case LandStatus.Farmland:
                //Switch to farmland material 
                materialToSwitch = farmlandMat;
                break;

            case LandStatus.Watered:
                //Switch to watered material
                materialToSwitch = wateredMat;

                //Cache the time it was watered
                timeWatered = TimeManager.Instance.GetGameTimestamp(); 
                break; 

        }

        //Get the renderer to apply the changes
        renderer.material = materialToSwitch; 
    }

    public void Select(bool toggle)
    {
        select.SetActive(toggle);
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Check the player's tool slot
        ItemData toolSlot = InventoryManager.Instance.equippedTool;

        //Try casting the itemdata in the toolslot as EquipmentData
        EquipmentData equipmentTool = toolSlot as EquipmentData; 

        //Check if it is of type EquipmentData 
        if(equipmentTool != null)
        {
            //Get the tool type
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe:
                    SwitchLandStatus(LandStatus.Farmland);
                    break;
                case EquipmentData.ToolType.WateringCan:
                    SwitchLandStatus(LandStatus.Watered);
                    break;
            }


        }
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Checked if 24 hours has passed since last watered
        if(landStatus == LandStatus.Watered)
        {
            //Hours since the land was watered
            int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
            Debug.Log(hoursElapsed + " hours since this was watered");

            if(hoursElapsed > 24)
            {
                //Dry up (Switch back to farmland)
                SwitchLandStatus(LandStatus.Farmland);
            }
        }
    }
}

Conclusion

As usual, to wrap up, below are the final versions of the script:

GameTimestamp.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class GameTimestamp 
{
    public int year; 
    public enum Season
    {
        Spring,
        Summer,
        Fall,
        Winter
    }
    public Season season;

    public enum DayOfTheWeek
    {
        Saturday,
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday
        
    }

    public int day; 
    public int hour;
    public int minute; 

    //Constructor to set up the class
    public GameTimestamp(int year, Season season, int day, int hour, int minute)
    {
        this.year = year;
        this.season = season;
        this.day = day;
        this.hour = hour;
        this.minute = minute;
    }

    //Creating a new instance of a GameTimestamp from another pre-existing one
    public GameTimestamp(GameTimestamp timestamp)
    {
        this.year = timestamp.year;
        this.season = timestamp.season;
        this.day = timestamp.day;
        this.hour = timestamp.hour;
        this.minute = timestamp.minute;
    }

    //Makes an increment of the time by 1 minute
    public void UpdateClock()
    {
        minute++; 

        //60 minutes in 1 hour
        if(minute >= 60)
        {
            //reset minutes
            minute = 0;
            hour++;
        }

        //24 hours in 1 day
        if(hour >= 24)
        {
            //Reset hours 
            hour = 0;

            day++; 
        }

        //30 days in a season
        if(day > 30)
        {
            //Reset days
            day = 1;

            //If at the final season, reset and change to spring
            if(season == Season.Winter)
            {
                season = Season.Spring;
                //Start of a new year
                year++; 
            } else
            {

                season++;
            }

            
            
        }
    }

    public DayOfTheWeek GetDayOfTheWeek()
    {
        //Convert the total time passed into days
        int daysPassed = YearsToDays(year) + SeasonsToDays(season) + day;

        //Remainder after dividing daysPassed by 7
        int dayIndex =  daysPassed % 7;

        //Cast into Day of the Week
        return (DayOfTheWeek)dayIndex;
    }

    //Convert hours to minutes
    public static int HoursToMinutes(int hour)
    {
        //60 minutes = 1 hour
        return hour * 60; 
    }

    //Convert Days to Hours
    public static int DaysToHours(int days)
    {
        //24 Hours in a day
        return days * 24; 
    }

    //Convert Seasons to days
    public static int SeasonsToDays(Season season)
    {
        int seasonIndex = (int)season;
        return seasonIndex * 30; 
    }

    //Years to Days
    public static int YearsToDays(int years)
    {
        return years * 4 * 30; 
    }

    // Calculate the difference between 2 timestamps in hours
    public static int CompareTimestamps(GameTimestamp timestamp1, GameTimestamp timestamp2)
    {
        //Convert timestamps to hours
        int timestamp1Hours = DaysToHours(YearsToDays(timestamp1.year)) + DaysToHours(SeasonsToDays(timestamp1.season)) + DaysToHours(timestamp1.day) + timestamp1.hour;
        int timestamp2Hours = DaysToHours(YearsToDays(timestamp2.year)) + DaysToHours(SeasonsToDays(timestamp2.season)) + DaysToHours(timestamp2.day) + timestamp2.hour;
        int difference = timestamp2Hours - timestamp1Hours;
        return Mathf.Abs(difference);
    }
}

TimeManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TimeManager : MonoBehaviour
{
    public static TimeManager Instance { get; private set; }

    [Header("Internal Clock")]
    [SerializeField]
    GameTimestamp timestamp;
    public float timeScale = 1.0f;

    [Header ("Day and Night cycle")]
    //The transform of the directional light (sun)
    public Transform sunTransform;

    //List of Objects to inform of changes to the time
    List<ITimeTracker> listeners = new List<ITimeTracker>();

    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }
    // Start is called before the first frame update
    void Start()
    {
        //Initialise the time stamp
        timestamp = new GameTimestamp(0, GameTimestamp.Season.Spring, 1, 6, 0);
        StartCoroutine(TimeUpdate());

    }

    IEnumerator TimeUpdate()
    {
        while (true)
        {
            Tick();
            yield return new WaitForSeconds(1 / timeScale);
        }
       
    }
    

    //A tick of the in-game time
    public void Tick()
    {
        timestamp.UpdateClock();

        //Inform each of the listeners of the new time state 
        foreach(ITimeTracker listener in listeners)
        {
            listener.ClockUpdate(timestamp);
        }

        UpdateSunMovement(); 
    }

    //Day and night cycle
    void UpdateSunMovement()
    {
        //Convert the current time to minutes
        int timeInMinutes = GameTimestamp.HoursToMinutes(timestamp.hour) + timestamp.minute;

        //Sun moves 15 degrees in an hour
        //.25 degrees in a minute
        //At midnight (0:00), the angle of the sun should be -90
        float sunAngle = .25f * timeInMinutes - 90;

        //Apply the angle to the directional light
        sunTransform.eulerAngles = new Vector3(sunAngle, 0, 0);
    }

    //Get the timestamp
    public GameTimestamp GetGameTimestamp()
    {
        //Return a cloned instance
        return new GameTimestamp(timestamp);
    }


    //Handling Listeners

    //Add the object to the list of listeners
    public void RegisterTracker(ITimeTracker listener)
    {
        listeners.Add(listener);
    }

    //Remove the object from the list of listeners
    public void UnregisterTracker(ITimeTracker listener)
    {
        listeners.Remove(listener);
    }
}

PlayerController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    //Movement Components
    private CharacterController controller; 
    private Animator animator;

    private float moveSpeed = 4f;

    [Header("Movement System")]
    public float walkSpeed = 4f;
    public float runSpeed = 8f;


    //Interaction components
    PlayerInteraction playerInteraction; 

    // Start is called before the first frame update
    void Start()
    {
        //Get movement components
        controller = GetComponent<CharacterController>();
        animator = GetComponent<Animator>();

        //Get interaction component
        playerInteraction = GetComponentInChildren<PlayerInteraction>(); 

    }

    // Update is called once per frame
    void Update()
    {
        //Runs the function that handles all movement
        Move();

        //Runs the function that handles all interaction
        Interact();


        //Debugging purposes only
        //Skip the time when the right square bracket is pressed
        if (Input.GetKey(KeyCode.RightBracket))
        {
            TimeManager.Instance.Tick();
        }
    }

    public void Interact()
    {
        //Tool interaction
        if (Input.GetButtonDown("Fire1"))
        {
            //Interact
            playerInteraction.Interact(); 
        }

        //TODO: Set up item interaction
    }

    
    public void Move()
    {
        //Get the horizontal and vertical inputs as a number
        float horizontal = Input.GetAxisRaw("Horizontal");
        float vertical = Input.GetAxisRaw("Vertical");

        //Direction in a normalised vector
        Vector3 dir = new Vector3(horizontal, 0f, vertical).normalized;
        Vector3 velocity = moveSpeed * Time.deltaTime * dir;

        //Is the sprint key pressed down?
        if (Input.GetButton("Sprint"))
        {
            //Set the animation to run and increase our movespeed
            moveSpeed = runSpeed;
            animator.SetBool("Running", true);
        } else
        {
            //Set the animation to walk and decrease our movespeed
            moveSpeed = walkSpeed;
            animator.SetBool("Running", false);
        }


        //Check if there is movement
        if (dir.magnitude >= 0.1f)
        {
            //Look towards that direction
            transform.rotation = Quaternion.LookRotation(dir);

            //Move
            controller.Move(velocity); 
            
        }

        //Animation speed parameter
        animator.SetFloat("Speed", velocity.magnitude); 



    }
}

ITimeTracker.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ITimeTracker 
{
    void ClockUpdate(GameTimestamp timestamp);
}

UIManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; 

public class UIManager : MonoBehaviour, ITimeTracker
{
    public static UIManager Instance { get; private set; }
    [Header("Status Bar")]
    //Tool equip slot on the status bar
    public Image toolEquipSlot;
    //Time UI
    public Text timeText;
    public Text dateText; 


    [Header("Inventory System")]
    //The inventory panel
    public GameObject inventoryPanel;

    //The tool equip slot UI on the Inventory panel
    public HandInventorySlot toolHandSlot; 

    //The tool slot UIs
    public InventorySlot[] toolSlots;

    //The item equip slot UI on the Inventory panel
    public HandInventorySlot itemHandSlot;

    //The item slot UIs
    public InventorySlot[] itemSlots;

    //Item info box
    public Text itemNameText;
    public Text itemDescriptionText; 


    private void Awake()
    {
        //If there is more than one instance, destroy the extra
        if (Instance != null && Instance != this)
        {
            Destroy(this);
        }
        else
        {
            //Set the static instance to this instance
            Instance = this;
        }
    }

    private void Start()
    {
        RenderInventory();
        AssignSlotIndexes();

        //Add UIManager to the list of objects TimeManager will notify when the time updates
        TimeManager.Instance.RegisterTracker(this); 
    }

    //Iterate through the slot UI elements and assign it its reference slot index
    public void AssignSlotIndexes()
    {
        for (int i =0; i<toolSlots.Length; i++)
        {
            toolSlots[i].AssignIndex(i);
            itemSlots[i].AssignIndex(i);
        }
    }

    //Render the inventory screen to reflect the Player's Inventory. 
    public void RenderInventory()
    {
        //Get the inventory tool slots from Inventory Manager
        ItemData[] inventoryToolSlots = InventoryManager.Instance.tools;

        //Get the inventory item slots from Inventory Manager
        ItemData[] inventoryItemSlots = InventoryManager.Instance.items;

        //Render the Tool section
        RenderInventoryPanel(inventoryToolSlots, toolSlots);

        //Render the Item section
        RenderInventoryPanel(inventoryItemSlots, itemSlots);

        //Render the equipped slots
        toolHandSlot.Display(InventoryManager.Instance.equippedTool);
        itemHandSlot.Display(InventoryManager.Instance.equippedItem);

        //Get Tool Equip from InventoryManager
        ItemData equippedTool = InventoryManager.Instance.equippedTool;

        //Check if there is an item to display
        if (equippedTool != null)
        {
            //Switch the thumbnail over
            toolEquipSlot.sprite = equippedTool.thumbnail;

            toolEquipSlot.gameObject.SetActive(true);

            return;
        }

        toolEquipSlot.gameObject.SetActive(false);
    }

    //Iterate through a slot in a section and display them in the UI
    void RenderInventoryPanel(ItemData[] slots, InventorySlot[] uiSlots)
    {
        for (int i = 0; i < uiSlots.Length; i++)
        {
            //Display them accordingly
            uiSlots[i].Display(slots[i]);
        }
    }

    public void ToggleInventoryPanel()
    {
        //If the panel is hidden, show it and vice versa
        inventoryPanel.SetActive(!inventoryPanel.activeSelf);

        RenderInventory();
    }

    //Display Item info on the Item infobox
    public void DisplayItemInfo(ItemData data)
    {
        //If data is null, reset
        if(data == null)
        {
            itemNameText.text = "";
            itemDescriptionText.text = "";

            return;
        }

        itemNameText.text = data.name;
        itemDescriptionText.text = data.description; 
    }

    //Callback to handle the UI for time
    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Handle the time
        //Get the hours and minutes
        int hours = timestamp.hour;
        int minutes = timestamp.minute; 

        //AM or PM
        string prefix = "AM ";
        
        //Convert hours to 12 hour clock
        if (hours > 12)
        {
            //Time becomes PM 
            prefix = "PM ";
            hours = hours - 12;
            Debug.Log(hours);
        }

        //Format it for the time text display
        timeText.text = prefix + hours + ":" + minutes.ToString("00");

        //Handle the Date
        int day = timestamp.day;
        string season = timestamp.season.ToString();
        string dayOfTheWeek = timestamp.GetDayOfTheWeek().ToString();

        //Format it for the date text display
        dateText.text = season + " " + day + " (" + dayOfTheWeek +")";

    }
}

Land.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Land : MonoBehaviour, ITimeTracker
{
    public enum LandStatus
    {
        Soil, Farmland, Watered
    }

    public LandStatus landStatus;

    public Material soilMat, farmlandMat, wateredMat;
    new Renderer renderer;

    //The selection gameobject to enable when the player is selecting the land
    public GameObject select;

    //Cache the time the land was watered 
    GameTimestamp timeWatered; 

    // Start is called before the first frame update
    void Start()
    {
        //Get the renderer component
        renderer = GetComponent<Renderer>();

        //Set the land to soil by default
        SwitchLandStatus(LandStatus.Soil);

        //Deselect the land by default
        Select(false);

        //Add this to TimeManager's Listener list
        TimeManager.Instance.RegisterTracker(this);
    }

    public void SwitchLandStatus(LandStatus statusToSwitch)
    {
        //Set land status accordingly
        landStatus = statusToSwitch;

        Material materialToSwitch = soilMat; 

        //Decide what material to switch to
        switch (statusToSwitch)
        {
            case LandStatus.Soil:
                //Switch to the soil material
                materialToSwitch = soilMat;
                break;
            case LandStatus.Farmland:
                //Switch to farmland material 
                materialToSwitch = farmlandMat;
                break;

            case LandStatus.Watered:
                //Switch to watered material
                materialToSwitch = wateredMat;

                //Cache the time it was watered
                timeWatered = TimeManager.Instance.GetGameTimestamp(); 
                break; 

        }

        //Get the renderer to apply the changes
        renderer.material = materialToSwitch; 
    }

    public void Select(bool toggle)
    {
        select.SetActive(toggle);
    }

    //When the player presses the interact button while selecting this land
    public void Interact()
    {
        //Check the player's tool slot
        ItemData toolSlot = InventoryManager.Instance.equippedTool;

        //Try casting the itemdata in the toolslot as EquipmentData
        EquipmentData equipmentTool = toolSlot as EquipmentData; 

        //Check if it is of type EquipmentData 
        if(equipmentTool != null)
        {
            //Get the tool type
            EquipmentData.ToolType toolType = equipmentTool.toolType;

            switch (toolType)
            {
                case EquipmentData.ToolType.Hoe:
                    SwitchLandStatus(LandStatus.Farmland);
                    break;
                case EquipmentData.ToolType.WateringCan:
                    SwitchLandStatus(LandStatus.Watered);
                    break;
            }


        }
    }

    public void ClockUpdate(GameTimestamp timestamp)
    {
        //Checked if 24 hours has passed since last watered
        if(landStatus == LandStatus.Watered)
        {
            //Hours since the land was watered
            int hoursElapsed = GameTimestamp.CompareTimestamps(timeWatered, timestamp);
            Debug.Log(hoursElapsed + " hours since this was watered");

            if(hoursElapsed > 24)
            {
                //Dry up (Switch back to farmland)
                SwitchLandStatus(LandStatus.Farmland);
            }
        }
    }
}

There are 6 comments:

  1. Hi,
    I am an avid watcher of this series, It is awesome! I have added the “//Add UIManager to the list of objects TimeManager will notify when the time updates
    TimeManager.Instance.RegisterTracker(this);” code to the Start function in UIManager. However the MonoBehaviour error still exists, saying that you cannot use the “new” keyword. Is there a fix for this? Thanks, Rob

    1. Hi Robert, I’m not sure if you got notified of my last reply. I had some time to take a closer look at what your error might have been, and I suspect that it might be that you forgot to remove the : MonoBehaviour portion in your GameTimestamp class.

      This is because GameTimestamp is initialised with the new keyword in the code, but MonoBehaviour objects cannot be initialised like that.

  2. As a heads up, you forgot to include adding:

    //Add UIManager to the list of objects TimeManager will notify when the time updates
    TimeManager.Instance.RegisterTracker(this);

    to the private void Start() of the UIManager in the transcript of this tutorial. I spent forever trying to figure out why my time wasn’t updating on the UI until I went to check the video version of the tutorial where it is mentioned (glad you have the multiple resources!).

    Otherwise, great tutorial, I have absolutely been loving this series and its making a dream come true. Thank you!

    1. Sorry, by “adding” I mean highlighting when its added to the scripts. Your scripts are correct, I just couldn’t find where in the tutorial it was included.

Leave a Reply

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

Note: You can use Markdown to format your comments.

For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

I agree to these terms.

This site uses Akismet to reduce spam. Learn how your comment data is processed.