首页 > 代码库 > 【转】Unity中的协同程序-使用Promise进行封装(一)

【转】Unity中的协同程序-使用Promise进行封装(一)

原文:http://gad.qq.com/program/translateview/7170767

译者:陈敬凤(nunu)    审校:王磊(未来的未来)

每个Unity的开发者应该都对协同程序非常的熟悉。对于很多Unity的开发者而言,协同程序就是用来编写大量异步和延时任务的一种方法。如果你不在乎速度的话,有非常非常多的特殊方法可以在任何所需的时间暂停和恢复执行。在实践中,它们可以营造一种并发函数的幻觉 (虽然他们与线程无关!)。然而,协同程序会有一些问题,许多程序员在使用协同程序的时候会偶然发现。让我们仔细看看这些问题。

 

协同程序的内部机制

那么在Unity协同程序的内部到底发生了什么?Unity协同程序到底是如何工作的?我们没有直接访问Unity源代码的权利,但是我们可以从手册中收集证据,并且通过C#的知识我们可以或多或少的假设它们到底是如何工作的。让我们尽量精简这个示例代码:

1
2
3
4
5
6
7
8
9
StartCoroutine(TestCoroutine());
 
//…
IEnumerator TestCoroutine()
{
    Debug.Log("Hello there!");
    yield return new WaitForSeconds(2);
    Debug.Log("Hello from future!");
}

 

你不需要什么特殊的才能,就能很容易的看出这段代码将在终端部分打印出“Hello there!”并在2秒之后打印出” Hello from future! “。但它是如何做到这一点的?要理解协同程序必须首先看下函数的签名——更准确地说,是函数的返回类型。IEnumerator作为一种对集合进行迭代的方法。它控制着从一个对象的执行转移到序列中下一个对象的执行。

为了做到这一点,它声明了两个非常重要的成员变量:一个是Current(当前)属性,它会引用枚举器(或者可以说是游标)目前正在访问的元素,另外一个是MoveNext()函数,它在移动到下一个元素的同时会计算新的Current(当前)值。它也有一个Reset()函数,这个函数会负责将枚举器设置到它的初始位置,但是我们跳过这一部分。

现在由于IEnumerator只是一个接口,并不显式地指定当前类型(除了是一个对象以外我们一无所知)。我们可以做任何我们想要的事情来计算下一个对象。MoveNext()函数只会做这项工作,并且我们已经访问到序列的最后一个元素的时候会返回fasle。

 

迭代器模块

如果是在一个纯C#的环境,我们可以轻松地在一个特化的迭代器模块里面“实现”这个接口。在实践中,c#编译器会将迭代器模块转换成状态机。这是一些会返回IEnumerator类型并且使用yield return语句来返回值的函数(是的,有可能是多个值)。

调用会使MoveNext()函数只是简单的返回true,并且无论你是从哪返回都将当前的位置保存到Current变量里面。如果你想要停止枚举器的话,你可以简单的调用yield break,这将确保MoveNext()函数返回false并终止整个序列(可以把它想象成在一个循环中进行break)。下面是这么做的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;
using System.Collections.Generic;
  
class TestIEnumerator
{
    static readonly string inEnumerator = "####";
  
    static IEnumerator GetTexts()
    {
        Console.WriteLine(inEnumerator + "First line of GetTexts()" + inEnumerator);
  
        Console.WriteLine(inEnumerator + "Just before the first yield" + inEnumerator);
        yield return "First returned text";
        Console.WriteLine(inEnumerator + "Just after the first yield" + inEnumerator);
  
        Console.WriteLine(inEnumerator + "Just before the second yield" + inEnumerator);
        int b = 2;
        int a = 5 + b;
        yield return "Second returned text - " + a;
        Console.WriteLine(inEnumerator + "Just after the second yield" + inEnumerator);
    }
  
    static void Main()
    {
        Console.WriteLine("Calling GetTexts()");
        IEnumerator iterator = GetTexts();
        Console.WriteLine("Calling MoveNext()...\n");
        bool returnedValue = http://www.mamicode.com/iterator.MoveNext();
        Console.WriteLine("\nReturned value = http://www.mamicode.com/{0}; Current = {1}", returnedValue, iterator.Current);
  
        Console.WriteLine("Calling MoveNext() again...\n");
        returnedValue = http://www.mamicode.com/iterator.MoveNext();
        Console.WriteLine("\nReturned value = http://www.mamicode.com/{0}; Current = {1}", returnedValue, iterator.Current);
  
        Console.WriteLine("Calling MoveNext() again...\n");
        returnedValue = http://www.mamicode.com/iterator.MoveNext();
        Console.WriteLine("\nReturned value = http://www.mamicode.com/{0} - stopping", returnedValue);
  
        Console.ReadKey();
    }
}

在你自己的电脑上编译这个程序,你会得到以下输出:

 

下面的例子使用了通用接口IEnumerator,但是正如前面所描述的那样,你可以使用常规的IEnumerator接口,在这个接口里面你的Current变量可以是任意类型。这也正是Unity对于协同程序的要求。

所以,请记住这一点,我们对于Unity到底与协同程序做了什么开始有了一个比较清晰的认识。StartCoroutine函数将协同程序添加到某个容器之中。Unity遍历StartCoroutine中执行的每一个协同程序并执行这些协同程序的MoveNext()函数,而这些函数会来继续执行他们之前中断的工作。正如上面的例子所显示的那样,它会在yield return语句之间评估表达式的值并返回一个值。如果它返回的是false的话,那么显然是在告诉Unity中止这个协同程序(它只是刚刚完成而已)。如果它返回的是true的话,,它会检查当前的属性(记住,这是一个非泛型接口!)并且看下是否有熟悉的类型。还记得第一个例子之中的WaitForSeconds()吗?Unity看到这个函数然后暂停了这个协同程序两秒钟。事实上,它实际上是从YieldInstruction基类型继承而来,你还有以下Unity可以识别的类型:

1) WaitForEndForFrame:在所有的摄像机和GUI都被渲染之后,会在这一帧的结尾来继续这个协同程序。

2) WaitForFixedUpdate:会等到下一个以固定帧速率更新的函数。

3) Coroutine类型自身,这是一个你可以用在之后协同程序的信息。

4) CustomYieldInstruction:这是引入用来写你自己的自定义yield语句-仅仅需要继承这个类然后覆盖keepWaiting属性。

 

所以焦点在哪里?

不用说,通过这些简单的类型你可以很容易的编写一些基于时间的游戏逻辑以及各种异步事件。你可以通过短短几行代码不使用任何肮脏的计时器在几秒钟后展示一个UI通知。不仅如此,你甚至可以返回UnityWebRequest.Send()函数并暂停你的函数直到你得到一个HTTP响应。你可能会问“这有什么问题吗?”

 

首先,你不能轻易地从协同程序返回一个值。函数签名需要返回IEnumerator来保持跟踪应该在何时何恢复你的方法。其次,在协同程序级别没有异常处理。你不能在try。。。catch块里面使用yield语句。基本上你要试着在协同程序里面的每一个non-yield语句里面尝试使用try。。。catch块。但是如果有多个表达式通过多个yield语句隔离开来怎么办?或者你想要一个接一个的将协同程序放入堆栈。但如果最上面的协同程序遇得到了一个运行时异常的时候该如何沿着管线沟通下面的协同程序?

 

幸运的是,这个问题可以有一个解决方案。在接下来的部分我们会看到Promise,它们最初是出现在Javascript之中。请在 这 里 阅读这部分的内容。

 

【转】Unity中的协同程序-使用Promise进行封装(一)