Time, TimeScale & DeltaTime in Editor

When you are building some editor tools for your games, you sometimes need to do some timer. Unfortunatly Unity doesn't provide any time managment solution in editor mode.

I have created a TimeEditor Class which reflects the behaviour of the Time class in editor mode. It allows us to have a deltaTime and a timeScale like in play mode. It works in editor, in play mode and in Build of course.

How to use:

In your project, change every occurence of
Time.time
by
TimeEditor.time
and that's it !

Simply put the script in an empty gameObject in the hierarchy in the scene where you use the TimeEditor.

Then at any time in a script (in an editor Script, or in a normal script), call

EditorTime.time
to have the timeScale Dependent time ! That means you can also change the timeScale in editor by doing
EditorTime.timeScale = 0.5f
. Note that you can set the timeScale to anything you want, but in play mode, a negatif timeScale is not allowed.


For Exemple, when I move a player at each Update, I usually use something like this:
_player.Move(speed * Time.deltaTime);

Now If I want my object to move also in editor, you just have to put
[ExecuteInEditMode]
just before my class, and use the TimeEditor class:
_player.Move(speed * TimeEditor.deltaTime);



The Script Execution Order of the
TimeEditor
class has to be before the default time of unity.

Go to

Edit > Project Settings
and add it to the list in the tab
Script Execution Order
, don't forget to press apply.




The TimeEditor script uses the class Singleton-Patern
SingletonMono<T>
. You do not necessary need it for this class to work, but it is convenient to have it. If you don't use it, replace
SingletonMono<TimeEditor>
by
Monobehaviour
, else download also the file
SingletonMono.cs
.



Scripts:

TimeEditor.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
#endif
using UnityEngine;

/// <summary>
/// Use this class instead of the Time class if you want to use time in editor
/// use TimeEditor.time instead of Time.time will work in editor & in play mode
///
/// use TimeEditor.time instead of Time.time
/// use TimeEditor.timeScale instead of Time.timeScale
/// use TimeEditor.unscaledTime instead of Time.unscaledTime
/// use TimeEditor.deltaTime instead of Time.deltaTime
/// use TimeEditor.unscaledDeltaTime instead of Time.unscaledDeltaTime
/// 
/// In editor, you can set the timeScale to negative to backward time !
/// but warning: doen't work in play mode
/// </summary>
[ExecuteInEditMode]
public class TimeEditor : SingletonMono<TimeEditor>
{
    private static float _editModeLastUpdate;   //the last time the controller was updated while in Edit Mode
    private static float _timeScale = 1f;       //current timeScale in both editor & play mode
    private static float _currentTime;          //current time timeScale-dependent (like Time.time but in editMode)
    private static float _currentTimeIndependentTimeScale;  //current time timeScale-independent (like Time.unscaledTime but in editMode)
    private static float _currentDeltaTime;     //current deltaTime timeScale-dependent (like Time.deltaTime but in editMode)
    private static float _currentUnscaledDeltaTime; //current deltaTime timeScale-independent (like Time.unscaledDeltaTime but in editMode)
    private static float _smoothDeltaTime;      //current deltatime, smoothed X time
    private static float _smoothUnscaledDeltaTime;      //current deltatime, smoothed X time

    private Queue<float> _olfDeltaTimes = new Queue<float>();         //list of all previousDeltaTime;
    private Queue<float> _olfUnscaledDeltaTimes = new Queue<float>();         //list of all previousDeltaTime;

    private readonly int _maxAmountDeltaTimeSaved = 10; //amount of previous deltaTime to save for the smoothDeltaTime algorythm

    //don't let the deltaTime get higher than 1/30: if you have low fps (bellow 30),
    //the game start to go in slow motion.
    //if you don't want this behavior, put the value at 0.4 for exemple as precaution, we don't
    //want the player, after a huge spike of 5 seconds, to travel walls !
    private readonly float _maxSizeDeltaTime = 0.033f;  

    #region public properties

    /// <summary>
    /// get the current timeScale
    /// </summary>
    public static float timeScale
    {
        get
        {
            return (_timeScale);
        }
        set
        {
            if (value != _timeScale)
            {
                _timeScale = value;
                Time.timeScale = Mathf.Max(0, _timeScale);  
            }
        }
    }
    /// <summary>
    /// the time (timeScale dependent) at the begening of this frame (Read only). This is the time in seconds since the start of the game / the editor compilation
    /// </summary>
    public static float time
    {
        get
        {
            return (_currentTime);
        }
    }

    /// <summary>
    /// The timeScale-independant time for this frame (Read Only). This is the time in seconds since the start of the game / the editor compilation
    /// </summary>
    public static float unscaledTime
    {
        get
        {
            return (_currentTimeIndependentTimeScale);
        }
    }

    /// <summary>
    /// The completion time in seconds since the last from (Read Only)
    /// </summary>
    public static float deltaTime
    {
        get
        {
            return (_currentDeltaTime);
        }
    }

    /// <summary>
    /// The timeScale-independent interval in seconds from the last frames to the curren tone
    /// </summary>
    public static float unscaledDeltaTime
    {
        get
        {
            return (_currentUnscaledDeltaTime);
        }
    }

    /// <summary>
    /// The timeScale-dependent smoothed DeltaTime, it's the average of the 'n = 10' previous frames
    /// </summary>
    public static float smoothDeltaTime
    {
        get
        {
            return (_smoothDeltaTime);
        }
    }

    /// <summary>
    /// The timeScale-independent smoothed DeltaTime, it's the average of the 'n = 10' previous frames
    /// </summary>
    public static float smoothUnscaledDeltaTime
    {
        get
        {
            return (_smoothUnscaledDeltaTime);
        }
    }
    #endregion

    #region private functions

    /// <summary>
    /// subscribe to EditorApplication.update to be able to call EditorApplication.QueuePlayerLoopUpdate();
    /// We need to do it because unity doens't update when no even are triggered in the scene.
    /// 
    /// Then, Start the timer, this timer will increase every frame (in play or in editor mode), like the normal Time
    /// </summary>
    private void OnEnable()
    {
#if UNITY_EDITOR
        EditorApplication.update += EditorUpdate;
        //EditorApplication.playmodeStateChanged += HandleOnPlayModeChanged;
#endif
        _editModeLastUpdate = Time.realtimeSinceStartup;
        StartCoolDown();
    }

    protected void OnDisable()
    {
#if UNITY_EDITOR
        EditorApplication.update -= EditorUpdate;
        //EditorApplication.playmodeStateChanged -= HandleOnPlayModeChanged;
#endif
    }

    /// <summary>
    /// at start, we initialize the current time
    /// </summary>
    private void StartCoolDown()
    {
        _currentTime = 0;
        _currentTimeIndependentTimeScale = 0;
        _editModeLastUpdate = Time.realtimeSinceStartup;
    }

    /// <summary>
    /// called every frame, add delta time to the current timer, with or not timeScale
    /// </summary>
    private void AddToTime()
    {
        _currentDeltaTime = (Time.realtimeSinceStartup - _editModeLastUpdate) * timeScale;
        _currentDeltaTime = Mathf.Min(_currentDeltaTime, _maxSizeDeltaTime);    //if fps drop bellow 30fps, go into slow motion

        _currentUnscaledDeltaTime = (Time.realtimeSinceStartup - _editModeLastUpdate);
        _currentTime += _currentDeltaTime;
        _currentTimeIndependentTimeScale += _currentUnscaledDeltaTime;

        SetSmoothDeltaTimes();
    }

    /// <summary>
    /// Set and manage the smoothdeltaTime, by adding the average of the 'n' previous deltaTimes together
    /// </summary>
    private void SetSmoothDeltaTimes()
    {
        float sumOfPreviousDeltaTimes = _olfDeltaTimes.GetSum();
        float sumOfPreviousUnslacedDeltaTime = _olfUnscaledDeltaTimes.GetSum();

        _smoothDeltaTime = (_currentDeltaTime + (sumOfPreviousDeltaTimes)) / (_olfDeltaTimes.Count + 1);
        _smoothUnscaledDeltaTime = (_currentUnscaledDeltaTime + (sumOfPreviousUnslacedDeltaTime)) / (_olfUnscaledDeltaTimes.Count + 1);
        //_smoothDeltaTime = (_currentDeltaTime + (sum of 'n' previous delta times)) / ('n' + 1)

        _olfDeltaTimes.Enqueue(_currentDeltaTime);
        _olfUnscaledDeltaTimes.Enqueue(_currentUnscaledDeltaTime);

        while (_olfDeltaTimes.Count > _maxAmountDeltaTimeSaved)
        {
            _olfDeltaTimes.Dequeue();
        }

        while (_olfUnscaledDeltaTimes.Count > _maxAmountDeltaTimeSaved)
        {
            _olfUnscaledDeltaTimes.Dequeue();
        }
    }

    /// <summary>
    /// called every editurUpdate, tell unity to execute the Update() method even if no event are triggered in the scene
    /// in the scene.
    /// </summary>
#if UNITY_EDITOR
    private void EditorUpdate()
    {
        if (!Application.isPlaying)
        {
            EditorApplication.QueuePlayerLoopUpdate();
        }
    }
#endif

    /// <summary>
    /// called every frame in play and in editor, thanks to EditorApplication.QueuePlayerLoopUpdate();
    /// add to the current time, then save the current time for later.
    /// </summary>
    private void Update()
    {
        AddToTime();
        _editModeLastUpdate = Time.realtimeSinceStartup;
    }
    #endregion
}
SingletonMono.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using UnityEngine;

/// <summary>
/// Be aware this will not prevent a non singleton constructor
///   such as `T myT = new T();`
/// To prevent that, add `protected T () {}` to your singleton class.
/// 
/// As a note, this is made as MonoBehaviour because we need Coroutines.
/// 
/// </summary>
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    private static object _lock = new object();

    public static T Instance
    {
        get
        {
            if (applicationIsQuitting)
            {
                if (Application.isPlaying)
                {
                    Debug.Log("is quitting ?");
                    return null;
                }
                else
                {
                    applicationIsQuitting = false;
                }
            }

            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = (T)FindObjectOfType(typeof(T));

                    if (FindObjectsOfType(typeof(T)).Length > 1)
                    {
                        Debug.LogWarning("[Singleton] Something went really wrong " +
                            " - there should never be more than 1 singleton!" +
                            " Reopening the scene might fix it.");

                        return _instance;
                    }

                    if (_instance == null)
                    {

                    }
                    else
                    {

                    }
                }

                return _instance;
            }
        }
        set
        {
            _instance = value;
        }
    }

    private static bool applicationIsQuitting = false;
    /// <summary>
    /// When Unity quits, it destroys objects in a random order.
    /// In principle, a Singleton is only destroyed when application quits.
    /// If any script calls Instance after it have been destroyed, 
    ///   it will create a buggy ghost object that will stay on the Editor scene
    ///   even after stopping playing the Application. Really bad!
    /// So, this was made to be sure we're not creating that buggy ghost object.
    /// </summary>

    private void OnApplicationQuit()
    {
        applicationIsQuitting = true;
    }
}



How it works:

First,
[ExecuteInEditMode]
allows us to call the update method of an Monobehaviour even in editor mode.

Then, we need to tell unity to update even if no event are fired in the scene. without it, unity don't update every frame as we might instinctively assume. To accomplish this, we need to call
EditorApplication.QueuePlayerLoopUpdate();
in the
EditorApplication.update
loop.

To achieve this, we subscribe our custom function EditorUpdate() to the internal editor loop of unity:
EditorApplication.update += EditorUpdate;


Then we need to have our own deltaTime if we want to have a timeScale in the editor. So every frame we save the deltaTime (it is the time difference in seconds between the last frame and the current frame), and we multiply it by the timeScale:
private void AddToTime()
{
    _currentDeltaTime = (Time.realtimeSinceStartup - _editModeLastUpdate) * timeScale;
    _currentDeltaTime = Mathf.Min(_currentDeltaTime, _maxSizeDeltaTime);    //if fps drop bellow 30fps, go into slow motion

    _currentUnscaledDeltaTime = (Time.realtimeSinceStartup - _editModeLastUpdate);
    _currentTime += _currentDeltaTime;
    _currentTimeIndependentTimeScale += _currentUnscaledDeltaTime;

    SetSmoothDeltaTimes();
}


After that, you have a TimeEditor class with the same behavior of the Time of Unity. You can do custom timers in your editor tools, and even do a slow motion, by changing the timeScale.
To test it, you can see bellow my timer & coolDown class, which use this EditorTime class.
I have added a
smoothDeltaTime
algorythm which take the 10 previous saved deltaTime (variable
_maxAmountDeltaTimeSaved
), and return the mean of them. I highly recomand to use this one if you want to apply damping on an high speed camera which follow an high speed target, and with inconsistant framerate.
The deltaTime is volontary capped to
_maxSizeDeltaTime
, set to 0.033f (1/30). In other words, if your game goes bellow 30fps, the game will start to go into slow-motion. If you don't want that to happend, change the variable to an higher value, like 0.4 seconds for exemple. This security avoid to have a huge deltaTime when you have a huge spiks in your framerate. Therefore it prevent the player, or any other moving object based on deltaTime, to teleport after a huge spike.









See also:

Chrono and coolDown

For all your timer and Cooldown in unity. No more Invoke(), Coroutine() or testing the time in hard way inside Update().


EditorOnly Components and Actions when building

This article show how to remove components you don't want in build while keeping them in gameObjects, and how to execute actions when you are building your application