3.UniTask延时操作

3.延时操作


3.1 知识点

UniTask 延时操作详解

在 Unity 开发中,处理异步操作时常需要使用延时功能。UniTask 提供了一套高效且无额外内存分配的延时 API,包括 UniTask.DelayUniTask.DelayFrameUniTask.NextFrameUniTask.YieldUniTask.WaitForEndOfFrame 等。本文将通过具体示例,详细介绍这些延时操作的用法和注意事项。

UniTask.Delay:时间延迟

UniTask.Delay 用于在指定时间后继续执行后续代码。它可以接受毫秒数或 TimeSpan 作为延时时长,并可选择是否忽略时间缩放(Time.timeScale)的影响。

using Cysharp.Threading.Tasks;
using UnityEngine;
using System;

public class DelayExample : MonoBehaviour
{
    async void Start()
    {
        Debug.Log($"[Delay] 开始测试 - 当前时间: {Time.time}");

        // 延时 1 秒(受 Time.timeScale 影响)
        await UniTask.Delay(1000);
        Debug.Log($"[Delay] 普通延时1秒后 - 当前时间: {Time.time}");

        // 使用 TimeSpan 延时 1 秒
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        Debug.Log($"[Delay] TimeSpan延时1秒后 - 当前时间: {Time.time}");

        // 忽略时间缩放的延时
        Time.timeScale = 0.5f;
        Debug.Log($"[Delay] 设置Time.timeScale = 0.5");
        await UniTask.Delay(1000, ignoreTimeScale: true);
        Debug.Log($"[Delay] 无视缩放的1秒延时后 - 当前时间: {Time.time}");

        Time.timeScale = 1f; // 恢复正常时间缩放
        Debug.Log($"[Delay] 恢复Time.timeScale = 1");
    }
}

在上述示例中,UniTask.Delay 的第三个参数 ignoreTimeScale 设置为 true 时,延时将不受 Time.timeScale 的影响。

UniTask.DelayFrame:帧延迟

UniTask.DelayFrame 用于在指定帧数后继续执行后续代码。

using Cysharp.Threading.Tasks;
using UnityEngine;

public class DelayFrameExample : MonoBehaviour
{
    async void Start()
    {
        int startFrame = Time.frameCount;
        Debug.Log($"[帧延时] 开始测试 - 当前帧数: {startFrame}");

        // 延时 10 帧
        await UniTask.DelayFrame(10);
        Debug.Log($"[帧延时] 10帧后的帧数: {Time.frameCount}");
    }
}

此方法在需要等待特定帧数时非常有用,例如在动画或物理模拟中。

UniTask.NextFrame:等待下一帧

UniTask.NextFrame 用于等待到下一帧再继续执行代码。

using Cysharp.Threading.Tasks;
using UnityEngine;

public class NextFrameExample : MonoBehaviour
{
    async void Start()
    {
        int startFrame = Time.frameCount;
        Debug.Log($"[下一帧] 开始帧数: {startFrame}");

        // 等待下一帧
        await UniTask.NextFrame();
        Debug.Log($"[下一帧] 下一帧的帧数: {Time.frameCount}");
    }
}

这在需要确保某些操作在下一帧执行时非常有用。

UniTask.Yield:让出执行权

UniTask.Yield 用于将执行权让出,允许其他任务在当前任务继续之前运行。它可以指定在 Unity 的哪个生命周期阶段恢复执行。

using Cysharp.Threading.Tasks;
using UnityEngine;
using System.Threading;

public class YieldExample : MonoBehaviour
{
    async void Start()
    {
        Debug.Log($"[Yield] 开始测试");

        // 默认在 Update 之后执行
        await UniTask.Yield();
        Debug.Log($"[Yield] 默认Yield后帧数: {Time.frameCount}");

        // 切换到 PreLateUpdate 阶段执行
        Debug.Log($"[Yield] 准备切换到PreLateUpdate阶段");
        await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
        Debug.Log($"[Yield] 当前处于PreLateUpdate阶段后的帧数: {Time.frameCount}");

        // 切换到线程池线程
        Debug.Log($"[Yield] 准备切换到线程池");
        await UniTask.SwitchToThreadPool();
        Debug.Log($"[Yield] 当前在线程池线程: {Thread.CurrentThread.ManagedThreadId}");

        // 返回主线程
        await UniTask.Yield();
        Debug.Log($"[Yield] 返回主线程后的线程ID: {Thread.CurrentThread.ManagedThreadId}");
    }
}

通过指定不同的 PlayerLoopTiming,可以控制代码在 Unity 生命周期的不同阶段执行。

UniTask.WaitForEndOfFrame:等待帧结束

UniTask.WaitForEndOfFrame 用于等待当前帧的结束,类似于协程中的 WaitForEndOfFrame

using Cysharp.Threading.Tasks;
using UnityEngine;

public class WaitForEndOfFrameExample : MonoBehaviour
{
    async void Start()
    {
        Debug.Log($"[WaitForEndOfFrame] 开始测试 - 当前帧: {Time.frameCount}");

        // 等待当前帧结束
        await UniTask.WaitForEndOfFrame(this);
        Debug.Log($"[WaitForEndOfFrame] 等待帧结束后 - 当前帧: {Time.frameCount}");
    }
}

需要注意的是,在 Unity 2023.1 之前的版本中,WaitForEndOfFrame 需要传入一个 MonoBehaviour 实例作为参数。在 Unity 2023.1 及之后的版本中,可以直接使用 await UniTask.WaitForEndOfFrame();,无需传入参数。

进行测试

using Cysharp.Threading.Tasks;
using UnityEngine;
using System;
using System.Threading;

/// <summary>
/// 用于测试 UniTask 各种延时操作的组件
/// 包含对 Delay、DelayFrame、NextFrame、Yield、WaitForEndOfFrame 等 API 的测试
/// </summary>
public class Lesson03_延时操作 : MonoBehaviour
{
    void Start() => TestDelayOperations().Forget();
    // 启动测试流程,.Forget() 用于忽略任务完成通知,避免 Unity 未观察任务的警告

    /// <summary>
    /// 主测试调度方法,按顺序执行所有延时操作测试
    /// </summary>
    async UniTask TestDelayOperations()
    {
        await TestDelay(); // 测试 UniTask.Delay 系列
        await TestDelayFrame(); // 测试帧延时操作
        await TestNextFrame(); // 测试下一帧等待
        await TestYield(); // 测试 Yield 相关操作
        await TestWaitForEndOfFrame(); // 测试 WaitForEndOfFrame
    }

    /// <summary>
    /// 测试 UniTask.Delay 不同参数及时间缩放影响
    /// </summary>
    async UniTask TestDelay()
    {
        Debug.Log($"[Delay] 开始测试 - 当前时间: {Time.time}");

        // 测试 1:使用毫秒参数延时 1 秒(受 Time.timeScale 影响)
        await UniTask.Delay(1000);
        Debug.Log($"[Delay] 普通延时1秒后 - 当前时间: {Time.time}");

        // 测试 2:使用 TimeSpan 方式延时 1 秒
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        Debug.Log($"[Delay] TimeSpan延时1秒后 - 当前时间: {Time.time}");

        // 测试 3:忽略时间缩放的延时(设置时间缩放为 0.5 倍验证)
        Time.timeScale = 0.5f;
        Debug.Log($"[Delay] 设置Time.timeScale = 0.5");
        await UniTask.Delay(1000, ignoreTimeScale: true); // 忽略时间缩放,实际等待 1 秒
        Debug.Log($"[Delay] 无视缩放的1秒延时后 - 当前时间: {Time.time}");

        Time.timeScale = 1f; // 恢复正常时间缩放
        Debug.Log($"[Delay] 恢复Time.timeScale = 1");
    }

    /// <summary>
    /// 测试 UniTask.DelayFrame 帧延时操作
    /// </summary>
    async UniTask TestDelayFrame()
    {
        int startFrame = Time.frameCount;
        Debug.Log($"[帧延时] 开始测试 - 当前帧数: {startFrame}");

        // 延时 10 帧后继续执行
        await UniTask.DelayFrame(10);
        Debug.Log($"[帧延时] 10帧后的帧数: {Time.frameCount}");
    }

    /// <summary>
    /// 测试 UniTask.NextFrame 等待下一帧
    /// </summary>
    async UniTask TestNextFrame()
    {
        int startFrame = Time.frameCount;
        Debug.Log($"[下一帧] 开始帧数: {startFrame}");

        // 等待下一帧渲染完成
        await UniTask.NextFrame();
        Debug.Log($"[下一帧] 下一帧的帧数: {Time.frameCount}");
    }

    /// <summary>
    /// 测试 UniTask.Yield 相关操作(含线程切换)
    /// </summary>
    async UniTask TestYield()
    {
        Debug.Log($"[Yield] 开始测试");

        // 基础用法:等待一帧(默认在 Update 之后执行)
        await UniTask.Yield();
        Debug.Log($"[Yield] 默认Yield后帧数: {Time.frameCount}");

        // 测试切换到 PreLateUpdate 阶段执行
        Debug.Log($"[Yield] 准备切换到PreLateUpdate阶段");
        await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
        Debug.Log($"[Yield] 当前处于PreLateUpdate阶段后的帧数: {Time.frameCount}");

        // 测试从线程池返回主线程
        Debug.Log($"[Yield] 准备切换到线程池");
        await UniTask.SwitchToThreadPool(); // 切换到线程池执行
        Debug.Log($"[Yield] 当前在线程池线程: {Thread.CurrentThread.ManagedThreadId}");

        // 通过 Yield 回到主线程
        await UniTask.Yield();
        Debug.Log($"[Yield] 返回主线程后的线程ID: {Thread.CurrentThread.ManagedThreadId}");
    }

    /// <summary>
    /// 测试 UniTask.WaitForEndOfFrame 等待帧结束
    /// </summary>
    async UniTask TestWaitForEndOfFrame()
    {
        Debug.Log($"[WaitForEndOfFrame] 开始测试 - 当前帧: {Time.frameCount}");
        // 使用新版 WaitForEndOfFrame(传入 MonoBehaviour 实例)
        await UniTask.WaitForEndOfFrame(this);
        Debug.Log($"[WaitForEndOfFrame] 等待帧结束后 - 当前帧: {Time.frameCount}");
    }
}

总结

UniTask 提供了丰富的延时操作方法,能够高效地替代传统的协程,实现更简洁和性能优化的异步代码。根据具体需求选择合适的延时方法,可以有效提升 Unity 项目的开发效率和运行


3.2 知识点代码

Lesson03_延时操作.cs

using Cysharp.Threading.Tasks;
using UnityEngine;
using System;
using System.Threading;

/// <summary>
/// 用于测试 UniTask 各种延时操作的组件
/// 包含对 Delay、DelayFrame、NextFrame、Yield、WaitForEndOfFrame 等 API 的测试
/// </summary>
public class Lesson03_延时操作 : MonoBehaviour
{
    void Start() => TestDelayOperations().Forget();
    // 启动测试流程,.Forget() 用于忽略任务完成通知,避免 Unity 未观察任务的警告

    /// <summary>
    /// 主测试调度方法,按顺序执行所有延时操作测试
    /// </summary>
    async UniTask TestDelayOperations()
    {
        await TestDelay(); // 测试 UniTask.Delay 系列
        await TestDelayFrame(); // 测试帧延时操作
        await TestNextFrame(); // 测试下一帧等待
        await TestYield(); // 测试 Yield 相关操作
        await TestWaitForEndOfFrame(); // 测试 WaitForEndOfFrame
    }

    /// <summary>
    /// 测试 UniTask.Delay 不同参数及时间缩放影响
    /// </summary>
    async UniTask TestDelay()
    {
        Debug.Log($"[Delay] 开始测试 - 当前时间: {Time.time}");

        // 测试 1:使用毫秒参数延时 1 秒(受 Time.timeScale 影响)
        await UniTask.Delay(1000);
        Debug.Log($"[Delay] 普通延时1秒后 - 当前时间: {Time.time}");

        // 测试 2:使用 TimeSpan 方式延时 1 秒
        await UniTask.Delay(TimeSpan.FromSeconds(1));
        Debug.Log($"[Delay] TimeSpan延时1秒后 - 当前时间: {Time.time}");

        // 测试 3:忽略时间缩放的延时(设置时间缩放为 0.5 倍验证)
        Time.timeScale = 0.5f;
        Debug.Log($"[Delay] 设置Time.timeScale = 0.5");
        await UniTask.Delay(1000, ignoreTimeScale: true); // 忽略时间缩放,实际等待 1 秒
        Debug.Log($"[Delay] 无视缩放的1秒延时后 - 当前时间: {Time.time}");

        Time.timeScale = 1f; // 恢复正常时间缩放
        Debug.Log($"[Delay] 恢复Time.timeScale = 1");
    }

    /// <summary>
    /// 测试 UniTask.DelayFrame 帧延时操作
    /// </summary>
    async UniTask TestDelayFrame()
    {
        int startFrame = Time.frameCount;
        Debug.Log($"[帧延时] 开始测试 - 当前帧数: {startFrame}");

        // 延时 10 帧后继续执行
        await UniTask.DelayFrame(10);
        Debug.Log($"[帧延时] 10帧后的帧数: {Time.frameCount}");
    }

    /// <summary>
    /// 测试 UniTask.NextFrame 等待下一帧
    /// </summary>
    async UniTask TestNextFrame()
    {
        int startFrame = Time.frameCount;
        Debug.Log($"[下一帧] 开始帧数: {startFrame}");

        // 等待下一帧渲染完成
        await UniTask.NextFrame();
        Debug.Log($"[下一帧] 下一帧的帧数: {Time.frameCount}");
    }

    /// <summary>
    /// 测试 UniTask.Yield 相关操作(含线程切换)
    /// </summary>
    async UniTask TestYield()
    {
        Debug.Log($"[Yield] 开始测试");

        // 基础用法:等待一帧(默认在 Update 之后执行)
        await UniTask.Yield();
        Debug.Log($"[Yield] 默认Yield后帧数: {Time.frameCount}");

        // 测试切换到 PreLateUpdate 阶段执行
        Debug.Log($"[Yield] 准备切换到PreLateUpdate阶段");
        await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
        Debug.Log($"[Yield] 当前处于PreLateUpdate阶段后的帧数: {Time.frameCount}");

        // 测试从线程池返回主线程
        Debug.Log($"[Yield] 准备切换到线程池");
        await UniTask.SwitchToThreadPool(); // 切换到线程池执行
        Debug.Log($"[Yield] 当前在线程池线程: {Thread.CurrentThread.ManagedThreadId}");

        // 通过 Yield 回到主线程
        await UniTask.Yield();
        Debug.Log($"[Yield] 返回主线程后的线程ID: {Thread.CurrentThread.ManagedThreadId}");
    }

    /// <summary>
    /// 测试 UniTask.WaitForEndOfFrame 等待帧结束
    /// </summary>
    async UniTask TestWaitForEndOfFrame()
    {
        Debug.Log($"[WaitForEndOfFrame] 开始测试 - 当前帧: {Time.frameCount}");
        // 使用新版 WaitForEndOfFrame(传入 MonoBehaviour 实例)
        await UniTask.WaitForEndOfFrame(this);
        Debug.Log($"[WaitForEndOfFrame] 等待帧结束后 - 当前帧: {Time.frameCount}");
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏