初识Unity3D中的协程
假设需要调用一个方法,正常的执行逻辑是该法一定会在返回前执行书写的逻辑,这意味着什么呢?意味着我们在执行某个方法的时候必须要在某一帧内执行完毕,这样也就说明了,直接调用一个方法不可能是在一段时间内逐步执行,比如说,使一个游戏对象的颜色实现随时间变化而变化,如果直接调用一个方法,一定是不行的,他会在一瞬间变化,
private void Fade()
{
for(float f = 1f;f>0f;f--)
{
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
}
}
那么有什么方法可以做到,让一个对象逐渐变透明吗?
1.在update里面执行,由于update是基于帧执行的,因此在这里可以实现
2.就是我们常用的 使用协程
使用协程的代码如下
IEnumerator Fade()
{
for(float f = 1f;f>0f;f--)
{
Color c = renderer.material.color;
c.a = f;
renderer.material.color = c;
yield return null;
}
}
可以看到,协程的基本形式就是这样,实际上,协程就像是一个方法,只不过他除了普通方法所具备的能力以外还有自己的独特魅力,延时,当其执行到 yield return的时候,他将会暂停逻辑执行,将控制权交还给Unity3D引擎,之后还可以从暂停的位置开始继续执行下面的逻辑。
使用StartCoroutine开始协程
方法签名如下
//
// 摘要:
// Starts a coroutine named methodName.
//
// 参数:
// methodName:
//
// value:
[ExcludeFromDocs]
public Coroutine StartCoroutine(string methodName);
//
// 摘要:
// Starts a coroutine.
//
// 参数:
// routine:
public Coroutine StartCoroutine(IEnumerator routine);
//
// 摘要:
// Starts a coroutine named methodName.
//
// 参数:
// methodName:
//
// value:
public Coroutine StartCoroutine(string methodName, [DefaultValue("null")] object value);
[Obsolete("StartCoroutine_Auto has been deprecated. Use StartCoroutine instead (UnityUpgradable) -> StartCoroutine([mscorlib] System.Collections.IEnumerator)", false)]
public Coroutine StartCoroutine_Auto(IEnumerator routine);
可以看到有不同的版本,但是主要的就是两个,一个是以Ienumerator作为参数,这样的协程开启是无参的,如果使用有参的话,需要使用参数为String,object两个参数的构造函数,分别传入要调用方法的签名与所需参数
使用StartCoroutine开始一个协程
void Update()
{
if(Input.GetKeyDown(KeyCode.F))
{
StartCoroutine("Dosomthing", 2f);
}
}
IEnumerator Dosomthing(float time)
{
for(int i =10;i>0; i--)
{
Debug.Log(i);
yield return new WaitForSeconds(time);
}
}
使用StopCoroutine停止一个协程
IEnumerator Start()
{
StartCoroutine("Dosomthing", 2f);
yield return new WaitForSeconds(1f);
}
IEnumerator Dosomthing(float time)
{
for(int i =10;i>0; i--)
{
Debug.Log(i);
yield return new WaitForSeconds(time);
}StopCoroutine("Dosomthing");
}
运行效果
由于使用了WaitForSeconds所以会有延迟的效果,需要注意的是,当代码刚刚进入的时候,首先执行依次Debug,打印出当前i的值,然后执行到“yield return new WaitForSeconds(time);”这行代码,此时遇到yield return因此协程暂停执行,并且将控制权重新交回给unity3D,需要注意的是,yield return返回的不是null,而是一个WaitForSencond类的对象,并将time作为其构造函数的对象,由于是WaitForSecond类的实例,所以协程会在time时间之后再次回到上次暂停的地方继续执行之后的代码。
当然实现延迟效果并不是只有这种方法
图片中的类都可以实现这种效果,有的效果是等到所有的摄像机和GUI被渲染完成之后,再恢复协程的执行、或等到FixedUpdate的最后一条语句执行结束后恢复协程执行等等,有兴趣的同学可以去看一下。
Unity3D协程背后的秘密–迭代器
首先想一下,什么时候需要用到迭代器?假设有一个数据容器,不管是List、Array都好,对于使用者来说,如果有一个方法,可以不用了解他的类型和内部实现就可以获取其元素,那么是最好不过了,所以迭代器应运而生,它通过持有迭代状态追踪当前的元素,并且识别下一个需要被迭代的元素,从而可以让使用者通过特定的界面询问容器中的每一个元素而不用了解底层的实现,如我们所知,迭代器是被封装再IEnumerable和IEnumberator这两个接口中,(当然还有他们的泛型模式,但是要注意的是,泛型形式显然是强类型的,且IEnumerator实现了IDisposable接口)
IEnumerator非泛型形式,代码如下
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
IEnumerator泛型形式如下
public interface IEnumerator<out T> : IEnumerator, IDisposable
{
T Current { get; }
}
IEnumerable泛型形式如下
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
非泛型形式
public interface IEnumerable
{
[DispId(-4)]
IEnumerator GetEnumerator();
}
IEnumerable接口定义了一个可以获取IEnumerator的方法GetEnumerator
而IEnumberator则再目标序列上实现循环迭代,使用的MoveNext()这个方法,以及使用current属性来实现,以知道目标序列全部迭代完成,使用这个接口可以保证我们实现最常见的Foreach,有的同学可能会很奇怪,为什么要两个接口呢?使用IEnumberable直接实现MoveNext、提供current属性不行吗?
假设有两个不同的迭代器要对同一个序列进行迭代,我们需要这两个迭代互补影响,能够正常的处理、保存等等,那么这就是IEnumerator要做的事情,委托了不违背单一职责的原则,,不让IEnumerable拥有过多的职责而陷入职责不明的境地,所以他没有MoveNext方法,为了更加直观的了解一个迭代器的执行步骤,来看下面的例子
void Start()
{
foreach(string s in GetEnumerableText())
{
Debug.Log(s);
}
}
IEnumerable<string> GetEnumerableText()
{
yield return "begin";
for(int i = 0;i<10;i++)
{
yield return i.ToString();
}
yield return "End";
}
分析:
1.Main调用GetEnumerableText()方法
2.GetEnumerable会为我们创建一个由编译器生成的新的类的实例,此时我们自己的代码并没有执行
3.Main调用MoveNext方法
4.迭代器开始执行,一直到它遇到的第一个yield return语句,此时迭代器会获取当前的值“Begin”,并且返回true告知此时还有数据
5.Main使用current属性获取数据以及打印出来
6.迭代器从上次遇到yiele return的地方开始执行,一直到遇到下一个yield return
7.迭代器按照这种方式循环,一直到MoveText返回false,用来告知此时的目标中已经没有数据了
注意
编译器会保证GetEnumerableText方法中的局部变量能够被保存,换句话说,虽然本例中的i时值类型,但是他的值起试试被迭代器保存在堆上的,这样才能保证每次调用MoveNext时,它是可用的,这也就是为什么迭代器块中的局部变量会被分配在堆上的原因
原来是状态机
首先定义一个返回IEnumerator的方法TestIterator
IEnumerator<int> TestIterator()
{
for(int i = 0; i<10;i++)
{
yield return i;
}
}
[CompilerGenerated]
private sealed class <TestIterator>d__2 : IEnumerator<int>, IEnumerator, IDisposable
{
private int <>1__state;
private int <>2__current;
public ExapleClass <>4__this;
private int <i>5__1;
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <TestIterator>d__2(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<i>5__1 = 0;
break;
case 1:
<>1__state = -1;
<i>5__1++;
break;
}
if (<i>5__1 < 10)
{
<>2__current = <i>5__1;
<>1__state = 1;
return true;
}
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
}
1.反编译之后的结果发现MoveText方法实现依托于switch语句,根据状态机的状态,执行不同的代码
2.可以看到,在调用TestIterator方法的时候,仅仅调用了d_2(编译器生成的用来实现迭代器的类)的构造函数,这个构造函数会设置迭代器的初始状态,此时参数为0,而构造函数回奖0赋值给记录迭代器状态的字段,this.<>1__state = <>1__state;这个时候,我们的代码并没有执行。
3.private sealed class < TestIterator>d__2 : IEnumerator< int>, IEnumerator, IDisposable通过这句话可以看到,这个类是实现了三个接口
4.< TestIterator >d_2类有三个字段,分别是
private int <>1__state;
private int <>2__current;
private int <i>5__1;
分别对应着,当前的状态,私有四段准总当前的值,在迭代器块中定义的局部变量i
5.MoveNext实现依托于Switch,根据状态机的状态,执行不同的代码
介绍完IEnumerator接口,再来看看IEnumerable
这次写一个返回类型为IEnumerable的按顺序返回0~9的是个数字功能的程序
// ExapleClass.<TestIterator>d__2
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
[CompilerGenerated]
private sealed class <TestIterator>d__2 : IEnumerator<int>, IEnumerator, IDisposable
{
private int <>1__state;
private int <>2__current;
public ExapleClass <>4__this;
private int <i>5__1;
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <TestIterator>d__2(int <>1__state)
{
this.<>1__state = <>1__state;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<i>5__1 = 0;
break;
case 1:
<>1__state = -1;
<i>5__1++;
break;
}
if (<i>5__1 < 10)
{
<>2__current = <i>5__1;
<>1__state = 1;
return true;
}
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
// ExapleClass.<GetEnumerableText>d__1
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
[CompilerGenerated]
private sealed class <GetEnumerableText>d__1 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable
{
private int <>1__state;
private string <>2__current;
private int <>l__initialThreadId;
public ExapleClass <>4__this;
private int <i>5__1;
string IEnumerator<string>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <GetEnumerableText>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<>2__current = "begin";
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
<i>5__1 = 0;
goto IL_008b;
case 2:
<>1__state = -1;
<i>5__1++;
goto IL_008b;
case 3:
{
<>1__state = -1;
return false;
}
IL_008b:
if (<i>5__1 < 10)
{
<>2__current = <i>5__1.ToString();
<>1__state = 2;
return true;
}
<>2__current = "End";
<>1__state = 3;
return true;
}
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
[DebuggerHidden]
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
<GetEnumerableText>d__1 result;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
result = this;
}
else
{
result = new <GetEnumerableText>d__1(0)
{
<>4__this = <>4__this
};
}
return result;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable<string>)this).GetEnumerator();
}
}
}
1.通过反编译可以看出来< TestIterator>d_2 类不仅实现了IEnumerable< int > 接口,还实现了IEnumerator< int>接口
2.IEnumerator和IEnumerator< int > 和上面的一样IEnumerator的Reset方法会抛出NotSupporteredException异常,IEnumerator和Ienumerator< int>仍然会返回<>2_current字段的值
3.多了一个private int <>l__initialThreadId;字段,这个字段会在< TestIterator >d_2中的构造函数中被赋值,用来标识创建该实例的线程
通过这些对比中能发现什么?没错我们会川江一个IEnumerable< T >的实例,然后通过一些语句例如(foreach)会调用GetEnumerator方法获取一个Enumerator< T >实例,然后去迭代数据,最终结束后式房贷哦迭代器的实例,而最初我们得到的IEnumerable< T>实例,在第一次调用GetEnumerator方法获得了一个Enumerator< T >失利之后就再也没有用到
再来分析一下IEnumerable的GetEnumerator方法
IEnumerator<string> IEnumerable<string>.GetEnumerator()
{
<GetEnumerableText>d__1 result;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
result = this;
}
else
{
result = new <GetEnumerableText>d__1(0)
{
<>4__this = <>4__this
};
}
return result;
}
可以看到,在不同的线程中调用GetEnumerator方法或者状态不是-2的时候(证明不是初始状态),会直接返回一个< TestIterator>d_2的新的实例来保存不同的状态,它要么将状态置为0并且返回this,要么就是返回一个新的实例,这个实例的初始状态置为0.
状态管理
通过上面反编译的代码可以看出,迭代器的实现主要依靠的是一个状态机,下面就来看一下一个状态机是如何管理状态的
状态切换
爹太其有四种可能的状态,分别是Before、Running、Suspended、After状态,其中Before是作为初始状态出现的
为了能够直观的观察状态的切换,下面提供一个例子
void Start()
{
Debug.Log("调用TestStateChange");
IEnumerable<int> iteratorable = TestStateChange();
Debug.Log("调用GetEnumerable");
IEnumerator<int> iterator = iteratorable.GetEnumerator();
Debug.Log("调用MoveNext方法");
bool hasNext = iterator.MoveNext();
Debug.LogFormat("是否有数据={0},current={1}",hasNext,iterator.Current);
Debug.Log("第二次调用MoveNext");
hasNext = iterator.MoveNext();
Debug.LogFormat("是否有数据={0},current={1}", hasNext, iterator.Current);
Debug.Log("第三次调用MoveNext");
hasNext = iterator.MoveNext();
Debug.LogFormat("是否有数据={0},current={1}", hasNext, iterator.Current);
}
IEnumerable<int> TestStateChange()
{
Debug.Log("我TestStateChange是第一行代码");
Debug.Log("我是第一个yieldReturn前的代码");
yield return 1;
Debug.Log("我是第一个yieldReturn后的代码");
Debug.Log("我是第二个yieldReturn前的代码");
yield return 2;
Debug.Log("我是第一个yieldReturn后的代码");
}
运行结果
进行反编译
看GetEnumerator方法
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
<TestStateChange>d__1 result;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
result = this;
}
else
{
result = new <TestStateChange>d__1(0)
{
<>4__this = <>4__this
};
}
return result;
}
最开始的状态时-2,但是调用完这个方法之后,状态值变成了0
再看MoveNext方法
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
UnityEngine.Debug.Log("我TestStateChange是第一行代码");
UnityEngine.Debug.Log("我是第一个yieldReturn前的代码");
<>2__current = 1;
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
UnityEngine.Debug.Log("我是第一个yieldReturn后的代码");
UnityEngine.Debug.Log("我是第二个yieldReturn前的代码");
<>2__current = 2;
<>1__state = 2;
return true;
case 2:
<>1__state = -1;
UnityEngine.Debug.Log("我是第一个yieldReturn后的代码");
return false;
}
}
可以清楚的看到装袋的值都是从0->1->2->-1,而且在每个分支中<>1_state都会首先赋值为-1,然后根据不同的状态再赋值为其他值
总结一下关于迭代器内部状态机的状态切换
-2状态:只有IEnumerable才有,它表明的是初始状态,也就是第一次调用GetEnumerator之前的专改
-1状态:他也是C#语言标准中规定的Running状态,表明当前迭代器正在运行,当然也可能是After状态,如
case 2:
<>1__state = -1;
UnityEngine.Debug.Log("我是第一个yieldReturn后的代码");
return false;
这力<>1_state被赋值为-1,但是此时已经没有数据,结束了
0状态:是Before状态,表明还未开始使用MoveNext。
通过以上分析,相信应该可以很好的了解协程与协程后的迭代器了,本文结束。
参考Unity3D脚本编程;