〇、把多个不同的脚本串联在一起顺序阻塞执行
把很多个脚本串联在一起,让他们按照先后顺序执行,等着前面的执行完毕,在执行后面的,这就是用【异步方法】实现的,能够顺序执行的流程。
如下图所示,流程脚本都绑定在空物体上,然后绑了脚本的所有空物体都是脚本物体,把他们集中统一管理。
一共有14个脚本,他们会按照先后顺序有条不紊的排队执行。
一、多个脚本按照先后顺序排队执行是如何实现的——原理
(1)流程按照顺序先后执行
既然要排队执行,那必定是后面的等着前面的执行完毕之后自己才执行,这就必须用到【等待await】功能,本文用[异步方法]而不是直接用[协程]。
(2)不同的脚本(Class)如何给他们制定一个统一的入口方法
- 给每个脚本添加一个【名字和签名】都一样的方法,调用的时候,直接Call这个方法,因为Class的类型不一样,所以在调用的时候,可能需要用到【反射】。能不能用一种偷懒的方法,直接调用FlowAsync( )呢?那就是下面介绍的方法:实现一个统一的接口
给所有的脚本都定义一个签名完全一致的普通方法作为调用的接口:
public class MyScript: MonoBehaviour
{
public async UniTask FlowAsync(CancellationToken ctk){}
}
(3)不同的脚本(Class)如何给他们强制实现一个入口方法——接口
注意下面的脚本,除了继承祖传的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;
/// <summary>
/// 接口:定义一个叫FlowAsync的异步方法
/// </summary>
public interface IFlowAsync
{
public UniTask FlowAsync(CancellationToken ctk);
}
只继承接口,而不实现接口的后果:
二、多个脚本按照先后顺序排队执行是如何实现的——代码
需求:如下图所示,我有14条流程,它们都挂在14个空物体上,这些空物体都挂在另一个叫【流程内容】的空物体下。当我启动【流程内容】节点上的脚本时,该脚本自动加载下面的子物体,然后按照先后顺序执行子物体上的脚本。
(1)流程脚本的一个父节点如何实现?
- 父脚本定义一个【脚本列表】,用来装子物体的脚本
/// <summary>
/// 流程脚本列表
/// </summary>
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】方法执行
- 其它功能:编辑器状态自动添加子物体,捕捉他们的脚本。子流程进行编号。子流程可以单步测试。
加载子物体上的步骤脚本
/// <summary>
/// 加载子物体上的步骤脚本
/// </summary>
#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
/// <summary>
/// 步骤编号:加载进来的子物体,给他们编个序号,从1到N
/// </summary>
/// <returns></returns>
#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模式下随便测
/// <summary>
/// 测试步骤:单步测试该步骤,注意编辑器模式下的修改很难撤销,所以在Running模式下随便测
/// </summary>
#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();
}
}
(2)流程脚本的一个子节点如何实现?
作为一个子节点,满足一个条件即可——继承接口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}");
}
}
三、流程的组织举例
主流程
其中第二个脚本节点包含子流程
四、部分代码清单
(1)流程父节点Step清单
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Threading;
/// <summary>
/// 流程的节点,该节点下面挂的直接子物体【脚本】都属于该节点的子节点
/// </summary>
public class Step : MonoBehaviour,IFlowAsync
{
/// <summary>
/// 流程脚本列表
/// </summary>
public List<MonoBehaviour> scripts = new List<MonoBehaviour>();
/// <summary>
/// 流程的名字
/// </summary>
public string this_name;
// Start is called before the first frame update
void Start()
{
this_name = this.name;
LoadSteps();
}
/// <summary>
/// 加载步骤子物体上的步骤
/// </summary>
#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);
}
}
}
/// <summary>
/// 步骤编号:加载进来的子物体,给他们编个序号,从1到N
/// </summary>
/// <returns></returns>
#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++;
}
}
/// <summary>
/// 测试步骤:单步测试该步骤,注意编辑器模式下的修改很难撤销,所以在Running模式下随便测
/// </summary>
#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();
}
}
}
(2)物体绕自身某个轴的旋转
using Cysharp.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
/// <summary>
/// 异步方法:物体绕自身的某个轴旋转
/// </summary>
public class Rotate : MonoBehaviour,IFlowAsync
{
/// <summary>
/// 旋转的物体
/// </summary>
[SerializeField][Header("旋转的物体")]
public GameObject target;
/// <summary>
/// 旋转轴
/// </summary>
[SerializeField]
[Header("旋转轴")]
public Vector3 axis;
/// <summary>
/// 速度
/// </summary>
[SerializeField]
[Header("速度")]
public float speed;
/// <summary>
/// 耗时
/// </summary>
[SerializeField]
[Header("耗时")]
public float duration;
/// <summary>
/// 旋转完毕恢复初始方位
/// </summary>
[SerializeField]
[Header("旋转完毕恢复初始方位")]
public bool restored = true;
#if UNITY_EDITOR
[ContextMenu("测试")]
#endif
void Test()
{
var ctsInfo = TaskSingol.CreatCts();
FlowAsync(ctsInfo.cts.Token);
}
/// <summary>
/// 自身旋转
/// </summary>
/// <param name="ctk"></param>
/// <returns></returns>
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();
}
}
- 上面代码中用到的DoRotate方法的实现
/// <summary>
/// 物体obj绕着自身的轴axis,进行旋转,旋转的速度为speed,当旋转的累计时间达到duration后,停止旋转
/// </summary>
/// <param name="obj">需要进行旋转的物体</param>
/// <param name="axis">旋转的轴向,应该是一个单位向量</param>
/// <param name="speed">旋转的速度,单位为度/秒</param>
/// <param name="duration">旋转的总时间,单位为秒</param>
/// <returns></returns>
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();
}
}
五、思路扩展
(1)流程控制思考
如果是单线的顺序流程,则很方便,如果中间包含分支执行呢,那就借鉴Linq的WhenAll和WhenAny来实现。
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
/// <summary>
/// 等待所有的子步骤执行完毕
/// </summary>
public class WhenAll: MonoBehaviour,IFlowAsync
{
/// <summary>
/// 流程组
/// </summary>
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);
}
/// <summary>
/// 主步骤
/// </summary>
/// <param name="ctk"></param>
/// <returns></returns>
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();
}
}
}
(2)延时等待
public async UniTask FlowAsync(CancellationToken ctk)
{
await UniTask.Delay(TimeSpan.FromSeconds(delayTimeInSeconds), cancellationToken: ctk);
}
(3)等待Animation播放结束
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));
}
(4)等待一个或者多个button被点击(【全部点击】或者【点击任意一个】)
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 功能介绍:异步流程——等待按钮被点击, 等待一个或者多个按钮被点击
/// 脚本参数:【All:全部】或者【Any:任意一个】被点击
/// </summary>
public class BottonOnClicked : MonoBehaviour,IFlowAsync
{
/// <summary>
/// 按钮组
/// </summary>
[Header("按钮组")]
public List<Button> buttons = new List<Button>();
/// <summary>
/// 等待的类型【all | any】
/// </summary>
[Header("等待的类型【all | any】")]
public string waitType;
/// <summary>
/// 点击后隐藏该button
/// </summary>
[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();
}
}
}