我在前一篇文章中尝试摸索了如何处理Unity Coroutine返回值的问题,虽然有几种方式来实现接受耗时任务的结果返回值,但是总觉得比较勉强,特别是如果EventHanler在几层Coroutine嵌套的情况下会搞得晕头转向。


双核Frank:Unity Coroutine返回值如何处理zhuanlan.zhihu.com


因此继续爬网络寻找前人的经验。

Async/Await是目前已经非常成熟的异步编程模式,其核心价值并非在于提升程序运行的速度,而是使代码的结构变得非常合乎我们平常的习惯,当你对这个模式有个基本认识的时候,你会发现一切就像刘皇叔差遣赵子龙跑快递然后坐等回信这么简单清晰。

经过搜索,发现网上已经有牛人对Coroutine、IEnumerator进行了扩展,使其可以轻松地被封装到Async/Await模式之中。

游戏蛮牛在几年前就有此项目的中文翻译:

【Unity 2017中使用Async-Await替代 coroutines】-蛮牛译馆-【游戏蛮牛】-游戏出海,ar增强现实,虚拟现实,unity3d,unity3d教程下载首选u3d,unity3d官网 - Powered by Discuz!www.manew.com


顺着上述博文爬到英文的原文:


http://www.stevevermeulen.com/index.php/2017/09/using-async-await-in-unity3d-2017/www.stevevermeulen.com

引用一下其博文中的代码示例,就能看出如何简单地使unity中的很多常见协程变成可await等待:


await new WaitForSeconds(1.0f);
        await CustomCoroutineAsync();
        var value = (string)(await CustomCoroutineWithReturnValue());
        var returnCode = await Process.Start("notepad.exe");
        await SceneManager.LoadSceneAsync("scene2");
        var assetBundle = await GetAssetBundle("www.my-server.com/myfile");
        var prefab = await assetBundle.LoadAssetAsync<GameObject>("myasset");
        GameObject.Instantiate(prefab);
        assetBundle.Unload(false);


具体可以克隆下GitHub上的源代码来慢慢研究。


https://github.com/svermeulen/Unity3dAsyncAwaitUtilgithub.com

整个项目还是比较完整的,核心有几点:

  1. 基于C#实现自定义Async/Await的要求,完成一个实现了INotifyCompletion接口的Awaiter(SimpleCoroutineAwaiter),这是关键。
  2. 利用Extensions扩展了unity常见的几个协程类,例如WaitForSeconds,WaitForUpdate,AsyncOperation,AssetBundleCreateRequest等。

由于个人技术底子薄,整个项目代码看下来还是很多地方没有彻底理解,但是摸索着利用项目中的代码实现了一个最简单版本的UnityWebReqeustResourceLoader资源加载类,其目的是搞清楚CustomAwaiter是如何工作的。

首先要写一个Awaiter类(需要引用System.Runtime.CompilerServices库):


public class CustomAwaiter<T> : INotifyCompletion
{
    public CustomAwaiter<T> GetAwaiter()
    {
        return this;
    }

    bool _isDone;
    Exception _exception;
    Action _continuation;
    T _result;

    public bool IsCompleted
    {
        get { return _isDone; }
    }

    public T GetResult()
    {
        return _result;
    }

    public void Complete(T result, Exception e)
    {
        _isDone = true;
        _exception = e;
        _result = result;
        if (_continuation != null)
        {
            _continuation();
        }
    }
    void INotifyCompletion.OnCompleted(Action continuation)
    {
        _continuation = continuation;
    }
}


这里必须实现INotifyCompletion接口所指定的OnCompleted方法,以及GetAwaiter、IsCompleted、GetResult、Complete方法和属性(不要问为什么,我也没太搞明白,特别是Complete中必须调用一下_continuation)。

其次是实现具体业务逻辑的类,例如加载资源。


public class UnityWebReqeustResourceLoader : MonoBehaviour
{
    public UnityWebReqeustResourceLoader()
    {   }

    public CustomAwaiter<Texture2D> GetTexture2D(string url)
    {
        var awaiter = new CustomAwaiter<Texture2D>();
        StartCoroutine( LoadTexture2D(url, awaiter));
        return awaiter;
    }

    private IEnumerator LoadTexture2D(string resourceInfo, CustomAwaiter<Texture2D> awaiter)
    {
        UnityWebRequest request = UnityWebRequestTexture.GetTexture(Application.persistentDataPath + "/" + resourceInfo); ;
        yield return request.SendWebRequest();

        if (request.isHttpError || request.isNetworkError)
        {
            Debug.Log(request.error);
        }
        else
        {
            Thread.Sleep(2000);
            var texture = DownloadHandlerTexture.GetContent(request);
            awaiter.Complete(texture, null);
        }
    }
}


这个业务类中,实现同一个逻辑的方法要分成两块来写,一个是传统Unity协程方法的写法,返回值类型是IEnumerator迭代器;另一部分则是一个Awaiter返回值的方法。这样,才能够在主程序中以await的方式来调用。

最后是主程序(挂在一个场景中的GameObject上)。注意这里已经可以是await xxx()的写法了,看起来是不是清晰明了!


public class CustomAsyncAwaitDemo : MonoBehaviour
{
    public Text caption;
    public Slider slider;
    public RawImage imageBox;
    public Texture2D texture2d;

    public UnityWebReqeustResourceLoader loader;

    async void Start()
    {
        caption.text = "Custom Async/Await Demo";
        gameObject.AddComponent<UnityWebReqeustResourceLoader>();
        loader = gameObject.GetComponent<UnityWebReqeustResourceLoader>();

        var result = await loader.GetTexture2D("/header-7.jpg");
        imageBox.texture = result;
    }

    private void Update()
    {
        slider.value += 0.01f;
    }
}


对于自定义Async/Await封装Unity协程的过程,我分析大致如下:


unity 圆角UI组件_c# imager让图片有圆角unity


上测试视频:

知乎视频www.zhihu.com



因为是最简实现版本,感觉在加载图片的时候卡了一下,而svermeulen的开源项目中利用SynchronizationContext把耗时任务转到了另外的线程,应该不会出现卡的现象,有兴趣的朋友看看他的SyncContextUtil.cs和AsyncCoroutineRunner.cs这几段代码。


更新一下视频,发现原来卡是因为demo中用Thread.Sleep来模拟耗时任务,而unity应该就是单线程,当然卡。采用WaitForSeconds就可以了。