原文: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 = iterator.MoveNext();
        Console.WriteLine("\nReturned value = {0}; Current = {1}", returnedValue, iterator.Current);
  
        Console.WriteLine("Calling MoveNext() again...\n");
        returnedValue = iterator.MoveNext();
        Console.WriteLine("\nReturned value = {0}; Current = {1}", returnedValue, iterator.Current);
  
        Console.WriteLine("Calling MoveNext() again...\n");
        returnedValue = iterator.MoveNext();
        Console.WriteLine("\nReturned value = {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进行封装(一)的更多相关文章

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

    原文:http://gad.qq.com/program/translateview/7170970 译者:王磊(未来的未来)    审校:崔国军(飞扬971)   在上一篇文章中,我们的注意力主要是 ...

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

    原文:http://gad.qq.com/program/translateview/7170967 译者:崔国军(飞扬971)    审校:王磊(未来的未来) 在这个系列的最后一部分文章,我们要通过 ...

  3. Unity 中的协同程序

    今天咱就说说,协同程序coroutine.(这文章是在网吧敲的,没有unity,但是所有结论都被跑过,不管你信得过我还是信不过我,都要自己跑一下看看,同时欢迎纠错)先说说啥是协程:协同程序是一个非常让 ...

  4. Lua中的协同程序

    [前言] 协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈.局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西.从概念上讲,线程与协同程序的主要区别在于,一个具有多个线程的 ...

  5. Lua中的协同程序 coroutine

    Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换.不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时 ...

  6. Lua中的协同程序 coroutine(转)

    Lua中的协程和多线程很相似,每一个协程有自己的堆栈,自己的局部变量,可以通过yield-resume实现在协程间的切换.不同之处是:Lua协程是非抢占式的多线程,必须手动在不同的协程间切换,且同一时 ...

  7. 小程序 请求Promise简单封装

    最近做小程序在调用后台接口的时候感觉总写很长一串,很冗杂.非常想念vue中promise封装的写法,于是自己初步封装了一下. 1.url 接口地址 2.headers请求头 3. params 请求参 ...

  8. 【转】关于Unity协同程序(Coroutine)的全面解析

    http://www.unity.5helpyou.com/2658.html 本篇文章我们学习下unity3d中协程Coroutine的的原理及使用 1.什么是协调程序 unity协程是一个能暂停执 ...

  9. Unity3D协同程序(Coroutine)

    摘要下: 1. coroutine, 中文翻译"协程".这个概念可能有点冷门,不过百度之,说是一种很古老的编程模型了,以前的操作系统里进程调度里用到过,现在操作系统的进程调度都是根 ...

随机推荐

  1. 餐厅点餐系统app第二天

    队友: 郭志豪:http://www.cnblogs.com/gzh13692021053/ 杨子健:http://www.cnblogs.com/yzj666/ 刘森松:http://www.cnb ...

  2. storm安装笔记以及提交拓扑任务

    Storm -- Distributed and fault-tolerant realtime computation 这是一个分布式的.容错的实时计算系统 把Storm依赖组件的版本贴出来供各位参 ...

  3. 【BZOJ4034】[HAOI2015]树上操作 树链剖分+线段树

    [BZOJ4034][HAOI2015]树上操作 Description 有一棵点数为 N 的树,以点 1 为根,且树点有边权.然后有 M 个 操作,分为三种: 操作 1 :把某个节点 x 的点权增加 ...

  4. [转]crontab环境变量设置

    原文连接:http://blog.csdn.net/zc02051126/article/details/20480289 come from http://www.360doc.com/conten ...

  5. PE的一些水 3-50

    T3: 分解质因数. lalala T4: 暴模. 然而数学方法怎么搞?---->也就是怎么手算?... 于是看了一下讨论区...发现原来我的数学已经低于小学生水平了... 我们把答案abccb ...

  6. 20145304 Java第九周学习报告

    20145304<Java程序设计>第九周学习总结 教材学习内容总结 JDBC简介 JDBC全名Java DataBase Connectivity,是Java联机数据库的标准规范.定义了 ...

  7. ubuntu 安装 Terminator

    sudo apt-get install terminator Ctrl-Shift-E: 垂直分割Ctrl-Shift-O: 水平分割Ctrl-Shift-P: 激活先前的窗口Ctrl-Shift- ...

  8. NOIp 2013 #2 花匠 Label:爆0的Water

    题目描述 花匠栋栋种了一排花,每株花都有自己的高度.花儿越长越大,也越来越挤.栋栋决定 把这排中的一部分花移走,将剩下的留在原地,使得剩下的花能有空间长大,同时,栋栋希 望剩下的花排列得比较别致. 具 ...

  9. 【BZOJ】3239: Discrete Logging

    http://www.lydsy.com/JudgeOnline/problem.php?id=3239 题意:原题很清楚了= = #include <bits/stdc++.h> usi ...

  10. 数组的Clone方法

    public void Test() { ,,}; var arr2 = arr1; var arr3 = (int[])arr1.Clone(); //浅拷贝 arr1[] = ; //arr2[0 ...