〇、把多个不同的脚本串联在一起顺序阻塞执行
把很多个脚本串联在一起,让他们按照先后顺序执行,等着前面的执行完毕,在执行后面的,这就是用【异步方法】实现的,能够顺序执行的流程。
 如下图所示,流程脚本都绑定在空物体上,然后绑了脚本的所有空物体都是脚本物体,把他们集中统一管理。
 一共有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();
       }        
   }
}

















