在Hierarchy层级面板上组织业务脚本流程——使用async和await

〇、把多个不同的脚本串联在一起顺序阻塞执行

把很多个脚本串联在一起,让他们按照先后顺序执行,等着前面的执行完毕,在执行后面的,这就是用【异步方法】实现的,能够顺序执行的流程。
如下图所示,流程脚本都绑定在空物体上,然后绑了脚本的所有空物体都是脚本物体,把他们集中统一管理。
一共有14个脚本,他们会按照先后顺序有条不紊的排队执行。
在Hierarchy层级面板上组织业务脚本流程——使用async和await_第1张图片

一、多个脚本按照先后顺序排队执行是如何实现的——原理

(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;

/// 
/// 接口:定义一个叫FlowAsync的异步方法
/// 
public interface IFlowAsync
{
    public UniTask FlowAsync(CancellationToken ctk);
}

只继承接口,而不实现接口的后果:
在Hierarchy层级面板上组织业务脚本流程——使用async和await_第2张图片

二、多个脚本按照先后顺序排队执行是如何实现的——代码

需求:如下图所示,我有14条流程,它们都挂在14个空物体上,这些空物体都挂在另一个叫【流程内容】的空物体下。当我启动【流程内容】节点上的脚本时,该脚本自动加载下面的子物体,然后按照先后顺序执行子物体上的脚本。
在Hierarchy层级面板上组织业务脚本流程——使用async和await_第3张图片

(1)流程脚本的一个父节点如何实现?

  • 父脚本定义一个【脚本列表】,用来装子物体的脚本
    /// 
    /// 流程脚本列表
    /// 
    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();
    }        
}

(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}");
    }
}

三、流程的组织举例

主流程
在Hierarchy层级面板上组织业务脚本流程——使用async和await_第4张图片
其中第二个脚本节点包含子流程
在Hierarchy层级面板上组织业务脚本流程——使用async和await_第5张图片

四、部分代码清单

(1)流程父节点Step清单

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();
        }        
    }
}

(2)物体绕自身某个轴的旋转

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();
    }
}

  • 上面代码中用到的DoRotate方法的实现
/// 
/// 物体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();
    }
} 

五、思路扩展

(1)流程控制思考

如果是单线的顺序流程,则很方便,如果中间包含分支执行呢,那就借鉴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();
        }        
    }
}

(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;

/// 
/// 功能介绍:异步流程——等待按钮被点击, 等待一个或者多个按钮被点击
/// 脚本参数:【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();
       }        
   }
}

你可能感兴趣的:(unity,unitask,异步,流程)