3.延时操作
3.1 知识点
UniTask 延时操作详解
在 Unity 开发中,处理异步操作时常需要使用延时功能。UniTask 提供了一套高效且无额外内存分配的延时 API,包括 UniTask.Delay
、UniTask.DelayFrame
、UniTask.NextFrame
、UniTask.Yield
和 UniTask.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