Search Unity

Discussion Free Script - Chronos - For Emulating the cycles of an old console!

Discussion in 'General Discussion' started by KevinConner, Aug 3, 2022.

  1. KevinConner

    KevinConner

    Joined:
    Jan 8, 2022
    Posts:
    11
    Hi Guys!

    I can't figure out how or where to upload this to the unity asset store, and not sure where to post this, but I wanted to give something back to the Unity Community.

    It's a script, a very simple script, which does a surface (non intensive) timing emulation of classic consoles of the late 70s, and early 90s.

    I call it Chronos but well it can be called anything you want.

    I'll just post my script in full here, and if anyone has any questions feel free to ask. Please remember this is a free script, and it is intended to be simple for a few reasons. I did try a few more complex variations that tended to present "issues" with Unity. As a result this is the least interfering version I could come up with for everyone to enjoy!

    How to use this code: Implement a condition statement that suspends the functions you want to suspend during the horizontal blank cycle ( don't be doing things when hertzCounter == 0, or maybe, do only certain things during that .001 second period ). Easiest place to suspend or enable functions is within FixedUpdate(). Keep that in mind - this code only specifically works with FixedUpdate() cycles. You can change the values of the hertzLimit to properly reflect the hertz cycle you're trying to emulate.

    hertzLimit = 30; for example, is for use when trying to emulate 90% of all the 8bit games, and a large number of 16b games out there running on a NTSC system. If you're doing pal, then you'd want 25 and 50. Don't forget to adjust the Time.fixedDeltaTime values as well. Have fun with this site to figure out what the Pal Video Timing really is: http://martin.hinner.info/vga/pal.html ;)


    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. // This code is emulating the hertz cycle frequencies of the retro games :D
    4.  
    5. // Description of the Emulation
    6.  
    7. // CRTs never operated at 60 frames per second.  They operated at 60 scan sweeps/cycles
    8. // plus 2 horizontal blank scan sweeps/cycles per second.
    9. // In the early age of Videogames, there were very few videogames which bothered to
    10. // prevent people from inputting commands during those two blank horizontal cycles.
    11. // See, what would happen is the actual graphics would stop updating, movement wouldn't
    12. // take place, but people could still input commands which could affect non-graphical gameplay,
    13. // as a result, some games glitched out, and people could - say - shift a car from 1st to 2nd.
    14. // Basically it's cheating to abuse this function.  I am emulating that experience here.
    15. // Most 8 bit systems operated on a 30 frame per second cycle +2 blank cycles (59.94 / 2),
    16. // that's because they had to process 2 scan sweeps per singular frame of animation.
    17. // The T.V. would shoot one sweep/cycle of even numbered lines, followed by another sweep/cycle
    18. // of odd numbered lines, plus 2 horizontal blank sweeps/cycles just to get 1 frame of animation.
    19. // This had to be done in order to make the image appear taller than it actually was, by
    20. // shooting the same line twice.  If they didn't, the image would look like a Venetian Blind.
    21. // So I have a 29.97 hertz cycle as an option, alongside a 59.94 hertz cycle.
    22. // Now, during the horizontal blanks, there was NO image information was transmitted to the CRTs.
    23. // they were called horizontal blanks specifically because the CRTs needed to re adjust and clear
    24. // whatever information they had in order to change the image they were shooting at the screen.
    25. // Well, I tried a few methods to give this feel and unique experience to those who use Unity,
    26. // but I ran into three problems.
    27. // First of all, Unity doesn't operate on a Rainbow Quartz Crystal.
    28. // Secondly, the main camera is the only thing that shuts visuals off.
    29. // So because Unity is a real bitch with timing, it's almost impossible to make it 100% accurate
    30. // for 2 blank cycles.  In addition, Unity doesn't handle processes within .0005 very well.  Why?
    31. // Unity is a big program, it runs a lot of things.  So unlike a Rainbow Quartz Crystal, it
    32. // just won't process things reliably during that time frame.  So I combined the two blank cycles
    33. // into a singular "faux" blank frame/cycle, which is the closest reliable emulation I can get.
    34. // Finally, the third problem - when a CRT shuts its lasers off, your eyes still have that image
    35. // burned into your retina for a little bit longer.  The effect on LCD screens, however.. just
    36. // isn't right in appearance.  So I just killed the idea of shutting off all visuals for 0.001s.
    37. // It feels clunky.  I mean you can enable main camera controls, but it just doesn't feel right.
    38. // The flicker is all off.  It's either too long or it's too short to be visibly present
    39. // (and if it's not visible, why bother?). (Camera.main.enable = true/false;)
    40. // The important aspect of this code, however, is the emulation of .001 seconds where game
    41. // logic functions require a real fast hand to implement, while visuals are suspended.
    42. // This code is as close as you can get to solid horizontal blank input in this particular engine.
    43.  
    44. // Much help and thanks to GameDev.TV's discord, where I found a ton of help from several
    45. // individuals in understanding how fixedDeltaTime and TimeSteps worked with FixedUpdate()
    46. // The most helpful individual goes by the handle Raistael.
    47. // Incidentally, they all called me crazy and insane for even wanting to do this.  WUAHAHHAHA
    48.  
    49. //*********************************************************
    50. // Table of Contents
    51. //
    52. // I.a. Variables for the Cycles
    53. //      contains:
    54. //      public static int hertzLimit, where you set the hertz emulation
    55. //      public static int hertzCounter, how you determine which cycle you're on
    56. //
    57. // I.a. Variables for the Game
    58. //
    59. // II.  Awake
    60. //
    61. // III. FixedUpdate
    62. //
    63. // IV.  HValuesForfixedDeltaTime()
    64. //      this is where the absolute values for hertz emulation
    65. //      are directly set to Time.fixedDeltaTime
    66. //*********************************************************
    67.  
    68.  
    69. public class Chronos : MonoBehaviour
    70. {
    71.  
    72. //*********************************************************
    73. // I.a. Variables for the Cycles
    74. //*********************************************************
    75.  
    76.     [Header("Emulating the Time Cycles")]
    77.     public static int hertzLimit = 30;
    78.     // if the option 30 herts is selected, then hertzLimit = 30.  This is a temporary value
    79.     // to be changed for the appropriate game.  As of now, almost all the games using this
    80.     // code are 8 bit games, which means 30 is a good default value.
    81.    
    82.     public static int hertzCounter; // if over 0 then it's ok to run processes.
    83.  
    84. //*********************************************************
    85. // I.a. Variables for the Game
    86. //*********************************************************
    87.  
    88.     public static int gameLives = 0;
    89.  
    90.     public static int gameContinues = 0;
    91.  
    92.     public static int gameHealth = 0;
    93.  
    94.     public static float speed = 3.0f;
    95.  
    96. //*********************************************************
    97. // II.  Awake
    98. //*********************************************************
    99.  
    100.     void Awake()
    101.     {
    102.        
    103.         int numGameSessions = FindObjectsOfType<Chronos>().Length;
    104.         // the above code counts the number of Chronos which exist
    105.  
    106.         if (numGameSessions > 1)
    107.             {
    108.                 Destroy(gameObject);
    109.             }
    110.             // the above code orders Zeus to kill the duplicate Chronos.
    111.         else
    112.             {
    113.                 DontDestroyOnLoad(gameObject);
    114.             }
    115.         // the above code makes sure the original Chronos survives between scenes.
    116.  
    117.         HValuesForfixedDeltaTime(); // section IV.
    118.     }
    119.  
    120.  
    121. //*********************************************************
    122. // III. FixedUpdate
    123. //*********************************************************
    124.  
    125.     void FixedUpdate()
    126.     {
    127.        
    128.         if (hertzCounter == 0)
    129.             {
    130.                 hertzCounter += 1;
    131.  
    132.                 HValuesForfixedDeltaTime(); // section IV. Resetting the proper emulated timing.
    133.             }
    134.         else if (hertzCounter != hertzLimit)
    135.             {
    136.                 hertzCounter +=1;
    137.             }
    138.         else if (hertzCounter == hertzLimit)
    139.             {
    140.                 Time.fixedDeltaTime = 0.001f; // emulating the 2 horizontal blank cycles.
    141.                 hertzCounter = 0;
    142.             }
    143.        
    144.  
    145.     }
    146.  
    147.  
    148. //*********************************************************
    149. // IV.  HValuesForfixedDeltaTime()
    150. //
    151. //      This is where the fixedDeltaTime values are set for
    152. //      the hertz cycles chosen to be emulated.
    153. //      This is how the FixedUpdate cycles are limited.
    154. //      Numerous tests have shown that any value below
    155. //      0.001 is unreliable.  That includes infinite values
    156. //      such as those generated by fractions.
    157. //*********************************************************
    158.  
    159.     public void HValuesForfixedDeltaTime()
    160.         {
    161.         if (hertzLimit == 30)
    162.             {
    163.             Time.fixedDeltaTime = 0.0333f;
    164.             }
    165.         else if (hertzLimit == 60)
    166.             {
    167.             Time.fixedDeltaTime = 0.01665f;
    168.             }
    169.         }
    170. }
    171.  
    172.  
    173.  
    174.  
    175.  
     
    CodeSmile and OCASM like this.
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,978
    Errm, since when do CRTs have lasers? :D
    They're cathode rays (electron beams), hence the name cathode ray tube = CRT.

    I hope not! :p

    It's in fact a brain thing. Between the eyes and the brain there's a lot of filtering going on which means the brain processes just a couple bits of data and interpolates the rest. The eyes themselves process visuals at a higher frequency than the brain. We are, so to speak, CPU limited. :D

    My thoughts exactly. :p
    But I admire the dedication.

    Lastly: I suppose all of this breaks when a user tweaks the FixedUpdateTime in Project Settings, right?

    PS: would be nice to see a video with the effect (with and without for comparison). Perhaps using one of the free platformer templates on the store.
     
  3. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,778
    LOL, these comments made my day:D
     
  4. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,571
    FixedUpdate does not run at regular intervals and is not a precise timer. It plays catchup with real time, and can fire in bursts.

    TV refresh rates were 60 Hz for NTSC and 50 Hz for PAL. However, they were rendering half-frames.
     
  5. KevinConner

    KevinConner

    Joined:
    Jan 8, 2022
    Posts:
    11
    HI!:D

    I didn't know anyone replied.

    I like to call them lasers because they lase!!!

    XD

    And your retinas are BURNED I SAY BURNED!

    I can take posts in good humor ;) I am glad I made you smile :)

    ok so yes, people can change FixedUpdateTime as needed, I just wanted to show the basic core of the functionality. A friend on Discord made a video showing the flicker effect. After employing the flicker I decided it was ore of a headache than it was additive to the gaming experience. So, I just removed the main camera on/off function on my own projects. HOWEVER, by ensuring you wrap all of your game's functionality within the hertsCounter value like this:

    if (hertzCounter > 0)
    {
    do the game code here
    }
    else
    { just nothing here or don't even include else }

    Then you can replicate at least the animation cycles as they were on the CRTs. The feel is definitely different. I could immediately tell the reaction time for my inputs felt more "retro" than any game that just used normal FPS without any sort of break. 0.01665 is noticeable.
     
  6. KevinConner

    KevinConner

    Joined:
    Jan 8, 2022
    Posts:
    11
    you are incorrect.

    Normal Update fires in bursts.

    I've timed Fixed Update with debug readouts and a counter and not once has it ever fired in bursts.
     
  7. You're very wrong. Please check the actual documentation:
    https://docs.unity3d.com/Manual/ExecutionOrder.html

    Just because you personally failed to provide the proper environment when the
    FixedUpdate
    fires in bursts, it doesn't mean it doesn't happen. You just did it wrong.
     
  8. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,571
    I'm one hundred percent correct. Also, instead trying to "time" things, read the docs. Docs determine what is t rue, and not your experiments.
    https://docs.unity3d.com/Manual/ExecutionOrder.html

    FixedUpdate will run in bursts, if it lags behind normal update. For example, If your FPS drops to ten, and your physics fps is fifty, before each Update t here will be five fixedupdates and they will not be evenly spaced. They'll run imediately one after another.

    FixedUpdate is not a timer, you see. It is not evenly fired. It simply advances time of physical world each time it is called, to catch up to real world time. If running it once is not enough, it will be run second time, and so on.
     
  9. neginfinity

    neginfinity

    Joined:
    Jan 27, 2013
    Posts:
    13,571
    Here's a test:

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Diagnostics;
    4. using System.Threading;
    5.  
    6. public class PhysicsUpdateDebug: MonoBehaviour{
    7.     Stopwatch fixedSw = new();
    8.     Stopwatch updateSw = new();
    9.     void FixedUpdate(){
    10.         fixedSw.Stop();
    11.         UnityEngine.Debug.Log($"FixedUpdate: {fixedSw.Elapsed.Milliseconds}");
    12.         fixedSw.Restart();
    13.     }
    14.  
    15.     void Update(){
    16.         Thread.Sleep(200);
    17.         updateSw.Stop();
    18.         UnityEngine.Debug.Log($"Update: {updateSw.Elapsed.Milliseconds}");
    19.         updateSw.Restart();
    20.     }
    21. }
    22.  
    23.  
    Here's its output:

    Code (csharp):
    1.  
    2. Update: 208
    3. FixedUpdate: 207
    4. FixedUpdate: 0
    5. FixedUpdate: 0
    6. FixedUpdate: 0
    7. FixedUpdate: 0
    8. FixedUpdate: 0
    9. FixedUpdate: 0
    10. FixedUpdate: 0
    11. FixedUpdate: 0
    12. FixedUpdate: 0
    13. FixedUpdate: 0
    14. Update: 208
    15. FixedUpdate: 211
    16.  
    Here you go. Those are the bursts I spoke of.
     
    MadeFromPolygons likes this.
  10. angrypenguin

    angrypenguin

    Joined:
    Dec 29, 2011
    Posts:
    15,620
    I wish that were true! :p The docs are pretty good, but occasionally they're missing stuff, and occasionally there are bugs. Having said that, in this case the docs are correct.

    To clarify for Kevin, they will have an even, fixed timestep in game time, but will be unevenly spaced in real world time. That's critical context, and it's also critical to know that Time.time gives you game time, not real world time. It only updates between frames, and represents the game time at the start of the current frame. Otherwise, things could not animate or simulate in a consistent manner.
     
    Last edited: Jun 12, 2023
    Neonlyte and Lurking-Ninja like this.