0
\$\begingroup\$

I have been trying to create in-game time/calendar system.

In my current project I want to be able to register events at specific times on specific days/seasons.

An in-game day is 30 minutes, each month is 30 days long, there are the usual 4 seasons.

So far I have decided a singleton class called TimeManager will handle the in-game time/date and this is where things can register an event at a specific time.

public class TimeManager : MonoBehaviour
{
    public float timeMultiplier = 1f;
    //One in-game day is 30 minutes.
    public float MINUTES_IN_INGAME_DAY = 30f;
    //there are 1800 seconds in 30 minutes
    public float INGAME_DAY_IN_SECONDS = 1800f;
    //there 60 seconds to a minute
    public static float SECONDS_IN_MINUTES = 60f;

    public static int DAYS_IN_MONTH = 30;
    public static TimeManager Instance;

    private int _CurrentDay = 0;
    private int _CurrentYear = 0;
    private int _CurrentMonth = 0;
    private int _CurrentSeason = 0;

    private float CurrentBaseTime = 0;

    public Days CurrentDay = Days.MONDAY;
    public Seasons CurrentSeason = Seasons.SPRING;
    public Months CurrentMonth = Months.JANUARY;

    private void Awake()
    {
        Instance = this;
    }

    private void Update()
    {
        UpdateTime();
    }

    private void OnValidate()
    {
        INGAME_DAY_IN_SECONDS = MINUTES_IN_INGAME_DAY * SECONDS_IN_MINUTES;
    }

    private void UpdateTime()
    {
        CurrentBaseTime += Time.deltaTime * timeMultiplier;

        if (CurrentBaseTime >= INGAME_DAY_IN_SECONDS)
        {
            //new month
            if(_CurrentDay > DAYS_IN_MONTH)
            {
                _CurrentDay = 0;
                _CurrentMonth++;

                //new year, reset season
                if (_CurrentMonth > 12)
                {
                    _CurrentYear++;
                    _CurrentMonth = 0;
                    CurrentSeason = Seasons.SPRING;
                }

                //new season
                if (_CurrentMonth % 4 == 0)
                {
                    _CurrentSeason++;
                    CurrentSeason = (Seasons)(_CurrentSeason % 4);
                }

                
                CurrentMonth = (Months)_CurrentMonth;
            }
            else
            {
                //same month
                _CurrentDay++;           
            }

            CurrentDay = (Days)(_CurrentDay % 7);
            //reset time to "midnight"
            CurrentBaseTime = 0;
        }
    }

    public int GetCurrentDay()
    {
        return _CurrentDay;
    }

    public float GetTimeInMinutes(float elapsedTime)
    {
        return (elapsedTime / SECONDS_IN_MINUTES);
    }

    public float GetTimeInHours(float elapsedTime)
    {
        return (elapsedTime / SECONDS_IN_MINUTES / MINUTES_IN_INGAME_DAY) * 24f;
    }

    private void OnGUI()
    {
        GUI.color = Color.black;
        GUI.Label(new Rect(10f, 0f, 400f, 20f), new GUIContent("World Time Info"));
        GUI.Label(new Rect(10f, 20f, 400f, 20f), new GUIContent("Current Time In Seconds : " + CurrentBaseTime));
        GUI.Label(new Rect(10f, 40f, 400f, 20f), new GUIContent("Current Day : " + GetCurrentDay()));
        GUI.Label(new Rect(10f, 60f, 400f, 20f), string.Format("H: {0} M : {1} S : {2}" , Mathf.Round(GetTimeInHours(CurrentBaseTime)), Mathf.Round(GetTimeInMinutes(CurrentBaseTime)), CurrentBaseTime - GetTimeInHours(CurrentBaseTime)));
    }
}

The problem I am having is taking the total seconds of the day and properly converting it to a 24 hour format, currently for every 2 times the minutes increments the hour value will go up by one. (gif included watch the minutes and hours)

enter image description here

I am entirely unsure where how to proceed with this, It's reached a point where I am bashing random things in and watching the outcome to see if it matches what I expect, I would like help converting in-game day in seconds to hours/minutes/seconds, in 24 hour format if possible.

I have read up on an alternative but atleast the one I read involved using the DateTime class, which I don't think would help me in my case, as the day and month length are none standard.

Thank you for reading.

\$\endgroup\$
2
\$\begingroup\$

First I'm going to answer your actual question:

You have configured your day to last 30 minutes. That means 1 hour is 30 / 24 = 1.25 minutes. That is why the hours appear to increase roughly every 2 minutes.


Additionally, your GUI code uses Mathf.Round(), when you should be using Mathf.Floor().

For example, let's imagine we're using real time (60 minutes/hour and 24 hours/day) and looking at 12:54:00am (54 minutes, 0 seconds into the day). In 24-hour time, the time should be displayed 00:54. However, if you use Mathf.Round(), the time will be displayed incorrectly.

The current hour is 54 / 60 = .9 hours. The current minutes is 54. When you round these values, the time is incorrectly displayed as 01:54 because .9 hours is rounded to 1 hour.


Additionally, you do not account for minutes correctly when displaying the time. Using a 60-minute hour/24-hour day again, let's say we're looking at 1:29:00am (01:29 in 24 hour time). This is 89 minutes into the day, or 1 hour and 29 minutes into the day.

Your GetTimeInMinutes() function returns the total number of minutes that have elapsed during the day, in this case 89 minutes. Your current GUI code does not subtract the minutes that are part of hours, and thus would render the time like this:

01:89:00

Lastly, it seems like your code is motivated by the idea of having 30 minutes of real-time translate to one day of in-game time. Your solution (which makes in-game seconds and minutes match real-time, but in-game hours last 1.25 minutes) is going to give you major headaches and make you completely miserable.

You should use a time ratio instead. If one in-game day lasts 30 minutes of real time, your in-game clock should move at 48x a real-time clock. You can then use C#'s built-in classes for time.

public class TimeManager {
    const float TIME_RATIO = 48;
    private DateTime startDate;
    private float startUnityTime;

    public TimeManager(DateTime startDate) {
        this.startDate = startDate;
        startUnityTime= Time.time;
    }

    public TimeSpan TimeElapsed {
        get {
            float secondsElapsed = Time.time - startUnityTime;
            double scaledSecondsElapsed = secondsElapsed * TIME_RATIO;
            TimeSpan elapsed = TimeSpan.FromSeconds(scaledSecondsElapsed);
            return elapsed;
        }
    }

    public DateTime InGameTime => startDate + TimeElapsed;
}

Video games often make the time ratio a multiple of 60. For example, if your ratio is 60:1, that means in-game time passes 60x faster than real time, so 1 real-time second is 1 in-game minute and 1 real-time minute is 1 in-game hour.

\$\endgroup\$
0
\$\begingroup\$

Ah, the joys of date and time handling. There is a reason why everyone recommends to leave this to a library.

However, your ingame calendar is a lot simpler than the Gregorian calendar. A minute is always 60 seconds (I assume), a day is always 30 minutes, month is always 30 days, and a year has exactly 12 month, without any funny leap-day shenanigans. This regularity means that there is actually no need to track hours, days, months and years separately, because it is trivial to calculate all of these by just from the seconds since game start.

minutes_since_game_start = (int)CurrentBaseTime / 60;
days_since_game_start = (int)CurrentBaseTime / (60 * 30);
month_since_game_start = (int)CurrentBaseTime / (60 * 30 * 30);
years_since_game_start = (int)CurrentBaseTime / (60 * 30 * 30 * 12);   

Day_in_current_month = (days_since_game_start % 30) + 1;
Month_in_current_year = (month_since_game_start % 12) + 1;
Season_in_current_year = (month_since_game_start % 12) / 3;

Note that the *_since_game_start variables will all start at 0. I only made those values start at 1 which you likely want to show in the UI.

\$\endgroup\$
4
  • \$\begingroup\$ thank you very much for the thorough explanation, I don't have a mathematical mind for these things so this really helps. \$\endgroup\$ Apr 20 at 17:04
  • \$\begingroup\$ I will mark the answer correct after I've had a chance to properly test it, I see no reason to believe you'd be wrong just want to be sure. \$\endgroup\$ Apr 20 at 17:34
  • \$\begingroup\$ I've had time to try it out and still do not understand how I would take these values in order to display the current time for the current day, eg : its 12:40:30 am of Day 3, this is why I tried resetting the CurrentBaseTime to 0 on each new day so I could attempt to figure out at what specific time I am at for the current day, I'm trying to make this as painless as possible for anyone helping me and I'm failing, I apologise. \$\endgroup\$ Apr 20 at 18:19
  • \$\begingroup\$ @Pheonix2105 The description in your question doesn't mention any hours, it only says "An in-game day is 30 minutes". In the system described here, you shouldn't need to reset CurrentBaseTime each day. It represents the seconds since the beginning of the game, and all other components of the time are derived directly from it. \$\endgroup\$
    – Philipp
    Apr 20 at 22:08
-1
\$\begingroup\$

Why not take the same approach as linux... Just count the total number of seconds since some fixed point (say the start of the game) and then format for display purposes...

    clock_time += Time.deltaTime; // Let's say 654064 seconds for the sake of the example
    var t = TimeSpan.FromSeconds(clock_time);
    Debug.LogFormat(String.Format("Elapsed Time: {0}", t.ToString()));
    // Outputs: "Elapsed Time: 7.13:41:04" when clock_time is 654064

You can also pull out the specific properties if you want...

t.Days, t.Minutes, t.Seconds, etc, etc

\$\endgroup\$
1
  • \$\begingroup\$ Be very careful when storing time in a float, and even more careful when adding small floats to a running total. If someone leaves the game running for 3 days (or suspends and resumes it in shorter play sessions totaling 3 days), then your clock_time value could begin to stutter or even halt entirely, as the increment you're adding each frame is so small relative to the total magnitude that it falls off the end of the mantissa's representable digits. \$\endgroup\$
    – DMGregory
    Apr 20 at 19:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.