一、UniTask和Task
UniTask是Unity中的Task实现,Task是C#中实现异步操作的一个模块(类)。UniTask与Task有着同样的使用思路(使用习惯,常用API等),可以说UniTask是借鉴Task而开发出来的。
二、需求的来源
以前有一个实验,操作就是点击物体,执行动画,点击物体,执行动画…如此子子孙孙无穷循环,直到地球爆炸(实验结束)。
2.1 原来的脚本
于是很容易就用UniTask的await把所有操作连成一片,写在一个脚本里,甚至一整个实验就一个脚本。
比如下面:
1)、面板参数定义
面板用到的参数全部释放在Inspector上面,代码的话带上注释和空格将近1800行
2)、异步流程的组织
操作流程的话,写在一个异步方法里,浩浩荡荡写了上千行…写起来倒是比较滑溜了,但是调试和复用的话,就有点…
2.2 要改造成什么样
如果用户说他要【上一步】、【下一步】和【跳步】
思路:把一串流程按照操作逻辑分块,不同的块编一个号,放到一个执行列表里面。
1)、从前的脚本是这样:
/// <summary>
/// 主流程:所有流程连成一片,无法分步执行
/// </summary>
/// <returns></returns>
public async UniTask Flow()
{
//第一步
await UniTask.Delay(1000);
//...
//第二步
await UniTask.Delay(1000);
//...
//第三步
await UniTask.Delay(1000);
//...
//...
//第N步
await UniTask.Delay(1000);
//...
}
2) 、分块思路
- (1)定义一个列表StepInfosList,用来装载要执行的步骤。
/// <summary>
/// 步骤信息表:系列化到面板,用于调试和观察,后期执行
/// </summary>
[Header("步骤信息表")]
public List<StepInfo> StepInfosList = new List<StepInfo>();
每一个节点StepInfo的结构如下:
3) 、分块后的脚本
/// <summary>
/// 步骤信息Class
/// </summary>
[Serializable]
public class StepInfo
{
/// <summary>
/// 当前序号
/// </summary>
[Header("当前步骤的序号")]
public int index;
/// <summary>
/// 步骤的名字
/// </summary>
[Header("步骤的名字")]
public string name;
/// <summary>
/// 正常流程
/// </summary>
public Func<UniTask> Flow;
/// <summary>
/// 恢复到【初始状态】的流程
/// </summary>
public Action Init;
/// <summary>
/// 跳到【结束状态】的流程
/// </summary>
public Action Final;
}
- (2)拆分步骤
把脚本按照逻辑分成不同的块(步骤),装入到执行步骤列表里
/// <summary>
/// 添加步骤:新改的写法——各个步骤单独分开,逐个添加到执行列表里
/// </summary>
/// <param name="ctk"></param>
/// <returns></returns>
public async UniTask AddSteps(CancellationToken ctk)
{
//第一步
var step = StepInfosList.AddFlow("第一步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第一步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
//第二步
step = StepInfosList.AddFlow("第二步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第二步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
//第三步
step = StepInfosList.AddFlow("第三步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第三步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
//第N步
step = StepInfosList.AddFlow("第N步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第N步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
}
三、跳步会有哪些操作
3.1 跳步的分类
3.2 跳步的实现
- 【1】获取目标步骤信息
- 【2】处理中间步骤的状态
- 【3】执行目标步骤的流程
/// <summary>
/// 给定步骤名字,执行指定的步骤【所谓任意跳步】
/// </summary>
/// <param name="taskName"></param>
async UniTask RunTask(string taskName)
{
if (cts.IsCancellationRequested)
{
Debug.Log("所有的任务已经被取消了!!");
return;
}
//【1】获取目标步骤信息
var targetStep = StepInfosList.First(x => x.name.Equals(taskName.Trim()));
//【2】处理中间步骤的状态
Debug.Log($"#################### 要执行的目标步骤为:targetStep = {targetStep.index} {targetStep.name} currentIndex = {currentIndex} ");
if (currentIndex == targetStep.index) //本步骤重新执行:恢复到本步骤初始状态
{
Debug.Log($"回到【{targetStep.name}】初始状态");
if (targetStep.Init == null)
{
Debug.LogWarning($"步骤【{targetStep.index} {targetStep.name}】的Init函数为空,请补全!");
}
else
{
targetStep.Init();
}
}
if (currentIndex < targetStep.index) //往后跳步:中间步骤的状态快速补足
{
StepInfosList
.Where(x => (x.index >= currentIndex) && (x.index < targetStep.index)).ToList()
.ForEach(s =>
{
Debug.Log($"回到【{s.name}】结束状态");
if (s.Final == null)
{
Debug.LogWarning($"步骤【{s.index} {s.name}】的Final函数为空,请补全!");
}
else
{
s.Final();
}
});
}
if (currentIndex > targetStep.index) //往前跳步:中间步骤的状态快速撤销
{
StepInfosList
.Where(x => (x.index >= targetStep.index) && (x.index <= currentIndex))
.OrderByDescending(x=>x.index).ToList() //注意倒序排列——由后往前执行
.ForEach(s =>
{
Debug.Log($"回到【{s.name}】初始状态");
if (s.Init == null)
{
Debug.LogWarning($"步骤【{s.index} {s.name}】的Init函数为空,请补全!");
}
else
{
s.Init();
}
});
}
//【3】执行目标步骤的流程
await targetStep.Flow();
currentIndex = targetStep.index;
Debug.Log($"执行步骤:{targetStep.index} {targetStep.name} 执行完毕,当前步骤currentIndex = {currentIndex}!");
}
3.3 执行所有步骤
用异步等待来逐个执行所有的步骤,如下所示:
/// <summary>
/// 执行所有的步骤
/// </summary>
/// <returns></returns>
private async UniTask RunAllTasks()
{
foreach (var step in StepInfosList)
{
Debug.Log($"当前执行的步骤:{step.index} {step.name}");
currentIndex = step.index;
await step.Flow();
}
}
3.4 如何取消所有步骤
- 1)定义一个CancellationTokenSource(暂且称它为-异步任务取消标记)
异步操作必须有取消,如果你不取消,当切换场景的时候,异步流程还在运行,则会出现资源的空引用。
举个不恰当的例子:你在家里玩电脑,然后让你的5岁的娃娃去楼下接妈妈回家。结果你媳妇从地下室坐电梯上来了,结局就是你的娃娃一直没等到他妈妈,结果呢,他呆坐在门口一直等,一直等,天黑也不回家。
问题在哪里:你指派给你小孩接妈妈这个异步操作,没有附加异步取消的令牌。下次你应该对他说:天黑还没等到妈妈的话,你就直接回家吃饭。或者我打电话给你,你就回家。
/// <summary>
/// 异步任务取消的标记
/// </summary>
private CancellationTokenSource cts;
- 2)取消异步任务
单凡用到上面cts的await,只要发现cts.IsCancellationRequested变成true,就会停止执行
/// <summary>
/// 取消所有的任务
/// </summary>
void CancelAllTasks()
{
cts.Cancel();
}
- 3) CancellationTokenSource是啥,如何服用?
CancellationTokenSource就是一个遥控器,遥控器一按关机,那么凡是被该遥控器遥控的电视都关机。那么如何把遥控器绑定给一个电视机呢,也就是如何把一个CancellationTokenSource绑定给一坨异步操作。
秘诀就是凡是await 操作的地方,你都绑一个CancellationTokenSource的token给它。
var cts = new CancellationTokenSource();
cancelButton.onClick.AddListener(() =>
{
cts.Cancel();
});
await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token);
await UniTask.DelayFrame(1000, cancellationToken: cts.Token);
- 4)把所有的步骤绑定在一个CancellationTokenSource上
/// <summary>
/// 创建任务,用一个新的【CancellationTokenSource】控制这些异步任务
/// </summary>
/// <returns>cts</returns>
CancellationTokenSource CreatTasks()
{
var cts = new CancellationTokenSource();
StepInfosList.Clear();
AddSteps(cts.Token).Forget();
return cts;
}
四、测试步骤
4.1 测试脚本
//加载步骤流程到执行列表中
Debug.Log($"【1】************加载步骤流程到执行列表中:{Time.realtimeSinceStartup}");
cts = CreatTasks();
//执行一遍所有的任务
Debug.Log($"【2】************执行一遍所有的任务:{Time.realtimeSinceStartup}");
await RunAllTasks();
//等待3秒钟,跳步到第三步执行
Debug.Log($"【3】************等待3秒钟,跳步到第三步执行:{Time.realtimeSinceStartup}");
await UniTask.Delay(3000, cancellationToken: cts.Token);
await RunTask("第三步");
//取消所有任务
Debug.Log($"【4】************取消所有任务:{Time.realtimeSinceStartup}");
CancelAllTasks();
//等待3秒钟,跳步到第1步执行
Debug.Log($"【5】************等待3秒钟,跳步到第1步执行:{Time.realtimeSinceStartup}");
await UniTask.Delay(3000, cancellationToken: cts.Token);
await RunTask("第一步");
4.2 测试结果
五、附录:脚本源码
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
/// <summary>
/// 步骤信息Class
/// </summary>
[Serializable]
public class StepInfo
{
/// <summary>
/// 当前序号
/// </summary>
[Header("当前步骤的序号")]
public int index;
/// <summary>
/// 步骤的名字
/// </summary>
[Header("步骤的名字")]
public string name;
/// <summary>
/// 正常流程
/// </summary>
public Func<UniTask> Flow;
/// <summary>
/// 恢复到【初始状态】的流程
/// </summary>
public Action Init;
/// <summary>
/// 跳到【结束状态】的流程
/// </summary>
public Action Final;
}
/// <summary>
/// 扩展方法
/// </summary>
public static class ExtensionMethods
{
/// <summary>
/// 在步骤列表MySteps中生成一个只包含步骤名的空步骤
/// </summary>
/// <param name="MySteps">步骤列表</param>
/// <param name="stepName">步骤名字</param>
/// <returns></returns>
public static StepInfo AddFlow(this List<StepInfo> MySteps, string stepName)
{
int idx = MySteps.Count == 0 ?
0
:
MySteps.Max(x => x.index) + 1;
var item = new StepInfo();
item.index = idx;
item.name = stepName;
item.Flow = null;
item.Final = null;
item.Init = null;
MySteps.Add(item);
return item;
}
}
/// <summary>
/// 步骤分步,任意跳转:一段操作流程,包含多个步骤,可以任意跳转到某个步骤执行。
/// 点击【上一步】时,需要对当前步骤所操作的内容进行撤销或者状态还原。
/// 点击【下一步】时,需要把跳过的步骤中的状态补足。
/// </summary>
public class FlowsDemo : MonoBehaviour
{
/// <summary>
/// 步骤信息表:系列化到面板,用于调试和观察,后期执行
/// </summary>
[Header("步骤信息表")]
public List<StepInfo> StepInfosList = new List<StepInfo>();
/// <summary>
/// 当前执行的步骤
/// </summary>
private int currentIndex;
/// <summary>
/// 异步任务取消的标记
/// </summary>
private CancellationTokenSource cts;
/// <summary>
/// 取消所有的任务
/// </summary>
void CancelAllTasks()
{
cts.Cancel();
}
/// <summary>
/// 创建任务,用一个新的【CancellationTokenSource】控制这些异步任务
/// </summary>
/// <returns>cts</returns>
CancellationTokenSource CreatTasks()
{
var cts = new CancellationTokenSource();
StepInfosList.Clear();
AddSteps(cts.Token).Forget();
return cts;
}
/// <summary>
/// 执行所有的步骤
/// </summary>
/// <returns></returns>
private async UniTask RunAllTasks()
{
foreach (var step in StepInfosList)
{
Debug.Log($"当前执行的步骤:{step.index} {step.name}");
currentIndex = step.index;
await step.Flow();
}
}
/// <summary>
/// 主流程:所有流程连成一片,无法分步执行
/// </summary>
/// <returns></returns>
public async UniTask Flow()
{
//第一步
await UniTask.Delay(1000);
//...
//第二步
await UniTask.Delay(1000);
//...
//第三步
await UniTask.Delay(1000);
//...
//...
//第N步
await UniTask.Delay(1000);
//...
}
/// <summary>
/// 添加步骤:新改的写法——各个步骤单独分开,逐个添加到执行列表里
/// </summary>
/// <param name="ctk"></param>
/// <returns></returns>
public async UniTask AddSteps(CancellationToken ctk)
{
//第一步
var step = StepInfosList.AddFlow("第一步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第一步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
step.Init = () => { Debug.Log("Init() - 回到第一步的初始状态");};
step.Final = () => { Debug.Log("Final() - 跳到第一步的结束状态"); };
//第二步
step = StepInfosList.AddFlow("第二步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第二步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
step.Init = () => { Debug.Log("Init() - 回到第二步的初始状态"); };
step.Final = () => { Debug.Log("Final() - 跳到第二步的结束状态"); };
//第三步
step = StepInfosList.AddFlow("第三步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第三步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
//第N步
step = StepInfosList.AddFlow("第N步");
step.Flow = async () =>
{
Debug.Log("Flow() - 第N步");
await UniTask.Delay(1000, cancellationToken: ctk);
};
}
/// <summary>
/// 给定步骤名字,执行指定的步骤【所谓任意跳步】
/// </summary>
/// <param name="taskName"></param>
async UniTask RunTask(string taskName)
{
if (cts.IsCancellationRequested)
{
Debug.Log("所有的任务已经被取消了!!");
return;
}
//【1】获取目标步骤信息
var targetStep = StepInfosList.First(x => x.name.Equals(taskName.Trim()));
//【2】处理中间步骤的状态
Debug.Log($"#################### 要执行的目标步骤为:targetStep = {targetStep.index} {targetStep.name} currentIndex = {currentIndex} ");
if (currentIndex == targetStep.index) //本步骤重新执行:恢复到本步骤初始状态
{
Debug.Log($"回到【{targetStep.name}】初始状态");
if (targetStep.Init == null)
{
Debug.LogWarning($"步骤【{targetStep.index} {targetStep.name}】的Init函数为空,请补全!");
}
else
{
targetStep.Init();
}
}
if (currentIndex < targetStep.index) //往后跳步:中间步骤的状态快速补足
{
StepInfosList
.Where(x => (x.index >= currentIndex) && (x.index < targetStep.index)).ToList()
.ForEach(s =>
{
Debug.Log($"回到【{s.name}】结束状态");
if (s.Final == null)
{
Debug.LogWarning($"步骤【{s.index} {s.name}】的Final函数为空,请补全!");
}
else
{
s.Final();
}
});
}
if (currentIndex > targetStep.index) //往前跳步:中间步骤的状态快速撤销
{
StepInfosList
.Where(x => (x.index >= targetStep.index) && (x.index <= currentIndex))
.OrderByDescending(x=>x.index).ToList() //注意倒序排列——由后往前执行
.ForEach(s =>
{
Debug.Log($"回到【{s.name}】初始状态");
if (s.Init == null)
{
Debug.LogWarning($"步骤【{s.index} {s.name}】的Init函数为空,请补全!");
}
else
{
s.Init();
}
});
}
//【3】执行目标步骤的流程
await targetStep.Flow();
currentIndex = targetStep.index;
Debug.Log($"执行步骤:{targetStep.index} {targetStep.name} 执行完毕,当前步骤currentIndex = {currentIndex}!");
}
/// <summary>
/// 要测试的【步骤名字】
/// </summary>
[Header("要执行的【步骤名字】")]
public string flowName;
#if UNITY_EDITOR
[ContextMenu("跳到指定的步骤")]
#endif
void TestFlow()
{
RunTask(flowName).Forget();
}
#if UNITY_EDITOR
[ContextMenu("取消所有任务")]
#endif
void TestFlow1()
{
CancelAllTasks();
}
#if UNITY_EDITOR
[ContextMenu("Demo测试")]
#endif
void TestFlow3()
{
Func<UniTask> TestDemo = async () =>
{
//加载步骤流程到执行列表中
Debug.Log($"【1】************加载步骤流程到执行列表中:{Time.realtimeSinceStartup}");
cts = CreatTasks();
//执行一遍所有的任务
Debug.Log($"【2】************执行一遍所有的任务:{Time.realtimeSinceStartup}");
await RunAllTasks();
//等待3秒钟,跳步到第三步执行
Debug.Log($"【3】************等待3秒钟,跳步到第三步执行:{Time.realtimeSinceStartup}");
await UniTask.Delay(3000, cancellationToken: cts.Token);
await RunTask("第三步");
//取消所有任务
Debug.Log($"【4】************取消所有任务:{Time.realtimeSinceStartup}");
CancelAllTasks();
//等待3秒钟,跳步到第1步执行
Debug.Log($"【5】************等待3秒钟,跳步到第1步执行:{Time.realtimeSinceStartup}");
await UniTask.Delay(3000, cancellationToken: cts.Token);
await RunTask("第一步");
};
Debug.Log($"开始测试:{Time.realtimeSinceStartup}");
TestDemo();
}
}