把很多个脚本串联在一起,让他们按照先后顺序执行,等着前面的执行完毕,在执行后面的,这就是用【异步方法】实现的,能够顺序执行的流程。
如下图所示,流程脚本都绑定在空物体上,然后绑了脚本的所有空物体都是脚本物体,把他们集中统一管理。
一共有14个脚本,他们会按照先后顺序有条不紊的排队执行。
既然要排队执行,那必定是后面的等着前面的执行完毕之后自己才执行,这就必须用到【等待await】功能,本文用[异步方法]而不是直接用[协程]。
给所有的脚本都定义一个签名完全一致的普通方法作为调用的接口:
public class MyScript: MonoBehaviour
{
public async UniTask FlowAsync(CancellationToken ctk){}
}
注意下面的脚本,除了继承祖传的MonoBehaviour,它还继承了IFlowAsync,IFlowAsync是什么鬼东西呢?看下文分解!
所有的脚本都实现了一个统一的接口IFlowAsync.FlowAsync:
public class MyScript: MonoBehaviour,IFlowAsync
{
public async UniTask FlowAsync(CancellationToken ctk){}
}
顾名思义,IFlowAsync就是一个接口啊,且看它的代码:
定义一个接口,包含一个叫FlowAsync的异步方法,所以继承该接口的脚本,都必须定义一个同名的方法,也就是实现接口。如果我只继承接口而不实现接口会怎么样,当然是报错。
using System.Threading;
using Cysharp.Threading.Tasks;
///
/// 接口:定义一个叫FlowAsync的异步方法
///
public interface IFlowAsync
{
public UniTask FlowAsync(CancellationToken ctk);
}
需求:如下图所示,我有14条流程,它们都挂在14个空物体上,这些空物体都挂在另一个叫【流程内容】的空物体下。当我启动【流程内容】节点上的脚本时,该脚本自动加载下面的子物体,然后按照先后顺序执行子物体上的脚本。
///
/// 流程脚本列表
///
public List<MonoBehaviour> scripts = new List<MonoBehaviour>();
foreach (var script in scripts)
{
if (script == null) continue;
var scriptName = script.name;
Debug.Log($"**********************开始步骤:{scriptName}");
await (script as IFlowAsync).FlowAsync(ctk);
Debug.Log($"**********************完成步骤:{scriptName}");
}
注意其中的一行代码
await (script as IFlowAsync).FlowAsync(ctk);
代码解释:这一句调用了各个脚本的接口方法——FlowAsync(),古人说【人上一百,形形色色】,脚本上一百也不例外。用了接口的好处就是用【as】操作符,把脚本转成接口类型,然后直接call接口的方法,这样就省去反射捕捉具体的Class类型的操作了。
阻塞执行的诀窍:用【await】关键字来等待一个【async】方法执行
加载子物体上的步骤脚本
///
/// 加载子物体上的步骤脚本
///
#if UNITY_EDITOR
[ContextMenu("加载步骤")]
#endif
void LoadSteps()
{
scripts.Clear();
var root = this.gameObject.transform;
int childCount = root.childCount;
for (int i = 0; i < childCount; i++)
{
Transform childTransform = root.GetChild(i);
//处理子物体
Debug.Log(childTransform.name);
//获取脚本,隐藏的不获取
if (childTransform.gameObject.activeSelf)
{
var script = childTransform.GetComponent<MonoBehaviour>();
scripts.Add(script);
}
}
}
步骤编号:加载进来的子物体,给他们编个序号,从1到N
///
/// 步骤编号:加载进来的子物体,给他们编个序号,从1到N
///
///
#if UNITY_EDITOR
[ContextMenu("步骤编号")]
#endif
async UniTask Test2()
{
int i = 0;
foreach (var script in scripts)
{
var name = script.name;
string newName = "";
if (script.name.Contains("】"))
{
var nameRight = name.Split('】')[1];
newName = $"【{i}】{nameRight}";
}
else
{
newName = $"【{i}】{name}";
}
script.name = newName;
i++;
}
}
测试步骤:单步测试该步骤,注意编辑器模式下的修改很难撤销,所以在Running模式下随便测
///
/// 测试步骤:单步测试该步骤,注意编辑器模式下的修改很难撤销,所以在Running模式下随便测
///
#if UNITY_EDITOR
[ContextMenu("测试步骤")]
#endif
void testAsync()
{
var ctsInfo = TaskSingol.CreatCts();
FlowAsync(ctsInfo.cts.Token);
}
public async UniTask FlowAsync(CancellationToken ctk)
{
try
{
foreach (var script in scripts)
{
if (script == null) continue;
var scriptName = script.name;
Debug.Log($"**********************开始步骤:{scriptName}");
await (script as IFlowAsync).FlowAsync(ctk);
Debug.Log($"**********************完成步骤:{scriptName}");
}
}
catch (Exception e)
{
Debug.Log($"{this_name}报错:{e.Message}");
Debug.Log($"\n 抛出一个OperationCanceledException");
throw new OperationCanceledException();
}
}
作为一个子节点,满足一个条件即可——继承接口IFlowAsync并实现方法FlowAsync,下面是一个简单的例子:
using System.Threading;
using UnityEngine;
using Cysharp.Threading.Tasks;
public class FlowA : MonoBehaviour, IFlowAsync
{
public async UniTask FlowAsync(CancellationToken ctk)
{
Debug.Log($"我是monobehaviourA {Time.realtimeSinceStartup}");
await UniTask.Delay(2000,cancellationToken:ctk);
Debug.Log($"UniTask.Delay(2000) {Time.realtimeSinceStartup}");
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Threading;
///
/// 流程的节点,该节点下面挂的直接子物体【脚本】都属于该节点的子节点
///
public class Step : MonoBehaviour,IFlowAsync
{
///
/// 流程脚本列表
///
public List<MonoBehaviour> scripts = new List<MonoBehaviour>();
///
/// 流程的名字
///
public string this_name;
// Start is called before the first frame update
void Start()
{
this_name = this.name;
LoadSteps();
}
///
/// 加载步骤子物体上的步骤
///
#if UNITY_EDITOR
[ContextMenu("加载步骤")]
#endif
void LoadSteps()
{
scripts.Clear();
var root = this.gameObject.transform;
int childCount = root.childCount;
for (int i = 0; i < childCount; i++)
{
Transform childTransform = root.GetChild(i);
//处理子物体
Debug.Log(childTransform.name);
//获取脚本,隐藏的不获取
if (childTransform.gameObject.activeSelf)
{
var script = childTransform.GetComponent<MonoBehaviour>();
scripts.Add(script);
}
}
}
///
/// 步骤编号:加载进来的子物体,给他们编个序号,从1到N
///
///
#if UNITY_EDITOR
[ContextMenu("步骤编号")]
#endif
async UniTask Test2()
{
int i = 0;
foreach (var script in scripts)
{
var name = script.name;
string newName = "";
if (script.name.Contains("】"))
{
var nameRight = name.Split('】')[1];
newName = $"【{i}】{nameRight}";
}
else
{
newName = $"【{i}】{name}";
}
script.name = newName;
i++;
}
}
///
/// 测试步骤:单步测试该步骤,注意编辑器模式下的修改很难撤销,所以在Running模式下随便测
///
#if UNITY_EDITOR
[ContextMenu("测试步骤")]
#endif
void testAsync()
{
var ctsInfo = TaskSingol.CreatCts();
FlowAsync(ctsInfo.cts.Token);
}
public async UniTask FlowAsync(CancellationToken ctk)
{
try
{
foreach (var script in scripts)
{
if (script == null) continue;
var scriptName = script.name;
Debug.Log($"**********************开始步骤:{scriptName}");
await (script as IFlowAsync).FlowAsync(ctk);
Debug.Log($"**********************完成步骤:{scriptName}");
}
}
catch (Exception e)
{
Debug.Log($"{this_name}报错:{e.Message}");
Debug.Log($"\n 抛出一个OperationCanceledException");
throw new OperationCanceledException();
}
}
}
using Cysharp.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
///
/// 异步方法:物体绕自身的某个轴旋转
///
public class Rotate : MonoBehaviour,IFlowAsync
{
///
/// 旋转的物体
///
[SerializeField][Header("旋转的物体")]
public GameObject target;
///
/// 旋转轴
///
[SerializeField]
[Header("旋转轴")]
public Vector3 axis;
///
/// 速度
///
[SerializeField]
[Header("速度")]
public float speed;
///
/// 耗时
///
[SerializeField]
[Header("耗时")]
public float duration;
///
/// 旋转完毕恢复初始方位
///
[SerializeField]
[Header("旋转完毕恢复初始方位")]
public bool restored = true;
#if UNITY_EDITOR
[ContextMenu("测试")]
#endif
void Test()
{
var ctsInfo = TaskSingol.CreatCts();
FlowAsync(ctsInfo.cts.Token);
}
///
/// 自身旋转
///
///
///
public async UniTask FlowAsync(CancellationToken ctk)
{
Debug.Log($"~~启动Rotate() {Time.realtimeSinceStartup}");
await target.DoRotate(axis, speed, duration,ctk,restored);
Debug.Log($"~~结束Rotate() {Time.realtimeSinceStartup}");
}
void OnDestroy()
{
TaskSingol.CancelAllTask();
}
}
///
/// 物体obj绕着自身的轴axis,进行旋转,旋转的速度为speed,当旋转的累计时间达到duration后,停止旋转
///
/// 需要进行旋转的物体
/// 旋转的轴向,应该是一个单位向量
/// 旋转的速度,单位为度/秒
/// 旋转的总时间,单位为秒
///
public static async UniTask DoRotate(this GameObject obj, Vector3 axis, float speed, float duration, CancellationToken ctk,bool restore = true)
{
try
{
float rotateTime = 0f;
Quaternion startRotation = obj.transform.rotation; // 初始旋转角度
bool isOver = false;
// Update的内容:Unity 2020.2, C# 8.0
Func<UniTask> UpdateLoop = async () =>
{
//绑定到Update中去执行
await foreach (var _ in UniTaskAsyncEnumerable.EveryUpdate())
{
if (rotateTime >= duration) break;
if (ctk.IsCancellationRequested)
{
throw new OperationCanceledException();
break;
}
float deltaTime = Time.deltaTime;
float rotateAngle = speed * deltaTime; // 计算旋转角度
obj.transform.Rotate(axis, rotateAngle, Space.Self); // 使用 Transform.Rotate 方法进行旋转
rotateTime += deltaTime;
}
isOver = true;
return;
};
UpdateLoop();
await UniTask.WaitUntil(() => isOver == true);
// 恢复初始旋转角度
if (restore == true)
{
obj.transform.rotation = startRotation;
}
}
catch (Exception e)
{
Debug.Log($"DoRotate报错:{e.Message}");
Debug.Log($" 抛出一个OperationCanceledException");
throw new OperationCanceledException();
}
}
如果是单线的顺序流程,则很方便,如果中间包含分支执行呢,那就借鉴Linq的WhenAll和WhenAny来实现。
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
///
/// 等待所有的子步骤执行完毕
///
public class WhenAll: MonoBehaviour,IFlowAsync
{
///
/// 流程组
///
public List<MonoBehaviour> scripts = new List<MonoBehaviour>();
private string this_name;
void Start()
{
LoadSteps();
this_name = this.name;
}
#if UNITY_EDITOR
[ContextMenu("加载步骤")]
#endif
void LoadSteps()
{
scripts.Clear();
var root = this.gameObject.transform;
int childCount = root.childCount;
for (int i = 0; i < childCount; i++)
{
Transform childTransform = root.GetChild(i);
//处理子物体
Debug.Log(childTransform.name);
//获取脚本,隐藏的不获取
if (childTransform.gameObject.activeSelf)
{
var script = childTransform.GetComponent<MonoBehaviour>();
scripts.Add(script);
}
}
}
#if UNITY_EDITOR
[ContextMenu("步骤编号")]
#endif
async UniTask Test2()
{
int i = 0;
foreach (var script in scripts)
{
var name = script.name;
string newName = "";
if (script.name.Contains("】"))
{
var nameRight = name.Split('】')[1];
newName = $"【{i}】{nameRight}";
}
else
{
newName = $"【{i}】{name}";
}
script.name = newName;
i++;
}
}
#if UNITY_EDITOR
[ContextMenu("测试")]
#endif
void Test()
{
var ctsInfo = TaskSingol.CreatCts();
FlowAsync(ctsInfo.cts.Token);
}
///
/// 主步骤
///
///
///
public async UniTask FlowAsync(CancellationToken ctk)
{
try
{
var allTasks = scripts.Select(s => (s as IFlowAsync).FlowAsync(ctk));
await UniTask.WhenAll(allTasks).AttachExternalCancellation(ctk);
}
catch (Exception e)
{
Debug.Log($"{this_name}.Anim报错:{e.Message}");
Debug.Log($"\n 抛出一个OperationCanceledException");
throw new OperationCanceledException();
}
}
}
public async UniTask FlowAsync(CancellationToken ctk)
{
await UniTask.Delay(TimeSpan.FromSeconds(delayTimeInSeconds), cancellationToken: ctk);
}
public async UniTask FlowAsync(CancellationToken ctk)
{
myAnimation.enabled = true;
if(order == "正序")
{
myAnimation[myAnimation.clip.name].time = 0;
myAnimation[myAnimation.clip.name].speed = 1;
}
else if (order == "倒序")
{
myAnimation[myAnimation.clip.name].time = myAnimation[myAnimation.clip.name].length;
myAnimation[myAnimation.clip.name].speed = -1;
}
myAnimation.Play(myAnimation.clip.name);//播放动画
var duration = myAnimation[myAnimation.clip.name].time;
await UniTask.Delay(TimeSpan.FromSeconds(duration));
}
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
///
/// 功能介绍:异步流程——等待按钮被点击, 等待一个或者多个按钮被点击
/// 脚本参数:【All:全部】或者【Any:任意一个】被点击
///
public class BottonOnClicked : MonoBehaviour,IFlowAsync
{
///
/// 按钮组
///
[Header("按钮组")]
public List<Button> buttons = new List<Button>();
///
/// 等待的类型【all | any】
///
[Header("等待的类型【all | any】")]
public string waitType;
///
/// 点击后隐藏该button
///
[Header("点击后隐藏该button")]
public bool hideAfterClicked = false;
#if UNITY_EDITOR
[ContextMenu("测试")]
#endif
public async UniTask FlowAsync(CancellationToken ctk)
{
try
{
buttons.ForEach(b => b.gameObject.SetActive(true));
if (waitType == "all")
{
await UniTask.WhenAll(buttons.Select(b => b.OnClickAsync(ctk))).AttachExternalCancellation(ctk);
}
else if (waitType == "any")
{
await UniTask.WhenAny(buttons.Select(b => b.OnClickAsync(ctk))).AttachExternalCancellation(ctk);
}
else
{
Debug.LogError("BottonOnClicked中的waitType只能是【all】或者【any】");
}
//隐藏按钮?
if (hideAfterClicked) buttons.ForEach(b => b.gameObject.SetActive(false));
}
catch (Exception e)
{
Debug.Log($"BottonOnClicked脚本报错:{e.Message}\n 抛出一个OperationCanceledException");
throw new OperationCanceledException();
}
}
}