6
\$\begingroup\$

I'm trying to create an in-game time system. Thus far it only works properly if you use 1440 minutes to the day (realtime), but I would like to be able to alter the day length to something like 10 minutes for example.

When adjusting it to 10, 20, etc, the clock starts to increment extremely rapidly to the point that an entire week passes in less than a second if you set it to one minute, or a day passes in about 12 seconds at around 10 realtime minutes to the day.

public float DayLengthInMinutes = 1440;// How long a day is in real world minutes
public int HoursToTheDay = 24;       // How many hours there are in a day
public int MinutesToTheHour = 60;    // How many minutes there are in an hour
public int SecondsToTheMinute = 60;  // How many seconds there are in a minute

public float _dayLength;                // Day length in seconds
public float _hourLength;               // Hour length in seconds
public float _minuteLength;             // Minute length in seconds
public float _secondLength;             // Seconds length in seconds
private float timer;                    // Timer that will be tracking game time

void Initialize () {
    timeSinceStart = Time.realtimeSinceStartup;
    _dayLength = _dayLengthInMinutes * 60;  //get seconds to the day
    _hourLength = _dayLength / HoursToTheDay; //determines how long an hour is
    _minuteLength = _hourLength / MinutesToTheHour; //determines how long a minute is
    _secondLength = _minuteLength / SecondsToTheMinute; //determines how long a second is
}
void CalculateTime () {
    if (timer >= _secondLength) {
        currentSecond++;
        timer = 0;
    }
    if (currentSecond >= _minuteLength) {
        currentMinute++;
        currentSecond = 0;
    }
    if (currentMinute >= _hourLength) {
        currentHour++;
        currentMinute = 0;
    }
    if (currentHour >= HoursToTheDay) {
        currentDay++;
        currentHour = 0;
        currentMinute = 0;
        currentSecond = 0;
        dayOrNight = "AM";
    }
}

EDIT: I'm actually basically finished with the while thing, it just isn't very user friendly yet. If people are interested I can answer with the two scripts it is currently running as. I haven't managed to get it into one without Unity crying and I'm not certain why.

\$\endgroup\$
  • \$\begingroup\$ That's a very interesting system! I would very like to see it in action. Also, was going to propose something, but Tom K answer hits the spot. \$\endgroup\$ – Hugo Rocha Jan 22 '15 at 22:38
6
\$\begingroup\$

I see your problem, so let's run through the code here. At the start, you say there are (1440 * 60) seconds in a day, but then you proceed to define the number of hours in the day, the number of minutes in an hour, and the number of seconds in a minute. It works when the numbers are correct, but let's go through the logic of what happens if you define 600 seconds in a day (so 10 minutes in "realtime" is a day). What happens is you have _dayLength = 600. This is then divided by 24, which gives a result that is the number of seconds in your new hour. _hourLength = 600 /24, so an hour is now 25 units, which in this case is "realtime seconds". You then define a minute as being _hourLength / 60, so _minuteLength is 0.417 (rounded up) "realtime seconds". You then define a _secondLength as being _minuteLength / 60, so a second is now 0.007 "realtime seconds".

Taking the function CalculateTime(), the first thing it does is see if the timer that has expired since the last time we ran this is greater than _secondLength. For the purposes of this example, let's say your timer value comes in at 0.1 "realtime seconds". In CalculateTime, it checks to see if timer is greater than _secondLength, which it is. So currentSeconds gets incremented by 1 (and for the purposes of this discussion, it is starting at 0, so it is now 1), and timer is set to 0. The next compare is whether currentSecond >= minuteLength. Since minute length is set to 0.417 "realtime seconds", and 1 is definitely greater than 0.417, currentSeconds is getting set to 0, and currentMinutes is being incremented by 1. Our timer is already way off here, but everytime we hit this loop, as long as timer is greater than _secondLength, we'll increment another minute. If we call this loop 60 times a second, then in 1 second an hour has gone by, and in 24 seconds a day has gone by.

There's a number of failures in this code, but it all starts with a fundamental one, which is that you are not consistent in whether you measuring "game seconds" or "realtime seconds". _secondLength is 0.007 "realtime seconds", which is fine, but where it gets mixed up is in the comparison of CurrentSecond >= _minuteLength. CurrentSecond is in "game seconds", not "realtime seconds", but _minuteLength is a minute measured in "realtime seconds", so you can't compare the two. I am assuming you still want 60 seconds in a minute, no matter what, so CurrentSecond >= _minuteLength should actually be CurrentSecond >= SecondsToTheMinute. This same problem occurs down the line with the rest of the time intervals.

There's an additional bug in the code, which is that everytime you call this loop, if more than one "game second" has passed, you will only count the first one, so it's not counting the overflow condition correctly. What you want to do is something like this:

while (timer >= _secondLength) 
{
        currentSecond++;
        timer -= _secondLength;
}

This will make it count correctly if more than one "game second" has passed. The same overflow condition exists in your other tests, so here's the whole CalculateTime function fixed:

void CalculateTime () 
{
    while (timer >= _secondLength) 
    {
        currentSecond++;
        timer -= _secondLength;
    }
    while (currentSecond >= SecondsToTheMinute) 
    {
        currentMinute++;
        currentSecond -= SecondsToTheMinute;
    }
    while (currentMinute >= MinutesToTheHour) 
    {
        currentHour++;
        currentMinute -= MinutesToTheHour;
    }
    while (currentHour >= HoursToTheDay) 
    {
        currentDay++;
        currentHour -= HoursToTheDay;
        dayOrNight = "AM";
    }
}
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ That is fantastic, thanks a ton! I originally had a pastebin link to the entire script for reference, but my post got edited to remove content that was "not needed" for me, and the pastebin link got pulled as well. Unfortunately most of the script is required for any of it to make sense, but your fix for my calculation function is exactly what I needed. Marked as the answer in hopes that it might help someone else. There was practically no information on the net that I could find about doing this so I did it all pretty well blind \$\endgroup\$ – cayne Jan 22 '15 at 20:21
  • \$\begingroup\$ @cayne external links tend to get pulled as they often go stale, best to put the content in the original post, as links go bad and then the Q/A won't make sense anymore to future readers \$\endgroup\$ – user2813274 Jul 9 '17 at 16:01
2
\$\begingroup\$
_hourLength = _dayLength / HoursToTheDay; //determines how long an hour is
_minuteLength = _hourLength / MinutesToTheHour; //determines how long a minute is
_secondLength = _minuteLength / SecondsToTheMinute; //determines how long a second is

Could be all that. Integer division results in integers, and you didn't supply enough code to know what type _dayLength and friends are.

In general, just get rid of all this hour and minute math. Count everything by seconds from some epoch. A 32-bit count of seconds is enough to hold almost 70 years; a 64-bit count of seconds is more time than the universe has left.

Just be sure to sensibly deal with frame time updates. e.g., a frame might only take 5 milliseconds, but you can't add .005 to an integer and get a useful result. You could keep a second floating-point time accumulator that rolls into your seconds count every time it reaches 1.0, or you could even just use double for your seconds count.

You can get the current year, month, day, hour, minutes, and seconds by simply dividing that world time counter by the appropriate value; no need to keep track of them. You can scale the speed of time by simply multiplying the elapsed time before adding it to the time counter; no need to modify any other expression or constants in the system.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ _daylength and friends are all floats. I've been using floats pretty much exclusively as I have not been certain what a better type to use would be so far. I had a feeling that Time.deltaTime would be a solution to dealing with frames rendering differently on different machines, but I was also playing with the idea of using the Time.realTimeSinceStartup as a counter, but I don't believe I can pause/reset that as easily as I could a floating point counter. \$\endgroup\$ – cayne Jan 22 '15 at 18:06
2
\$\begingroup\$

If a in-game day is 10 minutes in real life, you just need to get the number of seconds since the last 10 minutes and add a ratio to convert it to a 24 hours format.

Get the number of seconds from the last 10 minutes.

realTimeSecondsSinceTheLast10Minutes = (Minutes % 10) * 60 + seconds;

Get the ratio between a 10 minutes day to a 24 hour day

ratio = (60*60*24) / (60*10)

And add that ratio

worldSecondsSinceBeginingOfDay = realTimeSecondsSinceTheLast10Minutes * ratio;

And now you have a the number of world seconds since the beginning of the day. This can be converted to hours/minutes in a multiples of ways.

worldHours = worldSecondsSinceBeginingOfDay / 60 / 60
worldMinutes = (worldSecondsSinceBeginingOfDay % (60 * 60)) / 60
worldSeconds = worldSecondsSinceBeginingOfDay % 60

If you don't need to display the time, just work with realTimeSecondsSinceTheLast10Minutes and assume that a day is (60 * 10) seconds long.

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

[EDIT] (Original post below)

I found an even simpler way of doing this, using the TimeSpan class. I also updated the class to be semi-singletonish, with an 'Instance' property that gets auto set in the Awake event. (Just make sure not to use this before it is 'Awoken')

You only need to add this script to any existing game object in your scene, and you can access the timer like 'GameTimer.Instance.GameTime.Minutes' for example, from anywhere in your code.

While running, I watched the debug output while I adjusted the TimeRatio in the inspector and the time did slow down or speed up accordingly, so this appears to be working.

using System;
using UnityEngine;

public class GameTimer : MonoBehaviour {
    public static GameTimer Instance;
    public float TimeRatio = 0.01f;                         // Game Day ratio to Real Day
    public TimeSpan GameTime = new TimeSpan();              // Current adjusted game time
    private float gameSeconds = 0;                          // Accumulated 'Game Time' in seconds

    void Awake() {
        Instance = this;
    }

    public void Update() {                              
        gameSeconds += Time.deltaTime / TimeRatio;          // Add to 'GameTime' - number of ms at 'TimeRatio' rate
        GameTime = TimeSpan.FromSeconds(gameSeconds);       // Convert to a usable format
        Debug.Log("Current Game Time: " + ToString());
    }

    public override string ToString() {
        //                        DAYS,  HOUR : MINS : SECS . MS
        return string.Format("{0} Days, {1:00}:{2:00}:{3:00}.{4:000}", GameTime.Days, GameTime.Hours, GameTime.Minutes, GameTime.Seconds, GameTime.Milliseconds);
    }
}

------------------------------------------------------------

[ORIGINAL POST]

The following should give you what you want.

public class GameTimer : MonoBehaviour {
    public float TimeRatio = 0.01f;                     // Game Day ratio to Real Day
    public float GameTime = 0;                          // Accumulated 'Game Time' in seconds

    public void Update() {                              
        GameTime += Time.deltaTime / TimeRatio;         // Add to 'GameTime' at 'TimeRatio' rate
        Debug.Log("Current Game Time: " + ToString());
    }

    public override string ToString() {
        //                        DAYS,  HOUR : MINS : SECS . MS
        return string.Format("{0} Days, {1:00}:{2:00}:{3:00}.{4:000}", Days, Hours, Minutes, Seconds, Milliseconds);
    }

    public int Milliseconds {
        get { 
            return (int)((GameTime - TotalSeconds) * 1000); 
        }
    }

    public int Seconds {
        get {
            return TotalSeconds % 60; 
        }
    }

    public int Minutes {
        get { 
            return TotalSeconds / 60 % 60; 
        }
    }

    public int Hours {
        get { 
            return TotalSeconds / (60 * 60) % 60 % 24;
        }
    }

    public int Days {
        get { 
            return TotalSeconds / (60 * 60 * 24); 
        }
    }

    public int TotalSeconds {
        get {
            return (int)GameTime; 
        }
    }

}

The TimeRatio is what fraction of a real day the game day is. So 0.01 means the game day is 1/100th of a real day.

You can adjust the TimeRatio mid-game, and it will blend perfectly - only affecting the time from now on. Useful for powerups / modes that temporarily increase or decrease clock speed.

Just use GameTimer.Seconds, GameTimer.Hours, GameTimer.GameTime, GameTimer.ToString(), etc in your code to access the various properties. (If you make everything static, or via a reference variable.)

I haven't tested this too much, just thought it was a neat challenge, and may one day use it in one of my games.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Doug, please keep your edits to significant changes only. You've edited this post a number of times, many of which are very small changes that don't significantly change the content of the post. Every edit pushes the question to the top of the active list, where it shouldn't be for such small changes. \$\endgroup\$ – MichaelHouse Jan 28 '15 at 21:52
  • \$\begingroup\$ Sorry, had no idea. I assumed only new content would do that. Darn perfectionist tendencies! But it's done now. \$\endgroup\$ – Doug.McFarlane Jan 28 '15 at 22:03
  • \$\begingroup\$ Great! It looks good to me. +1 \$\endgroup\$ – MichaelHouse Jan 28 '15 at 22:04
  • \$\begingroup\$ That's actually pretty cool. I couldn't get timespan to work in the way I wanted so I basically wrote my own implementation, but it can't seem to handle anything under a millisecond, or over a decade. \$\endgroup\$ – cayne Feb 7 '15 at 6:30

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.