Search Unity

Resolved Building an accurate clock (that will stay accurate over time)?

Discussion in 'Scripting' started by mbiggs2334, May 12, 2023.

  1. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    Hello. I'm wondering the best way to approach building an in-game clock that can match 'wall clock' time to the best accuracy possible.

    Seeing as how the time display is going to mimic how we use clocks in the real world I don't need ms precision. But my biggest concern is accuracy of the measured time over actual time. My research as led me in a better direction than where I started but I still am not sure of the best approach.

    Accumulating with
    Time.deltaTime
    seems to be more or less out due to either potential framerate problems or float point inconsistencies after a certain period. I read similar things about using
    Time.time
    .

    I read that
    Stopwatch
    may be a viable option but I'm not sure if there are any caveats to that.

    Am I overcomplicating things? Originally, I was just using an
    InvokeRepeating
    on
    Start
    . But if my understanding of
    InvokeRepeating
    is correct, if the main thread gets held up for whatever reason the timing can then be thrown off. Is there a way of keeping accurately measured time over potentially long periods of time?
     
  2. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209
    One caveat to stopwatch is that all Microsoft time keeping functions are fairly expensive, by that I mean you cant have alot of them in one frame and expect a game playable frame rate.

    Time delta is good floats are cheap to keep time. And floats are fairly accurate. But floats stop counting accurately to real time if a long frame occurs or something otherwise stalls it or fluctuates your frames.

    Timespan is another method and again you cannot get away with using too many! Only the one or two clock if you intend to have other stuff happening in the scene. The main expense is the string production as most of these result strings. While float results a nice number. Likewise an integer based timer.

    For a acceptable world accurate clock you might prefer to draw time data from an online source.
     
    mbiggs2334 likes this.
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Edit: For future reference, more elaborate answer can be found in post #17 (namely I forgot about Time.timeAsDouble which should be used if accuracy over long periods is required); and btw, the code that I provide in this thread is to be seen more as an experiment (although a successful one); snippets of this are valid if you want to track intervals of time, such as cooldowns etc. But it's mostly just a playful gizmo art.
    ---

    I'd recommend doing occasional reality checks by comparing the accumulated value against Time.time, realtimeSinceStartup, or timeSinceLevelLoad.

    It makes sense to cross-check your estimated passage of time constantly (or regularly) and to compute an error.
    Once this error exceeds some small threshold, immediately apply the correction.

    This way you get pretty accurate sub-second readings that do not wobble out of reality due to accumulation error, however depending on your frame rate variance, your millisecond rating might not be perfect. But your wall clock rating will.
     
    Last edited: May 15, 2023
    mbiggs2334 and StarBornMoonBeam like this.
  4. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    Thank you both for the information and advice.

    I just had a thought, but tell me if there is a flaw in it.
    InvokeRepeating
    may potentially be held up in the main thread, but that would still maintain 'in-game' time accuracy, wouldn't it?

    If the main thread gets held up, then so do the other things in the game. In that scenario I may have "lost time" compared to real time, but in game time it would still be accurate and in line with itself, wouldn't it?

    Edit: Realizing this new thought, I may have mis-posted the question. I don't think I need it to be accurate to real time, assuming my thought is correct.
     
  5. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Do not use coroutines for this. Time measuring devices need not and should not be coroutines.
    Coroutines are Sarlacc pits for beginners.

    I don't know which school and whose tutorial tells you to think about everything through coroutines. Don't do it. Steer clear of them. Forget about them. Go work in a mine, or even worse: in a call center, all in an attempt to abstain hard from having to measure time via coroutines.

    Look all you need to do is to make one simple script
    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using TimeUtil = UnityEngine.Time;
    4.  
    5. public class MyTime : MonoBehaviour {
    6.  
    7.   private const float TOLERANCE = 1E-2f;
    8.  
    9.   static MyTime _instance;
    10.   static public MyTime Instance => _instance;
    11.  
    12.   float _lastCheck;
    13.   public float Time { get; private set; } // you can get this by doing MyTime.Instance.Time
    14.  
    15.   void Awake() {
    16.     _instance = this;
    17.     Time = _lastCheck = TimeUtil.time;
    18.   }
    19.  
    20.   void Update() {
    21.     Time += TimeUtil.deltaTime; // accumulate delta time as normal
    22.  
    23.     if((int)(Time - _lastCheck) > 0) { // check for discrepancies once per second
    24.       var t = TimeUtil.time;
    25.  
    26.       if(Math.Abs(Time - t) > TOLERANCE) { // react to error being to high
    27.         var next = (Time + t) / 2f; // lessen the correction abruptness
    28.         Debug.Log($"correction: was {Time:F4} now {next:F4}");
    29.         Time = next;
    30.       }
    31.  
    32.       _lastCheck = t;
    33.     }
    34.  
    35.   }
    36.  
    37. }
    You may add this as a proof of concept
    Code (csharp):
    1.   void OnDrawGizmos() {
    2.     const float QUAD = .5f * MathF.PI;
    3.     const float TAU = 4f * QUAD;
    4.     var pos = transform.position;
    5.  
    6.     for(int i = 0; i < 12 * 5; i++) {
    7.       Color c;
    8.       float t1;
    9.       if(i % 5 == 0) {
    10.         c = (i % 3) == 0? Color.white : Color.cyan;
    11.         t1 = (i % 3) == 0? .7f : .9f;
    12.       } else {
    13.         c = Color.cyan;
    14.         t1 = .96f;
    15.       }
    16.       drawDial(pos, 1f, t1, 1f, TAU / (12f * 5f) * i, c);
    17.     }
    18.  
    19.     var sec = (int)Time % 60;
    20.     var min = (int)Time / 60f;
    21.     var hrs = (int)Time / 3600f;
    22.  
    23.     drawDial(pos, 1f, 0f, .55f, QUAD - TAU / 12f * hrs, Color.white);
    24.     drawDial(pos, 1f, 0f, .8f, QUAD - TAU / 60f * min, Color.blue);
    25.     drawDial(pos, 1f, 0f, .9f, QUAD - TAU / 60f * sec, Color.red);
    26.  
    27.     drawCircle(pos, 1f, color: Color.cyan);
    28.   }
    29.  
    30.   void drawDial(Vector3 c, float r, float t1, float t2, float rad, Color? color = null) {
    31.     if(color.HasValue) Gizmos.color = color.Value;
    32.  
    33.     var q = trig(rad);
    34.     var a = Vector3.Lerp(c, c + r * q, t1);
    35.     var b = Vector3.Lerp(c, c + r * q, t2);
    36.  
    37.     drawSeg(a, b);
    38.   }
    39.  
    40.   void drawCircle(Vector3 c, float r, int segments = 48, Color? color = null) {
    41.     if(color.HasValue) Gizmos.color = color.Value;
    42.  
    43.     var last = Vector2.zero;
    44.     var step = 2f * MathF.PI / segments;
    45.  
    46.     for(int i = 0; i <= segments; i++) {
    47.       var next = c + r * trig(i * step);
    48.       if(i > 0) drawSeg(last, next);
    49.       last = next;
    50.     }
    51.   }
    52.  
    53.   static Vector3 trig(float rad) => new Vector3(MathF.Cos(rad), MathF.Sin(rad), 0f);
    54.  
    55.   void drawSeg(Vector3 a, Vector3 b, Color? color = null) {
    56.     if(color.HasValue) Gizmos.color = color.Value;
    57.     Gizmos.DrawLine(a, b);
    58.   }
    Hit play and check your scene view.

    Edit:
    Add this to the end of OnDrawGizmos for the milliseconds
    Code (csharp):
    1. var msc = Time % 1f * 1000f;
    2. var microDial = new Vector3(-1f, 1f, 0f) / 3f;
    3. drawDial(pos + microDial, 1f, 0f, .22f, QUAD - TAU / 1000f * msc, Color.white);
    4. drawCircle(pos + microDial, .22f, color: Color.white);
     
    Last edited: May 13, 2023
  6. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    Thank you for taking the time to write that up. But no one has even mentioned coroutines, so I'm a little confused on where you're coming from.

    I'll give your script a try when I'm able, thank you!
     
  7. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    InvokeRepeating is a coroutine. Sorry if what I wrote doesn't apply to you, here on the forum it's a constant plague, people use coroutines to hammer everything. They are useful, don't get me wrong, but we don't use hammers to cut a slice of bread.

    Anyway, you can add this for fun
    Code (csharp):
    1. void drawSine(Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    2.   if(color.HasValue) Gizmos.color = color.Value;
    3.  
    4.   var last = Vector2.zero;
    5.   var step = (max - min) / segments;
    6.  
    7.   for(int i = 0; i <= segments; i++) {
    8.     var next = c + new Vector3(2f * scale.x / segments * i, scale.y * MathF.Sin(i * step + min), 0f);
    9.     if(i > 0) drawSeg(last, next);
    10.     last = next;
    11.   }
    12. }
    This goes at the end of OnDrawGizmos.
    Code (csharp):
    1. drawSine(pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), 6f * Time, 6f * Time + 6f * TAU, color: Color.yellow);
     
    Last edited: May 13, 2023
  8. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    The first codebox is what is meant as a solution, I'm just having good fun with the rest of it.

    With that piece you can extensively add all kinds of behaviors: you can attach events to it, make your entire system react to it, make time-keeping available throughout, perhaps deal with time differently during a pause (currently we hijack the Unity's own 'scaled' time-keeping, so you can use Time.scale), and so on, and a single repository to maintain all of it. If you need more help about any of this, ask.
     
    mbiggs2334 likes this.
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Btw the correction (on my computer) gets applied on every 3 minutes (more or less) under a framerate of 300+. So it makes sense to do this to maintain a stable frame-by-frame measurement. If you don't care about sub-seconds or time lapse, then obviously just reach for Time.time and be happy with it.

    Performance wise, all this code does (most of the time) is adding delta time and subtracting two numbers. Every N frames (let's say 60 for a framerate of 60), it also subtracts and compares to decide whether to apply correction.

    It is immensely lightweight for something that you can use throughout without caring for time ever again. It's worth highlighting this again: you can hook timestamped book-keeping to this, and let potential subscribers get notified for alarms, cooldowns and whatnot. For animations, game events, or grandpa clocks in your game.

    So no coroutines, no multiple points of maintenance, no nothing.
     
    Last edited: May 13, 2023
    mbiggs2334 likes this.
  10. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    Thank you so much Orion, the script works wonderfully!!! I really appreciate all the help and knowledge; I'll do my best to put it to good use.

    Just for my own edification, can you explain why using coroutines is a bad idea for timekeeping?
    Also, would changing the Time float to a double be worth the trouble to maintain accuracy over long periods of time?

    Edit: Also, just for clarity, are you recommending avoiding coroutines in general or specifically for timekeeping?
     
    orionsyndrome likes this.
  11. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Coroutines are to be avoided regardless of timekeeping. They are not bad however (I've edited post #7 to clarify this a bit more), they're like people who can't say NO to anything you do, and so after hurting their feelings for far too long, they make a huge mess out of your birthday party, apparently out of nowhere.

    Coroutines are made on top of a feature in C# called enumerators. And enumerators are a large topic in and of themselves, and while they're not in any way problematic, using them for TIME (which is what coroutines explicitly do, given that they distribute some work over multiple frames) introduces invisible complexities. This is also called a "debt" in jargon. You constantly have this debt in your system. And payback is yet to come, that's the invisible trap.

    I cannot exhaustively name all such complexities, but if you're going down this path of abusing coroutines for every time-based solution, you will inevitably end up entangled in a big mess of pretty much uncontrollable behavior. I guess they have this alluring quality of looking easy to use and comprehend. Whereas in reality, as your project becomes more complex, you begin to see emergent interactions between them, some of them you want stopped, some of them resumed, some of them you need to react with something else, and some of these things are conditional and so on. Pretty late in the project you'll have hardcore issues like you go back to your main menu, and explosions simply won't stop and then you'll try patching this ad hoc, but then you'll also stop a coroutine that won't resume for some reason.

    Unity simply hasn't developed them with such extensibility and heavy-duty maintenance in mind. Coroutines, as they are, are just convenient gimmicks, maybe too convenient and should not be front and center for every beginner. You're supposed to use them sporadically to get something simple going without having to invest too much time in time-keeping frameworks. A good example in one of my projects was a confirmation dialog, where I use a coroutine to basically animate everything, and acknowledge user input, but everything stays compact in one place in one of my classes, and I don't have to create new classes and whatnot. But I'm sure there are plenty more examples, like twinkling details in menus, maybe recurrent random sounds getting triggered, and other simple stuff that you're absolutely positive won't end up interacting with your state logic, and you have good means of controlling.

    I think it's ok to change it to double. You could also introduce another integer that measures only hours, and reset the original 32-bit float every hour. It's practically the same thing, but has an added benefit of working (negligibly) faster on a frame-by-frame basis.

    Btw, here's the final addition to the above script, I promise I won't add more stuff :)
    Change the seconds dial (in OnDrawGizmos) to
    Code (csharp):
    1. drawDial(pos, 1f, 0f, .9f, QUAD - TAU / 60f * easeDial(Time % 60f), Color.red);
    (and remove
    var sec = ...
    )

    Then add this code
    Code (csharp):
    1. static float easeDial(float n) {
    2.   var f = n % 1f;
    3.   return f < .5f? (int)n + easeInOutBack(2f * f)
    4.                 : MathF.Ceiling(n);
    5. }
    6.  
    7. static float easeInOutBack(float n) {
    8.   const float c1 = 1.70158f;
    9.   const float c2 = c1 * 1.525f;
    10.  
    11.   return n < .5f? (sqr(2f * n) * ((c2 + 1f) * 2f * n - c2)) * .5f
    12.                 : (sqr(2f * n - 2f) * ((c2 + 1f) * (n * 2f - 2f) + c2) + 2f) * .5f;
    13. }
    14.  
    15. static float sqr(float n) => n * n;
    Edit:
    Or copy paste this
    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using TimeUtil = UnityEngine.Time;
    4.  
    5. public class MyTime : MonoBehaviour {
    6.  
    7.   private const float TOLERANCE = 1E-2f;
    8.  
    9.   static MyTime _instance;
    10.   static public MyTime Instance => _instance;
    11.  
    12.   float _lastCheck;
    13.   public float Time { get; private set; }
    14.  
    15.   void Awake() {
    16.     _instance = this;
    17.     Time = _lastCheck = TimeUtil.time;
    18.   }
    19.  
    20.   void Update() {
    21.     Time += TimeUtil.deltaTime;
    22.  
    23.     if((int)(Time - _lastCheck) > 0) {
    24.       var t = TimeUtil.time;
    25.  
    26.       if(Math.Abs(Time - t) > TOLERANCE) {
    27.         var next = (Time + t) / 2f;
    28.         Debug.Log($"correction: was {Time:F4} now {next:F4}");
    29.         Time = next;
    30.       }
    31.  
    32.       _lastCheck = t;
    33.     }
    34.  
    35.   }
    36.  
    37.   void OnDrawGizmos() {
    38.     const float QUAD = .5f * MathF.PI;
    39.     const float TAU = 4f * QUAD;
    40.     var pos = transform.position;
    41.  
    42.     for(int i = 0; i < 12 * 5; i++) {
    43.       Color c;
    44.       float t1;
    45.       if(i % 5 == 0) {
    46.         c = (i % 3) == 0? Color.white : Color.cyan;
    47.         t1 = (i % 3) == 0? .7f : .9f;
    48.       } else {
    49.         c = Color.cyan;
    50.         t1 = .96f;
    51.       }
    52.       drawDial(pos, 1f, t1, 1f, TAU / (12f * 5f) * i, c);
    53.     }
    54.  
    55.     var min = (int)Time / 60f;
    56.     var hrs = (int)Time / 3600f;
    57.  
    58.     drawDial(pos, 1f, 0f, .55f, QUAD - TAU / 12f * hrs, Color.white);
    59.     drawDial(pos, 1f, 0f, .8f, QUAD - TAU / 60f * min, Color.blue);
    60.     drawDial(pos, 1f, 0f, .9f, QUAD - TAU / 60f * easeDial(Time % 60f), Color.red);
    61.  
    62.     var msc = Time % 1f * 1000f;
    63.     var microDial = new Vector3(-1f, 1f, 0f) / 3f;
    64.     drawDial(pos + microDial, 1f, 0f, .22f, QUAD - TAU / 1000f * msc, Color.white);
    65.     drawCircle(pos + microDial, .22f, color: Color.white);
    66.  
    67.     drawSine(pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), 6f * Time, 6f * Time + 6f * TAU, color: Color.yellow);
    68.  
    69.     drawCircle(pos, 1f, color: Color.cyan);
    70.   }
    71.  
    72.   void drawSine(Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    73.     if(color.HasValue) Gizmos.color = color.Value;
    74.  
    75.     var last = Vector2.zero;
    76.     var step = (max - min) / segments;
    77.  
    78.     for(int i = 0; i <= segments; i++) {
    79.       var next = c + new Vector3(2f * scale.x / segments * i, scale.y * MathF.Sin(i * step + min), 0f);
    80.       if(i > 0) drawSeg(last, next);
    81.       last = next;
    82.     }
    83.   }
    84.  
    85.   void drawDial(Vector3 c, float r, float t1, float t2, float rad, Color? color = null) {
    86.     if(color.HasValue) Gizmos.color = color.Value;
    87.  
    88.     var q = trig(rad);
    89.     var a = Vector3.Lerp(c, c + r * q, t1);
    90.     var b = Vector3.Lerp(c, c + r * q, t2);
    91.  
    92.     drawSeg(a, b);
    93.   }
    94.  
    95.   void drawCircle(Vector3 c, float r, int segments = 48, Color? color = null) {
    96.     if(color.HasValue) Gizmos.color = color.Value;
    97.  
    98.     var last = Vector2.zero;
    99.     var step = 2f * MathF.PI / segments;
    100.  
    101.     for(int i = 0; i <= segments; i++) {
    102.       var next = c + r * trig(i * step);
    103.       if(i > 0) drawSeg(last, next);
    104.       last = next;
    105.     }
    106.   }
    107.  
    108.   static Vector3 trig(float rad) => new Vector3(MathF.Cos(rad), MathF.Sin(rad), 0f);
    109.  
    110.   void drawSeg(Vector3 a, Vector3 b, Color? color = null) {
    111.     if(color.HasValue) Gizmos.color = color.Value;
    112.     Gizmos.DrawLine(a, b);
    113.   }
    114.  
    115.   static float easeDial(float n) {
    116.     var f = n % 1f;
    117.     return f < .5f? (int)n + easeInOutBack(2f * f)
    118.                   : MathF.Ceiling(n);
    119.   }
    120.  
    121.   static float easeInOutBack(float n) {
    122.     const float c1 = 1.70158f;
    123.     const float c2 = c1 * 1.525f;
    124.  
    125.     return n < .5f? (sqr(2f * n) * ((c2 + 1f) * 2f * n - c2)) * .5f
    126.                   : (sqr(2f * n - 2f) * ((c2 + 1f) * (n * 2f - 2f) + c2) + 2f) * .5f;
    127.   }
    128.  
    129.   static float sqr(float n) => n * n;
    130.  
    131. }
     
    Last edited: May 13, 2023
    mbiggs2334 likes this.
  12. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Btw it's worth noting (an observation I'm making after leaving this to run for a while) that resetting the accumulator is pretty much mandatory. The farther you go, the more corrections will be reported, which is likely because the float itself is losing precision. It begins to accelerate after 40 minutes or so, because that's already 2400 seconds (so already 12th bit on the integer part). So I'm thinking it should be modified to count minutes internally and it can do this practically indefinitely without losing precision. Maybe I'll do that tomorrow.
     
  13. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    Wow. To so many things here haha. This is great. You're a wealth of knowledge.

    Thankfully I haven't used too many coroutines thus far. Other than one I had for time I had one to change a post processing effect over a couple seconds to show a vignette when the character is in 'stealth' and maybe one other that escapes me at the moment. But going forward I will absolutely be taking your words to heart. Probably going to even reworking the coroutines I have now to not use them. Your adage of large annoying bugs down the road really resonates with me haha.

    Also, I just checked out the gizmo portion. Freaking wild. I didn't know you can make things that complex in the scene view. So cool.
     
    orionsyndrome likes this.
  14. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    I'm going to write my own implementation of the minute counter and (assuming you do end up being able to write it and come back and post it) and compare it to yours to see how it looks. Hopefully my instincts are on the right track.
     
  15. mbiggs2334

    mbiggs2334

    Joined:
    Mar 31, 2022
    Posts:
    54
    Not super related to the post, but what's your take on delegates?
     
  16. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Well, delegates are high level constructs used to encapsulate the idea of function pointers.

    While C-like pointers are pretty much hidden from sight because they're treated as unsafe in C#, they are still a necessary concept to make all kinds of rewiring possible, and so delegates fit into this role but are designed to be safe, because they're themselves types (just like classes and interfaces), and so you can produce instances that fit the method description by hooking existing functions to them (or even producing new ones on the fly via reflection). You can then pass them around as arguments like you would pass any other reference type. I hope you can appreciate the amount of freedom and power offered by such features.

    Events and callbacks basically cannot function without delegates. And these are very important concepts for any serious system design. Although, since the introduction of anonymous functions (and lambdas) you can basically avoid having to explicitly write a delegate for every simple callback or dispatch service, they are still pretty much desired for events and other more verbose systems.

    There are also generic delegates Action and Func which are extremely convenient, especially with simple callbacks, predicates, and boxed implementations. All of these features turned C# into a beast with the amount of expressivity, if you ask me. Previously the boilerplate was ludicrous, but now I sometimes feel like it's a much lighter language, like Haxe or Lua, and that usually directly translates into better readability and productivity. I come btw from Actionscript where I got used to writing callbacks and async-friendly code, and it was really painful when I started adopting C# 15 years ago. (Not to mention the horrors of Unity's immediate mode GUI after getting spoiled by the event-bubbling beauty of retained GUI in Flash.)

    So there you go, delegates are a must, but how exactly you intend to use them is up to you, given that there are multiple ways to approach them.
     
    mbiggs2334 likes this.
  17. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Ok, so I've experimented and researched a bit more, and first of all, I've found a confirmation that Unity indeed internally keeps track of time via double. And this is what is supplied through Time.timeAsDouble. So that should be super-reliable.

    Now whether that measurement drifts over real time, which shouldn't be the case, it's hard to tell and I can't confirm that part. If that's the case, well bad luck, not even Stopwatch can help you with that. If that's not the case then you don't actually need a dedicated time piece, unless you want to explicitly measure precise intervals.

    I've introduced a minutes register and played more with this, i.e. the ability to retrieve time as a single and as a double (because we now need to aggregate measurements back from the individual pieces, it makes sense to make an accurate version of it).

    Oh and I added time scaling control, it directly manipulates timeScale.

    In this version, because it relies on timeAsDouble auto-correcting makes almost no sense and almost never triggers. But still, here's the full class for completeness sake. I will build a version out of this that can dispatch events, that's much more useful.

    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using TimeUtil = UnityEngine.Time;
    4. using Debug = UnityEngine.Debug;
    5.  
    6. public class ReliableTime : MonoBehaviour {
    7.  
    8.   [SerializeField] [Range(0f, 100f)] float _timeScale = 1f;
    9.  
    10.   private const int SIXTY = 60;
    11.   private const double TOLERANCE = 1E-2;
    12.  
    13.   static ReliableTime _instance;
    14.   static public ReliableTime Instance => _instance;
    15.  
    16.   float _lastTime;  // in seconds
    17.   float _measTime;  // in seconds
    18.   int _minRegister; // in minutes
    19.  
    20.   public float Time => SIXTY * _minRegister + _measTime;
    21.   public double TimeAsDouble => SIXTY * (double)_minRegister + (double)_measTime;
    22.  
    23.   public (int h, int m, float s) GetTime()
    24.     => ( (int)(_minRegister / (float)SIXTY),
    25.          _minRegister % SIXTY,
    26.          _measTime );
    27.  
    28.   void Awake() {
    29.     _instance = this;
    30.     setValues(TimeUtil.timeAsDouble);
    31.     _lastTime = _measTime;
    32.   }
    33.  
    34.   void setValues(double absTime) {
    35.     _measTime = (float)(absTime % SIXTY);
    36.     _minRegister = (int)(absTime / SIXTY);
    37.   }
    38.  
    39.   void Update() {
    40.     TimeUtil.timeScale = _timeScale;
    41.  
    42.     _measTime += TimeUtil.deltaTime;
    43.  
    44.     if(abs(_measTime - _lastTime) >= 1f) {
    45.       _lastTime = floor(_measTime);
    46.  
    47.       if(_timeScale <= 1f) {
    48.         var time = GetTime();
    49.         Debug.Log($"{time.h}:{time.m}:{time.s:F3}");
    50.       }
    51.  
    52.       var measured = TimeAsDouble;
    53.       var lapsed = TimeUtil.timeAsDouble;
    54.  
    55.       if(Math.Abs(measured - lapsed) > TOLERANCE) {
    56.         var next = (measured + lapsed) / 2f;
    57.         Debug.Log($"correction: was {measured:F4} now {lapsed:F4}");
    58.         setValues(lapsed);
    59.       }
    60.  
    61.       if(_measTime >= SIXTY) {
    62.         _measTime -= SIXTY;
    63.         _minRegister++;
    64.       }
    65.     }
    66.   }
    67.  
    68.   //----------------------------------------
    69.  
    70.   static readonly float D90 = .5f * MathF.PI;
    71.   static readonly float TAU = 4f * D90;
    72.  
    73.   void OnDrawGizmos() {
    74.     var t = Time;
    75.  
    76.     var pos = transform.position;
    77.  
    78.     for(int i = 0; i < 12 * 5; i++) {
    79.       Color c;
    80.       float t1;
    81.       if(i % 5 == 0) {
    82.         c = (i % 3) == 0? Color.white : Color.cyan;
    83.         t1 = (i % 3) == 0? .7f : .9f;
    84.       } else {
    85.         c = Color.cyan;
    86.         t1 = .96f;
    87.       }
    88.       drawDial(pos, 1f, t1, 1f, TAU / (12f * 5f) * i, c);
    89.     }
    90.  
    91.     var min = (int)t / 60f;
    92.     var hrs = (int)t / 3600f;
    93.  
    94.     drawDial(pos, 1f, 0f, .55f, D90 - TAU / 12f * hrs, Color.white);
    95.     drawDial(pos, 1f, 0f, .8f, D90 - TAU / 60f * min, Color.blue);
    96.     drawDial(pos, 1f, 0f, .9f, D90 - TAU / 60f * easedSecondsDial(t), Color.red);
    97.  
    98.     var msc = frac(t) * 1000f;
    99.     var microDial = new Vector3(-1f, 1f, 0f) / 3f;
    100.     drawDial(pos + microDial, 1f, 0f, .22f, D90 - TAU / 1000f * msc, Color.white);
    101.     drawCircle(pos + microDial, .22f, segments: 24);
    102.  
    103.     drawPlrSine(pos + .1f * Vector3.up, new Vector3(2f, -.8f, 1.2f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), 6f * t, 6f * t + 12f * TAU, segments: 96, color: Color.yellow);
    104.     drawPlrSine(pos, new Vector3(MathF.PI * 10f / 6f, .2f, 0f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), -6f * t, -6f * t + 6f * TAU, segments: 24, color: Color.gray);
    105.  
    106.     drawCircle(pos, 1f, color: Color.cyan);
    107.   }
    108.  
    109.   void drawPlrSine(Vector3 o, Vector3 s, Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    110.     if(color.HasValue) Gizmos.color = color.Value;
    111.  
    112.     var last = Vector3.zero;
    113.     var step = (max - min) / segments;
    114.  
    115.     for(int i = 0; i <= segments; i++) {
    116.       var next = mad(1f, new Vector3(2f * scale.x / segments * i, scale.y * sin(i * step + min), 0f), c);
    117.       if(i > 0) drawSeg(polar(ref o, ref s, last), polar(ref o, ref s, next));
    118.       last = next;
    119.     }
    120.  
    121.     static Vector3 polar(ref Vector3 c, ref Vector3 scale, Vector3 p) {
    122.       p -= c; return mad(scale.y * (p.y + scale.z), trig(scale.x * p.x + D90), c);
    123.     }
    124.   }
    125.  
    126.   // void drawSine(Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    127.   //   if(color.HasValue) Gizmos.color = color.Value;
    128.  
    129.   //   var last = Vector3.zero;
    130.   //   var step = (max - min) / segments;
    131.  
    132.   //   for(int i = 0; i <= segments; i++) {
    133.   //     var next = mad(1f, new Vector3(2f * scale.x / segments * i, scale.y * sin(i * step + min), 0f), c);
    134.   //     if(i > 0) drawSeg(last, next);
    135.   //     last = next;
    136.   //   }
    137.   // }
    138.  
    139.   void drawDial(Vector3 c, float r, float t1, float t2, float rad, Color? color = null) {
    140.     if(color.HasValue) Gizmos.color = color.Value;
    141.     var q = mad(r, trig(rad), c);
    142.     drawSeg(lerp(c, q, t1), lerp(c, q, t2));
    143.   }
    144.  
    145.   void drawCircle(Vector3 c, float r, int segments = 48, Color? color = null) {
    146.     if(color.HasValue) Gizmos.color = color.Value;
    147.  
    148.     var last = Vector2.zero;
    149.     var step = 2f * MathF.PI / segments;
    150.  
    151.     for(int i = 0; i <= segments; i++) {
    152.       var next = mad(r, trig(i * step), c);
    153.       if(i > 0) drawSeg(last, next);
    154.       last = next;
    155.     }
    156.   }
    157.  
    158.   void drawSeg(Vector3 a, Vector3 b, Color? color = null) {
    159.     if(color.HasValue) Gizmos.color = color.Value;
    160.     Gizmos.DrawLine(a, b);
    161.   }
    162.  
    163.   static float easedSecondsDial(float n) {
    164.     n = mod(n + .7f - 1f, 60f);
    165.     var f = frac(n);
    166.     return floor(n) + (f < .5f? 0f : easeInOutBack(2f * (f - .5f)));
    167.   }
    168.  
    169.   static float easeInOutBack(float n) {
    170.     const float c1 = 1.70158f;
    171.     const float c2 = c1 * 1.525f;
    172.  
    173.     return n < .5f? (sqr(2f * n) * ((c2 + 1f) * 2f * n - c2)) * .5f
    174.                   : (sqr(2f * n - 2f) * ((c2 + 1f) * (n * 2f - 2f) + c2) + 2f) * .5f;
    175.   }
    176.  
    177.   static float abs(float n) => Math.Abs(n);
    178.   static float floor(float n) => MathF.Floor(n);
    179.   static float frac(float n) => n % 1f;
    180.   static float sin(float rad) => MathF.Sin(rad);
    181.   static float cos(float rad) => MathF.Cos(rad);
    182.   static Vector3 mad(float a, Vector3 b, Vector3 c) => a * b + c;
    183.   static Vector3 lerp(Vector3 a, Vector3 b, float t) => (1f - t) * a + t * b;
    184.   static Vector3 trig(float rad) => new Vector3(cos(rad), sin(rad), 0f);
    185.   static float sqr(float n) => n * n;
    186.   static float mod(float n, float m) => (n %= m) < 0f? m + n : n;
    187.  
    188. }
    Edit:
    Oops, had a bug due to time scale
     
    Last edited: May 13, 2023
  18. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    And of course a 7-segment display, who needs a clockface without a 7-segment display.
    Ok I'll stop doing this now, it's a bit crazy :)
    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using TimeUtil = UnityEngine.Time;
    4. using Debug = UnityEngine.Debug;
    5.  
    6. public class ReliableTime : MonoBehaviour {
    7.  
    8.   [SerializeField] [Range(0f, 100f)] float _timeScale = 1f;
    9.  
    10.   private const int SIXTY = 60;
    11.   private const double TOLERANCE = 1E-2;
    12.  
    13.   static ReliableTime _instance;
    14.   static public ReliableTime Instance => _instance;
    15.  
    16.   float _lastTime;  // in seconds
    17.   float _measTime;  // in seconds
    18.   int _minRegister; // in minutes
    19.  
    20.   public float Time => SIXTY * _minRegister + _measTime;
    21.   public double TimeAsDouble => SIXTY * (double)_minRegister + (double)_measTime;
    22.  
    23.   public (int h, int m, float s) GetTime()
    24.     => ( (int)(_minRegister / (float)SIXTY),
    25.          _minRegister % SIXTY,
    26.          _measTime );
    27.  
    28.   void Awake() {
    29.     _instance = this;
    30.     setValues(TimeUtil.timeAsDouble);
    31.     _lastTime = _measTime;
    32.   }
    33.  
    34.   void setValues(double absTime) {
    35.     _measTime = (float)(absTime % SIXTY);
    36.     _minRegister = (int)(absTime / SIXTY);
    37.   }
    38.  
    39.   void Update() {
    40.     TimeUtil.timeScale = _timeScale;
    41.  
    42.     _measTime += TimeUtil.deltaTime;
    43.  
    44.     if(abs(_measTime - _lastTime) >= 1f) {
    45.       _lastTime = floor(_measTime);
    46.  
    47.       if(_timeScale <= 1f) {
    48.         var time = GetTime();
    49.         Debug.Log($"{time.h}:{time.m}:{time.s:F3}");
    50.       }
    51.  
    52.       var measured = TimeAsDouble;
    53.       var lapsed = TimeUtil.timeAsDouble;
    54.  
    55.       if(Math.Abs(measured - lapsed) > TOLERANCE) {
    56.         var next = (measured + lapsed) / 2f;
    57.         Debug.Log($"correction: was {measured:F4} now {lapsed:F4}");
    58.         setValues(lapsed);
    59.       }
    60.  
    61.       if(_measTime >= SIXTY) {
    62.         _measTime -= SIXTY;
    63.         _minRegister++;
    64.       }
    65.     }
    66.   }
    67.  
    68.   //----------------------------------------
    69.  
    70.   static readonly float D90 = .5f * MathF.PI;
    71.   static readonly float TAU = 4f * D90;
    72.  
    73.   void OnDrawGizmos() {
    74.     var t = Time;
    75.  
    76.     var pos = transform.position;
    77.  
    78.     drawClockMarks(pos, 1f, Color.cyan, Color.white);
    79.  
    80.     var sec = (int)t % 60f;
    81.     var min = (int)t / 60f % 60f;
    82.     var hrs = (int)t / 3600f % 12f;
    83.  
    84.     draw7SegmentValue(new Vector3(.12f, .35f, 0f), .02f, .07f, .01f, (int)hrs, Color.yellow);
    85.     draw7SegmentValue(new Vector3(.34f, .35f, 0f), .02f, .07f, .01f, (int)min, Color.yellow);
    86.     draw7SegmentValue(new Vector3(.56f, .35f, 0f), .02f, .07f, .01f, (int)sec, Color.yellow);
    87.  
    88.     drawDial(pos, 1f, 0f, .55f, D90 - TAU / 12f * hrs, Color.white);
    89.     drawDial(pos, 1f, 0f, .8f, D90 - TAU / 60f * min, Color.blue);
    90.     drawDial(pos, 1f, 0f, .9f, D90 - TAU / 60f * easedSecondsDial(t), Color.red);
    91.  
    92.     var msc = frac(t) * 1000f;
    93.     var microDial = new Vector3(-1f, 1f, 0f) / 3f;
    94.     drawDial(pos + microDial, 1f, 0f, .22f, D90 - TAU / 1000f * msc, Color.white);
    95.     drawCircle(pos + microDial, .22f, segments: 24);
    96.  
    97.     drawPlrSine(pos + .1f * Vector3.up, new Vector3(2f, -.8f, 1.2f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), 6f * t, 6f * t + 12f * TAU, segments: 96, color: Color.yellow);
    98.     drawPlrSine(pos, new Vector3(TAU * 5f / 6f, .2f, 0f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), -6f * t, -6f * t + 6f * TAU, segments: 24, color: Color.gray);
    99.  
    100.     drawCircle(pos, 1f, color: Color.cyan);
    101.   }
    102.  
    103.   void drawPlrSine(Vector3 o, Vector3 s, Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    104.     if(color.HasValue) Gizmos.color = color.Value;
    105.  
    106.     var last = Vector3.zero;
    107.     var step = (max - min) / segments;
    108.  
    109.     for(int i = 0; i <= segments; i++) {
    110.       var next = mad(1f, new Vector3(2f * scale.x / segments * i, scale.y * sin(i * step + min), 0f), c);
    111.       if(i > 0) drawSeg(polar(ref o, ref s, last), polar(ref o, ref s, next));
    112.       last = next;
    113.     }
    114.  
    115.     static Vector3 polar(ref Vector3 c, ref Vector3 scale, Vector3 p) {
    116.       p -= c; return mad(scale.y * (p.y + scale.z), trig(scale.x * p.x + D90), c);
    117.     }
    118.   }
    119.  
    120.   // void drawSine(Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    121.   //   if(color.HasValue) Gizmos.color = color.Value;
    122.  
    123.   //   var last = Vector3.zero;
    124.   //   var step = (max - min) / segments;
    125.  
    126.   //   for(int i = 0; i <= segments; i++) {
    127.   //     var next = mad(1f, new Vector3(2f * scale.x / segments * i, scale.y * sin(i * step + min), 0f), c);
    128.   //     if(i > 0) drawSeg(last, next);
    129.   //     last = next;
    130.   //   }
    131.   // }
    132.  
    133.   void drawClockMarks(Vector3 c, float r, Color color1, Color color2) {
    134.     for(int i = 0; i < SIXTY; i++) {
    135.       Color col;
    136.       float t1;
    137.       if(i % 5 == 0) {
    138.         col = (i % 3) == 0? color2 : color1;
    139.         t1 = (i % 3) == 0? .7f : .9f;
    140.       } else {
    141.         col = color1;
    142.         t1 = .96f;
    143.       }
    144.       drawDial(c, r, t1, 1f, TAU / SIXTY * i, col);
    145.     }
    146.   }
    147.  
    148.   void drawDial(Vector3 c, float r, float t1, float t2, float rad, Color? color = null) {
    149.     if(color.HasValue) Gizmos.color = color.Value;
    150.     var q = mad(r, trig(rad), c);
    151.     drawSeg(lerp(c, q, t1), lerp(c, q, t2));
    152.   }
    153.  
    154.   void drawCircle(Vector3 c, float r, int segments = 48, Color? color = null) {
    155.     if(color.HasValue) Gizmos.color = color.Value;
    156.  
    157.     var last = Vector3.zero;
    158.     var step = TAU / segments;
    159.  
    160.     for(int i = 0; i <= segments; i++) {
    161.       var next = mad(r, trig(i * step), c);
    162.       if(i > 0) drawSeg(last, next);
    163.       last = next;
    164.     }
    165.   }
    166.  
    167.   void drawSeg(Vector3 a, Vector3 b, Color? color = null) {
    168.     if(color.HasValue) Gizmos.color = color.Value;
    169.     Gizmos.DrawLine(a, b);
    170.   }
    171.  
    172.   static float easedSecondsDial(float n) {
    173.     n = mod(n + .7f - 1f, 60f);
    174.     var f = frac(n);
    175.     return floor(n) + (f < .5f? 0f : easeInOutBack(2f * (f - .5f)));
    176.   }
    177.  
    178.   static float easeInOutBack(float n) {
    179.     const float c1 = 1.70158f;
    180.     const float c2 = c1 * 1.525f;
    181.  
    182.     return n < .5f? (sqr(2f * n) * ((c2 + 1f) * 2f * n - c2)) * .5f
    183.                   : (sqr(2f * n - 2f) * ((c2 + 1f) * (n * 2f - 2f) + c2) + 2f) * .5f;
    184.   }
    185.  
    186.   void draw7SegmentValue(Vector3 p, float space, float scale, float slant, int value, Color color, int digits = 2) {
    187.     for(int i = digits - 1; i >= 0; i--) {
    188.       int pw = pow(10, i);
    189.       int digit = value / pw;
    190.       draw7SegmentDigit(p, scale, digit, slant, color);
    191.       value -= digit * pw;
    192.       p.x += scale + space;
    193.     }
    194.   }
    195.  
    196.   //     0
    197.   //    ___
    198.   // 5 |   | 1
    199.   //   +---+
    200.   // 4 | 6 | 2
    201.   //   '---'
    202.   //     3
    203.  
    204.   static Vector3[] _ppts;
    205.   static int[] _pieces = new int[] { 0x3f, 0x6, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x7, 0x7f, 0x6f };
    206.  
    207.   void draw7SegmentDigit(Vector3 p, float scale, int digit, float slant, Color? color = null) {
    208.     if(_ppts is null) {
    209.       _ppts = new Vector3[6];
    210.       for(int y = 0; y < 3; y++)
    211.         for(int x = 0; x < 2; x++)
    212.           _ppts[2*y+x] = new Vector3(x, -y, 0f);
    213.     }
    214.  
    215.     drawSeg(pt(0), pt(1), test(0));
    216.     drawSeg(pt(1), pt(3), test(1));
    217.     drawSeg(pt(3), pt(5), test(2));
    218.     drawSeg(pt(4), pt(5), test(3));
    219.     drawSeg(pt(2), pt(4), test(4));
    220.     drawSeg(pt(0), pt(2), test(5));
    221.     drawSeg(pt(2), pt(3), test(6));
    222.  
    223.     Vector3 pt(int index) => p + scale * _ppts[index] + new Vector3(slant * _ppts[index].y, 0f);
    224.     Color test(int bit) => (color.HasValue && (_pieces[digit] & (1 << bit)) != 0)? color.Value
    225.                                                                                  : default(Color);
    226.   }
    227.  
    228.   static float abs(float n) => Math.Abs(n);
    229.   static float floor(float n) => MathF.Floor(n);
    230.   static float frac(float n) => n % 1f;
    231.   static float sin(float rad) => MathF.Sin(rad);
    232.   static float cos(float rad) => MathF.Cos(rad);
    233.   static Vector3 mad(float a, Vector3 b, Vector3 c) => a * b + c;
    234.   static Vector3 lerp(Vector3 a, Vector3 b, float t) => (1f - t) * a + t * b;
    235.   static Vector3 trig(float rad) => new Vector3(cos(rad), sin(rad), 0f);
    236.   static float sqr(float n) => n * n;
    237.   static float mod(float n, float m) => (n %= m) < 0f? m + n : n;
    238.   static int pow(float b, float p) => (int)MathF.Pow(b, p);
    239.   static float sat(float n) => Mathf.Clamp01(n);
    240.  
    241. }
     
    Last edited: May 13, 2023
    Bunny83 likes this.
  19. StarBornMoonBeam

    StarBornMoonBeam

    Joined:
    Mar 26, 2023
    Posts:
    209

    My fav part
    Code (CSharp):
    1.  
    2. [LIST=1]
    3. [*]//     0
    4. [*]  //    ___
    5. [*]  // 5 |   | 1
    6. [*]  //   +---+
    7. [*]  // 4 | 6 | 2
    8. [*]  //   '---'
    9. [*]  //     3
    10. [*]
    11. [/LIST]
    12.  

    I made a nice ETA tracker for a ball impact today but I can't show because this site is not my personal blog. But it works accurately using timespan and I may run many of those in build frame rate above 500 fps remains accurate to zero point impact. At 1900 resolution.

    Frame delta time is still a good measure when converted into timespan from seconds. Sure to get more accurate you would need to verify to a real world source and make time shifts to put your delta back in line.

    Timespan can access millisecond as well which is useful to display 1000 ms as 1 second sometimes.

    You also don't have to format the string. You can grab timespan second minute hour day month so. Quite good really.


    You could represent all timespan attributes as hands on a clock including custom like planet orbits moon phase
     
    Last edited: May 13, 2023
  20. unUmGong

    unUmGong

    Joined:
    May 13, 2023
    Posts:
    11
    Nice gizmo
    Is there way to draw gizmo in game?
     
  21. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    No, gizmos are by definition an editor feature. You can render lines via other means, for example GL API and there are numerous assets on the store, ranging from GL to shader drawers, allowing for primitives, nice antialiasing and whatnot. Unity normally uses LineRenderer as a common in-game solution.
     
    unUmGong likes this.
  22. unUmGong

    unUmGong

    Joined:
    May 13, 2023
    Posts:
    11
    Code (CSharp):
    1.    
    2.     public IEnumerator UNUMGONG;
    3.     public float T_I_M_E;
    4.     bool unUmGongs = true;
    5.     void start()
    6.     {
    7.         UNUMGONG = DoCheck();
    8.     }
    9.     IEnumerator DoCheck()
    10.     {
    11.         for ( ; unUmGongs ;  )
    12.         {
    13.             T_I_M_E += Time.deltaTime;
    14.            yield return new WaitForSeconds(Time.deltaTime);
    15.         }
    16.     }
    17.  
     
  23. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Ok this works for me with music at 120 BPM
    Do recommend :)

    Code (csharp):
    1. using System;
    2. using UnityEngine;
    3. using TimeUtil = UnityEngine.Time;
    4. using Debug = UnityEngine.Debug;
    5.  
    6. public class ReliableTime : MonoBehaviour {
    7.  
    8.   [SerializeField] [Range(0f, 100f)] float _timeScale = 1f;
    9.  
    10.   private const int SIXTY = 60;
    11.   private const double TOLERANCE = 1E-2;
    12.  
    13.   static ReliableTime _instance;
    14.   static public ReliableTime Instance => _instance;
    15.  
    16.   float _lastTime;  // in seconds
    17.   float _measTime;  // in seconds
    18.   int _minRegister; // in minutes
    19.  
    20.   public float Time => SIXTY * _minRegister + _measTime;
    21.   public double TimeAsDouble => SIXTY * (double)_minRegister + (double)_measTime;
    22.  
    23.   public (int h, int m, float s) GetTime()
    24.     => ( (int)(_minRegister / (float)SIXTY),
    25.          _minRegister % SIXTY,
    26.          _measTime );
    27.  
    28.   void Awake() {
    29.     _instance = this;
    30.     setValues(TimeUtil.timeAsDouble);
    31.     _lastTime = _measTime;
    32.   }
    33.  
    34.   void setValues(double absTime) {
    35.     _measTime = (float)(absTime % SIXTY);
    36.     _minRegister = (int)(absTime / SIXTY);
    37.   }
    38.  
    39.   void Update() {
    40.     TimeUtil.timeScale = _timeScale;
    41.  
    42.     _measTime += TimeUtil.deltaTime;
    43.  
    44.     if(abs(_measTime - _lastTime) >= 1f) {
    45.       _lastTime = floor(_measTime);
    46.  
    47.       if(_timeScale <= 1f) {
    48.         var time = GetTime();
    49.         Debug.Log($"{time.h}:{time.m}:{time.s:F3}");
    50.       }
    51.  
    52.       var measured = TimeAsDouble;
    53.       var lapsed = TimeUtil.timeAsDouble;
    54.  
    55.       if(abs(measured - lapsed) > TOLERANCE) {
    56.         var next = (measured + lapsed) / 2f;
    57.         Debug.Log($"correction: was {measured:F4} now {next:F4}");
    58.         setValues(next);
    59.       }
    60.  
    61.       if(_measTime >= SIXTY) {
    62.         _measTime -= SIXTY;
    63.         _minRegister++;
    64.       }
    65.     }
    66.   }
    67.  
    68.   //----------------------------------------
    69.  
    70.   static readonly float D90 = .5f * MathF.PI;
    71.   static readonly float TAU = 4f * D90;
    72.  
    73.   void OnDrawGizmos() {
    74.     var t = Time;
    75.  
    76.     var pos = transform.position;
    77.  
    78.     drawClockMarks(pos, 1f, Color.cyan, Color.white);
    79.  
    80.     var sec = floor(t) % 60f;
    81.     var min = floor(t) / 60f % 60f;
    82.     var hrs = floor(t) / 3600f % 12f;
    83.  
    84.     draw9SegmentValue(new Vector3(.12f, .35f, 0f), .02f, .07f, .01f, (int)hrs, Color.yellow);
    85.     draw9SegmentValue(new Vector3(.34f, .35f, 0f), .02f, .07f, .01f, (int)min, Color.yellow);
    86.     draw9SegmentValue(new Vector3(.56f, .35f, 0f), .02f, .07f, .01f, (int)sec, Color.yellow);
    87.  
    88.     drawDial(pos, 1f, 0f, .55f, D90 - TAU / 12f * hrs, Color.white);
    89.     drawDial(pos, 1f, 0f, .8f, D90 - TAU / 60f * min, Color.blue);
    90.     drawDial(pos, 1f, 0f, .9f, D90 - TAU / 60f * easedSecondsDial(t), Color.red);
    91.  
    92.     var msc = frac(t) * 1000f;
    93.     var microDial = new Vector3(-1f, 1f, 0f) / 3f;
    94.     drawDial(pos + microDial, 1f, 0f, .22f, D90 - TAU / 1000f * msc, Color.white);
    95.     drawCircle(pos + microDial, .22f, segments: 24);
    96.  
    97.     //drawPlrSine(pos + .1f * Vector3.up, new Vector3(2f, -.8f, 1.2f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), 6f * t, 6f * t + 12f * TAU, segments: 96, color: Color.yellow);
    98.     drawPlrSine(pos + .1f * Vector3.up, new Vector3(2f, -.8f, 1.2f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), 6f * t, 2f * sin(TAU * t) + 12f * t + 12f * TAU, segments: 96, color: Color.yellow);
    99.     drawPlrSine(pos, new Vector3(TAU * 5f / 6f, .2f, 0f), pos + new Vector3(-.6f, -.333f, 0f), new Vector2(.6f, .1f), -6f * t, -6f * t + 6f * TAU, segments: 24, color: Color.gray);
    100.  
    101.     drawCircle(pos, 1f, color: Color.cyan);
    102.   }
    103.  
    104.   void drawPlrSine(Vector3 o, Vector3 s, Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    105.     if(color.HasValue) Gizmos.color = color.Value;
    106.  
    107.     var last = Vector3.zero;
    108.     var step = (max - min) / segments;
    109.  
    110.     for(int i = 0; i <= segments; i++) {
    111.       var next = mad(1f, new Vector3(2f * scale.x / segments * i, scale.y * sin(i * step + min), 0f), c);
    112.       if(i > 0) drawSeg(polar(ref o, ref s, last), polar(ref o, ref s, next));
    113.       last = next;
    114.     }
    115.  
    116.     static Vector3 polar(ref Vector3 c, ref Vector3 scale, Vector3 p) {
    117.       p -= c; return mad(scale.y * (p.y + scale.z), trig(scale.x * p.x + D90), c);
    118.     }
    119.   }
    120.  
    121.   // void drawSine(Vector3 c, Vector2 scale, float min, float max, int segments = 48, Color? color = null) {
    122.   //   if(color.HasValue) Gizmos.color = color.Value;
    123.  
    124.   //   var last = Vector3.zero;
    125.   //   var step = (max - min) / segments;
    126.  
    127.   //   for(int i = 0; i <= segments; i++) {
    128.   //     var next = mad(1f, new Vector3(2f * scale.x / segments * i, scale.y * sin(i * step + min), 0f), c);
    129.   //     if(i > 0) drawSeg(last, next);
    130.   //     last = next;
    131.   //   }
    132.   // }
    133.  
    134.   void drawClockMarks(Vector3 c, float r, Color color1, Color color2) {
    135.     for(int i = 0; i < SIXTY; i++) {
    136.       Color col;
    137.       float t1;
    138.       if(i % 5 == 0) {
    139.         col = (i % 3) == 0? color2 : color1;
    140.         t1 = (i % 3) == 0? .7f : .9f;
    141.       } else {
    142.         col = color1;
    143.         t1 = .96f;
    144.       }
    145.       drawDial(c, r, t1, 1f, TAU / SIXTY * i, col);
    146.     }
    147.   }
    148.  
    149.   void drawDial(Vector3 c, float r, float t1, float t2, float rad, Color? color = null) {
    150.     if(color.HasValue) Gizmos.color = color.Value;
    151.     var q = mad(r, trig(rad), c);
    152.     drawSeg(lerp(c, q, t1), lerp(c, q, t2));
    153.   }
    154.  
    155.   void drawCircle(Vector3 c, float r, int segments = 48, Color? color = null) {
    156.     if(color.HasValue) Gizmos.color = color.Value;
    157.  
    158.     var last = Vector3.zero;
    159.     var step = TAU / segments;
    160.  
    161.     for(int i = 0; i <= segments; i++) {
    162.       var next = mad(r, trig(i * step), c);
    163.       if(i > 0) drawSeg(last, next);
    164.       last = next;
    165.     }
    166.   }
    167.  
    168.   void drawSeg(Vector3 a, Vector3 b, Color? color = null) {
    169.     if(color.HasValue) Gizmos.color = color.Value;
    170.     Gizmos.DrawLine(a, b);
    171.   }
    172.  
    173.   static float easedSecondsDial(float n) {
    174.     n = mod(n + .7f - 1f, 60f);
    175.     var f = frac(n);
    176.     return floor(n) + (f < .5f? 0f : easeInOutBack(2f * (f - .5f)));
    177.   }
    178.  
    179.   static float easeInOutBack(float n) {
    180.     const float c1 = 1.70158f;
    181.     const float c2 = c1 * 1.525f;
    182.  
    183.     return n < .5f? (sqr(2f * n) * ((c2 + 1f) * 2f * n - c2)) * .5f
    184.                   : (sqr(2f * n - 2f) * ((c2 + 1f) * (n * 2f - 2f) + c2) + 2f) * .5f;
    185.   }
    186.  
    187.   void draw9SegmentValue(Vector3 p, float space, float scale, float slant, int value, Color color, int digits = 2) {
    188.     for(int i = digits - 1; i >= 0; i--) {
    189.       int pw = pow(10, i);
    190.       int digit = value / pw;
    191.       draw9SegmentDigit(p, scale, digit, slant, color);
    192.       value -= digit * pw;
    193.       p.x += scale + space;
    194.     }
    195.   }
    196.  
    197.   //      0
    198.   //    _____
    199.   // 5 |   7/| 1
    200.   //   +--6--+
    201.   // 4 |/8   | 2
    202.   //   '-----'
    203.   //      3
    204.  
    205.   static Vector3[] _ppts;
    206.   static int[] _pieces = new int[] { 0x1bf, 0x86, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x1c1, 0x7f, 0x6f };
    207.  
    208.   void draw9SegmentDigit(Vector3 p, float scale, int digit, float slant, Color? color = null) {
    209.     if(_ppts is null) {
    210.       _ppts = new Vector3[7];
    211.       for(int y = 0; y < 3; y++)
    212.         for(int x = 0; x < 2; x++)
    213.           _ppts[2*y+x] = new Vector3(x, -y, 0f);
    214.       _ppts[6] = new Vector3(.5f, -1f, 0f);
    215.     }
    216.  
    217.     drawSeg(pt(0), pt(1), test(0));
    218.     drawSeg(pt(1), pt(3), test(1));
    219.     drawSeg(pt(3), pt(5), test(2));
    220.     drawSeg(pt(4), pt(5), test(3));
    221.     drawSeg(pt(2), pt(4), test(4));
    222.     drawSeg(pt(0), pt(2), test(5));
    223.     drawSeg(pt(2), pt(3), test(6));
    224.     drawSeg(pt(1), pt(6), test(7));
    225.     drawSeg(pt(6), pt(4), test(8));
    226.  
    227.     Vector3 pt(int index) => p + scale * _ppts[index] + new Vector3(slant * _ppts[index].y, 0f);
    228.     Color test(int bit) => (color.HasValue && (_pieces[digit] & (1 << bit)) != 0)? color.Value
    229.                                                                                  : default(Color);
    230.   }
    231.  
    232.   static double abs(double n) => Math.Abs(n);
    233.   static float abs(float n) => Math.Abs(n);
    234.   static float floor(float n) => MathF.Floor(n);
    235.   static float frac(float n) => n % 1f;
    236.   static float sin(float rad) => MathF.Sin(rad);
    237.   static float cos(float rad) => MathF.Cos(rad);
    238.   static Vector3 mad(float a, Vector3 b, Vector3 c) => a * b + c;
    239.   static Vector3 lerp(Vector3 a, Vector3 b, float t) => (1f - t) * a + t * b;
    240.   static Vector3 trig(float rad) => new Vector3(cos(rad), sin(rad), 0f);
    241.   static float sqr(float n) => n * n;
    242.   static float mod(float n, float m) => (n %= m) < 0f? m + n : n;
    243.   static int pow(float b, float p) => (int)MathF.Pow(b, p);
    244.   static float sat(float n) => Mathf.Clamp01(n);
    245.  
    246. }
    Edit: fixed the mistake explained in the next post.
     
    Last edited: May 15, 2023
  24. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    Btw I somehow made a (small) error in the following lines
    Code (csharp):
    1. Debug.Log($"correction: was {measured:F4} now {lapsed:F4}");
    2. setValues(lapsed);
    Both
    lapsed
    should be
    next
    , though it doesn't matter that much.
     
  25. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    For anyone learning about how to do stuff where precision matters, I highly recommend reading Bruce Dawson's blog post Don't Store That in a Float.

    The key bits to get out of there for most use cases for most people are that:
    • You want your game's overall elapsed time to be tracked in a double (as Unity does).
    • You can reliably do most of your moment-to-moment stuff with time deltas stored in a float.
    • Sometimes, the order of operations matters because of when and how precision is lost.
    Yeah, that post covers exactly this, too. To get maximum stability, i.e. no or minimal change in the rate of accumulated error due to precision changes throughout your range of values:
     
  26. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    Honestly? Probably, depending on the use cases. You've said that you want it to match external clock time "to the best accuracy possible", but then you also said "I don't need ms precision".

    Unless my players have some reason to compare it to actual real-time over significant periods, I'd probably use something like:
    Code (csharp):
    1. double clockEffectiveTimeThisFrame = // Probably doesn't need to be a double, but don't lose precision until we need to.
    2.     Time.timeAsDouble // The double provided by Unity.
    3.     - timeClockStartedInTheGame // A double you store from Time.timeAsDouble when you start the clock in the game.
    4.     + clockTimeOffset; // Only if needed. Inspector variable which sets the clock's initial value if not zero.
    5.  
    That's assuming that your game clock should pause when your game does, and so on. If time should march on when the game is paused I'd just use an external time source to do the same. Yes, as you've said, external time sources tend to be "expensive" to access, but you only need to access it once per frame, which is a non-issue. After that everything is standard arithmetic.
     
    orionsyndrome likes this.
  27. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,116
    @angrypenguin
    Time.timeAsDouble is more than ample. I honestly forgot about it, but as a bonus I re-confirmed that it is indeed the 64-bit ground-truth maintained by Unity. But then again, the example I've provided (in the later versions) actually accumulates deltas in the 32-bit float, but registers the minutes, then reconstructs the time on inquiry. This solves issues with precision -- it's surprisingly stable for any sort of in-game time-keeping, but as I said it's just an experiment. I've tested it for hours while I was afk, but I don't know if it would pass the most rigorous of tests (like days or longer), probably not.