Unity: Пауза без паузы

Из-за особенностей работы timeScale могут возникнуть некоторые сложности в реализации такого заклинания как Time Stop (пример реализации заклинания здесь):

Там останавливается все, кроме часового механизма в левом нижнем углу и анимации персонажа, который скастовал Остановку времени.

Долго я пытался найти что-то, что помогло бы реализовать этот эффект, наконец нашел статью…

Далее идет просто перевод статьи "Pausing Without Pausing".

 

Проще всего реализовать паузу в игре — это установить Time.timeScale = 0. Пока переменная timeScale равна нулю, в ваших скриптах продолжают вызываться методы Update, но переменная Time.deltaTime всегда будет возвращать значение 0. Это хорошо работает пока есть необходимость остановить все действия на экране, но также вносит жесткие ограничения для анимации элементов меню или оверлеев, т.к. Time.timeScale = 0 также останавливает анимацию и системы частиц.
С этими ограничениями мы впервые столкнулись когда пытались реализовать карту мира в игре Lovers. Когда игрок подходит к пункту с картой корабля, мы отображаем оверлей с картой текущего уровня. Поскольку карта загораживает корабль и, тем самым препятствует нормальной игре, нам нужно остановить игру, пока открыта карта. Однако, полностью статический экран мог с трудом передать информацию (да и выглядела бы такая карта скучно). Поэтому для достижения наших целей нам нужно было как-то иначе отслеживать сколько прошло времени с последнего обновления цикла.
Получается, что Time.realtimeSinceStartup идеальный механизм для этого. Как говорит его название, Time.realtimeSinceStartup использует системные часы для того, чтобы отслеживать сколько прошло времени с начала игры, независимо от того, какое значение timeScale мы установили. Зная предыдущее значение Time.realtimeSinceStartup в последнем вызове Update, мы можем вычислить приблизительное значение deltaTime с последнего фрейма:

TimeScaleIndependentUpdate.cs
using UnityEngine;
using System.Collections;

public class TimeScaleIndependentUpdate : MonoBehaviour
{
  //inspector fields
  public bool pauseWhenGameIsPaused = true;

  //private fields
  float previousTimeSinceStartup;

  protected virtual void Awake()
  {
    previousTimeSinceStartup = Time.realtimeSinceStartup;
  }

  protected virtual void Update()
  {
    float realtimeSinceStartup = Time.realtimeSinceStartup;
    deltaTime = realtimeSinceStartup - previousTimeSinceStartup;
    previousTimeSinceStartup = realtimeSinceStartup;

    //It is possible (especially if this script is attached to an object that is 
    //created when the scene is loaded) that the calculated delta time is 
    //less than zero.  In that case, discard this update.
    if(deltaTime < 0)
      {
      Debug.LogWarning("Delta time less than zero, discarding (delta time was " 
      + deltaTime + ")");

      deltaTime = 0;
      }

    //NOTE: You will want to change "GameStateManager.SharedInstance.Paused()" 
    //to whatever you use to check if the game has been paused by the user
    if(pauseWhenGameIsPaused && GameStateManager.SharedInstance.Paused())
      {
        deltaTime = 0;
      }
  }

  public IEnumerator TimeScaleIndependentWaitForSeconds(float seconds)
  {
    float elapsedTime = 0;
    while(elapsedTime < seconds)
    {
      yield return null;
      elapsedTime += deltaTime;
    }
  }

  #region Property methods

  public float deltaTime { get; private set; }

  #endregion
}

Однако, отдельно этого скрипта будет недостаточно, поскольку мы хотим использовать компонент Анимации Unity для движения динамических элементов карты. Чтобы это сделать, мы создали подкласс класса TimeScaleIndependentUpdate, который "вручную" запускает анимацию:

TimeScaleIndependentAnimation.cs
using UnityEngine;
using System.Collections;

public class TimeScaleIndependentAnimation : TimeScaleIndependentUpdate
{
  //inspector fields
  public bool playOnStart;
  public string playOnStartStateName;

  //private fields
  AnimationState currentState;
  System.Action currentCompletionHandler;
  float elapsedTime;
  bool playing;

  void Start()
  {
    if(playOnStart)
      {
        Play(playOnStartStateName);
      }
  }

  protected override void Update()
  {
    base.Update();

    if(playing)
      {
      elapsedTime += deltaTime;
      currentState.normalizedTime = elapsedTime / currentState.length;

      if(elapsedTime >= currentState.length)
        {
          playing = false;

          if(currentState.wrapMode == WrapMode.Loop)
            {
            Play(currentState.name);
            }
          else
            {
            if(currentCompletionHandler != null)
              {
              currentCompletionHandler();
              }
            }
         }
      }
  }

  public void Play(string stateName, System.Action completionHandler = null)
  {
    elapsedTime = 0f;
    currentState = animation[stateName];
    currentState.normalizedTime = 0;
    currentState.enabled = true;
    currentState.weight = 1;
    currentCompletionHandler = completionHandler;
    playing = true;
  }
}

Используя свойство normalizedTime класса AnimationState и посчитанное нами значение delta time, мы подчистили анимацию в каждом вызове Update. Теперь, все что нам нужно сделать, это присоединить этот скрипт к тому GameObject, на который не должна действовать остановка времени при значении Time.timeScale = 0:
Unity: Пауза без паузы
(Оригинальную гифку пришлось уменьшить, чтобы обойти ограничение на аплоад.)
Unity: Пауза без паузы

Как видно на картинке выше, игра останавливается, когда появляется карта, но при этом значки на карте продолжают анимацию.

Анимацию для системы частиц во время паузы игры также можно реализовать. Класс ParticleSystem содержит удобный для этого метод Simulate, который, подобно компоненту анимации (Animation), позволяет нам также "вручную" запустить анимацию частиц. Все что для этого необходимо, это простой подкласс класса TimeScaleIndependentUpdate:

TimeScaleIndependentParticleSystem.cs
using UnityEngine;
using System.Collections;

public class TimeScaleIndependentParticleSystem : TimeScaleIndependentUpdate
{
	protected override void Update()
	{
		base.Update();

		particleSystem.Simulate(deltaTime, true, false);
	}
}

Мы можем комбинировать эти три скрипта для создания довольно сложных эффектов. В игре "Lovers in a Dangerous Spacetime", как только игрок собирает достаточное количество друзей, чтобы открыть варп-тунель, мы проигрываем небольшую заставку, при значении Time.timeScale = 0:
Unity: Пауза без паузы

Этот эффект в основном зависит от метода TimeScaleIndependentWaitForSeconds класса TimeScaleIndependentUpdate, который является примерной реализацией метода встроенного класса WaitForSeconds и это очень удобно применять для создания подпрограмм.

От себя хочу добавить, что если нужно реализовать иммунитет к остановке времени в меню, то для компонента Animator достаточно изменить параметр Update Mode
с Normal:
Unity: Пауза без паузы
на Unscaled Time:
Unity: Пауза без паузы