Aller au contenu principal

Time, timeScale et deltaTime dans l’éditeur

Le temps dans l’éditeur

Quand on crée des outils d’éditeur pour ses jeux, on a parfois besoin de timers. Malheureusement, Unity ne fournit aucune gestion du temps en mode édition.

J’ai créé une classe TimeEditor qui reproduit le comportement de la classe Time en mode édition. Elle fournit un deltaTime et un timeScale, comme en mode Play. Elle fonctionne dans l’éditeur, en mode Play, et bien sûr dans les builds.

Utilisation​

Dans votre projet, remplacez chaque Time.time par TimeEditor.time, et c’est tout !

Le script TimeEditor sur un GameObject vide dans la hiérarchie

Placez simplement le script sur un GameObject vide dans la hiérarchie de la scène où vous utilisez le TimeEditor.

Ensuite, à tout moment dans un script (d’éditeur ou classique), appelez TimeEditor.time pour obtenir le temps qui dépend du timeScale ! Vous pouvez donc aussi changer le timeScale dans l’éditeur avec TimeEditor.timeScale = 0.5f. Le timeScale peut prendre n’importe quelle valeur, mais en mode Play un timeScale négatif n’est pas autorisé.

Un objet qui bouge en mode édition

Par exemple, pour déplacer un joueur à chaque Update, j’utilise en général quelque chose comme ceci :

_player.Move(speed * Time.deltaTime);

Si je veux que mon objet bouge aussi dans l’éditeur, il me suffit de mettre [ExecuteInEditMode] juste avant ma classe, et d’utiliser la classe TimeEditor :

_player.Move(speed * TimeEditor.deltaTime);

Ordre d’exécution des scripts​

attention

Dans le Script Execution Order, la classe TimeEditor doit s’exécuter avant le Default Time.

Allez dans Edit > Project Settings et ajoutez-la à la liste de l’onglet Script Execution Order. N’oubliez pas de cliquer sur Apply.

TimeEditor dans les réglages Script Execution Order

attention

Le script TimeEditor utilise la classe singleton SingletonMono<T>. Elle n’est pas indispensable au fonctionnement de cette classe, mais c’est pratique. Si vous ne l’utilisez pas, remplacez SingletonMono<TimeEditor> par MonoBehaviour ; sinon, téléchargez aussi le fichier SingletonMono.cs.

Scripts​

TimeEditor.cs
TimeEditor.cs
/// <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
SingletonMono.cs
/// <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;
}
}

Comment ça marche​

D’abord, [ExecuteInEditMode] permet d’appeler la méthode Update d’un MonoBehaviour même en mode édition.

Ensuite, il faut dire à Unity de se mettre à jour même si aucun événement n’est déclenché dans la scène. Sans cela, Unity ne se met pas à jour à chaque frame, contrairement à ce qu’on pourrait croire. Pour y arriver, il faut appeler EditorApplication.QueuePlayerLoopUpdate(); dans la boucle EditorApplication.update.

Pour cela, on abonne notre fonction EditorUpdate() à la boucle interne de l’éditeur d’Unity : EditorApplication.update += EditorUpdate;

Il nous faut ensuite notre propre deltaTime si on veut un timeScale dans l’éditeur. À chaque frame, on sauvegarde donc le deltaTime (l’écart en secondes entre la frame précédente et l’actuelle), et on le multiplie par le timeScale :

private void AddToTime()
{
_currentDeltaTime = (Time.realtimeSinceStartup - _editModeLastUpdate) * timeScale;
_currentDeltaTime = Mathf.Min(_currentDeltaTime, _maxSizeDeltaTime); //if fps drop below 30fps, go into slow motion

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

SetSmoothDeltaTimes();
}

Vous avez alors une classe TimeEditor qui se comporte comme le Time d’Unity. Vous pouvez créer des timers dans vos outils d’éditeur, et même faire des ralentis en changeant le timeScale. Pour l’essayer, jetez un œil à mes classes de chrono et de cooldown, qui utilisent cette classe TimeEditor.

info

J’ai ajouté un algorithme smoothDeltaTime qui prend les 10 derniers deltaTime sauvegardés (variable _maxAmountDeltaTimeSaved) et renvoie leur moyenne. Je le recommande vivement pour amortir une caméra rapide qui suit une cible rapide avec un framerate irrégulier.

attention

Le deltaTime est volontairement plafonné à _maxSizeDeltaTime, réglé à 0.033f (1/30). Autrement dit, si votre jeu descend sous les 30 fps, il commence à passer au ralenti. Si vous ne voulez pas de ce comportement, augmentez cette variable, à 0,4 seconde par exemple.

Ce garde-fou évite un énorme deltaTime lors d’un gros pic de framerate. Il empêche donc le joueur, ou tout autre objet déplacé selon le deltaTime, de se téléporter après un pic.

Fichiers source​

Voir aussi​