Time.timeScale 是 Unity 的时间缩放变量。如果将此设置为0,那么 Time.time 将停止,并且 PhysicsAnimator 在默认情况下也将停止。所以,将 Time.timeScale 设置为0,通常为了做暂停相关的东西。

但是,有时候希望即使将 TimeScale 设置为 0,有些东西也能继续运行,比如在写 Shader 时,经常会用到 _Time 属性,但是它会受到 Time.timeScale 的影响。不过有以下几种方式可以做到不受影响。

方法一

private void Update()
{
   material.SetFloat("_UnscaledTime", Time.unscaledTime);
}

这种方式比较直接,但是也有弊端,如果要设置的 Shader 很多,会增加很多 MonoBehaviour。

方法二

Unity 可以设置 Shader 全局变量,即材质之间共享属性。

private void Update()
{
   Shader.SetGlobalFloat("_UnscaledTime", Time.unscaledTime);
}

这样场景中只需要一个做这件事情的 MonoBehaviour 的脚本即可。这个方法可以覆盖大多数情况。

方法三

如果场景中不想放任何脚本,还有一个更强大的方法,那就是使用 PlayerLoop 来独立于 MonoBehaviour 更新全局属性。

首先引入一个工具类 PlayerLoopModifier.cs,代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.LowLevel;

public class PlayerLoopModifier : IDisposable
{
    private PlayerLoopSystem root;

    public PlayerLoopModifier()
    {
        root = PlayerLoop.GetCurrentPlayerLoop();
    }

    public bool InsertAfter<T>(PlayerLoopSystem subSystem) where T : struct
    {
        return InsertAfter<T>(subSystem, ref root);
    }

    public bool InsertBefore<T>(PlayerLoopSystem subSystem) where T : struct
    {
        return InsertBefore<T>(subSystem, ref root);
    }

    public bool InsertIn<T>(PlayerLoopSystem subSystem) where T : struct
    {
        return InsertIn<T>(subSystem, ref root);
    }

    public void Dispose()
    {
        PlayerLoop.SetPlayerLoop(root);
    }

    private static bool InsertAfter<T>(PlayerLoopSystem subSystem, ref PlayerLoopSystem parentSystem)
        where T : struct
    {
        var subSystems = parentSystem.subSystemList?.ToList();
        if (subSystems == default) return false;

        bool found = false;
        for (int i = 0; i < subSystems.Count; i++)
        {
            var s = subSystems[i];
            if (s.type == typeof(T))
            {
                found = true;
                subSystems.Insert(i + 1, subSystem);
                break;
            }
        }

        if (!found)
        {
            for (int i = 0; i < subSystems.Count; i++)
            {
                var s = subSystems[i];
                if (InsertAfter<T>(subSystem, ref s))
                {
                    found = true;
                    subSystems[i] = s;
                    break;
                }
            }
        }

        if (found)
            parentSystem.subSystemList = subSystems.ToArray();

        return found;
    }

    private static bool InsertBefore<T>(PlayerLoopSystem subSystem, ref PlayerLoopSystem parentSystem)
        where T : struct
    {
        var subSystems = parentSystem.subSystemList?.ToList();
        if (subSystems == default) return false;

        bool found = false;
        for (int i = 0; i < subSystems.Count; i++)
        {
            var s = subSystems[i];
            if (s.type == typeof(T))
            {
                found = true;
                subSystems.Insert(i, subSystem);
                break;
            }
        }

        if (!found)
        {
            for (int i = 0; i < subSystems.Count; i++)
            {
                var s = subSystems[i];
                if (InsertBefore<T>(subSystem, ref s))
                {
                    found = true;
                    subSystems[i] = s;
                    break;
                }
            }
        }

        if (found)
            parentSystem.subSystemList = subSystems.ToArray();

        return found;
    }

    private static bool InsertIn<T>(PlayerLoopSystem subSystem, ref PlayerLoopSystem parentSystem)
        where T : struct
    {
        var subSystems = parentSystem.subSystemList?.ToList();
        if (subSystems == default) subSystems = new List<PlayerLoopSystem>();

        bool found = false;
        if (parentSystem.type == typeof(T))
        {
            subSystems.Insert(0, subSystem);
            found = true;
        }
        else
        {
            for (int i = 0; i < subSystems.Count; i++)
            {
                var s = subSystems[i];
                if (InsertIn<T>(subSystem, ref s))
                {
                    found = true;
                    subSystems[i] = s;
                    break;
                }
            }
        }

        if (found)
            parentSystem.subSystemList = subSystems.ToArray();

        return found;
    }
}

然后开始开始编写控制代码 UnscaledShaderTime.cs

using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;

public static class UnscaledShaderTime
{
    [RuntimeInitializeOnLoadMethod]
    private static void Register()
    {
        using (var modifier = new PlayerLoopModifier())
        {
            modifier.InsertAfter<Update.ScriptRunBehaviourUpdate>(new PlayerLoopSystem()
            {
                updateDelegate = OnUpdate,
                type = typeof(UnscaledShaderTimeUpdate)
            });
        }
    }

    private static void OnUpdate()
    {
        Shader.SetGlobalFloat("_UnscaledTime", Time.unscaledTime);
    }

    public struct UnscaledShaderTimeUpdate
    {
    }
}

RuntimeInitializeOnLoadMethod 属性,是 Unity 的东西,不管是不是 MonoBehaviour,都会在游戏启动的时候执行这个方法。