Unity 新手入门 如何理解协程 IEnumerator yield
协程是什么,能干什么?
为了能通俗的解释,我们先用一个简单的例子来看看协程可以干什么
首先,我突发奇想,要实现一个倒计时器,我可能是这样写的:
public class CoroutineTest : MonoBehaviour
{
public float sumtime = 3;
void Update()//Update是每帧调用的
{
{
sumtime -= Time.deltaTime;
if (sumtime <= 0)
Debug.Log(\"Done!\");
}
}
}
我们知道,写进 Update() 里的代码会被每帧调用一次,
所以,让总时间sumtime在Update()中每一帧减去一个增量时间Time.deltaTime(可以理解成帧与帧的间隔时间)就能实现一个简单的倒计时器
但是,当我们需要多个独立的计时器时,用同样的思路,我们的代码可能就会写成这样:
public class CoroutineTest : MonoBehaviour
{
public float sumtime1 = 3;
public float sumtime2 = 2;
public float sumtime3 = 1;
void Update()
{
sumtime1 -= Time.deltaTime;
if (sumtime1 <= 0)
Debug.Log(\"timer1 Done!\");
sumtime2 -= Time.deltaTime;
if (sumtime2 <= 0)
Debug.Log(\"timer2 Done!\");
sumtime3 -= Time.deltaTime;
if (sumtime3 <= 0)
Debug.Log(\"timer3 Done!\");
}
}
重复度很高,计时器越多看的越麻烦
然后有朋友可能会提到,我们是不是可以用一个循环来解决这个问题
for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime)
{
//nothing
}
Debug.Log(\"This happens after 5 seconds\");
现在每一个计时器变量都成为for循环的一部分了,这看上去好多了,而且我不需要去单独设置每一个跌倒变量。
但是
但是
但是
我们知道Update() 是每帧调用一次的,我们不能把这个循环直接写进Update() 里,更不能写一个方法在Update() 里调用,因为这相当于每帧开启一个独立的循环
好了,你可能知道我想说什么了,我们正好可以用协程来干这个
public class CoroutineTest : MonoBehaviour
{
void Start()
{
StartCoroutine(Count3sec());
}
IEnumerator Count3sec()
{
for (float sumtime = 3; sumtime >= 0; sumtime -= Time.deltaTime)
yield return 0;
Debug.Log(\"This happens after 3 seconds\");
}
}
你很可能看不懂上面的几个关键字,但不急,我们一个个解释上面的代码干了什么
StartCoroutine(Count3sec());
这一句用来开始我们的Count3sec方法
然后你可能想问的是
- IEnumerator 是什么?返回值是什么?
- For循环中的yield return是什么?
理解以下的话稍有难度,但暂时理解不了问题也不大
详细的讲:
IEnumerator 是C#的一个迭代器,你可以把它当成指向一个序列的某个节点的指针,C#提供了两个重要的接口,分别是Current(返回当前指向的元素)和 MoveNext()(将指针向前移动一个单位,如果移动成功,则返回true)IEnumerator是一个interface接口,你不用担心的具体实现
通常,如果你想实现一个接口,你可以写一个类,实现成员,等等。迭代器块(iterator block) 是一个方便的方式实现IEnumerator,你只需要遵循一些规则,并实现IEnumerator由编译器自动生成。
一个迭代器块具备如下特征:
- 返回IEnumerator
- 使用yield关键字
那么yield关键字是干嘛的?它用来声明序列中的下一个值,或者一个无意义的值。如果使用yield x(x是指一个具体的对象或数值)的话,那么movenext返回为true并且current被赋值为x,如果使用yield break使得movenext()返回false(停止整个协程)
看不太懂?问题不大
简单来说:
你现在只需要理解,上面代码中,IEnumerator类型的方法Count3sec就是一个协程,并且可以通过yield关键字控制协程的运行
一个协程的执行,可以在任何地方用yield语句来暂停,yield return的值决定了什么时候协程恢复执行。通俗点讲,当你“yield”一个方法时,你相当于对这个程序说:“现在停止这个方法,然后在下一帧中,从这里重新开始!”
yield return 0;
然后你可能会问,yield return后面的数字表示什么?比如yield return 10,是不是表示延缓10帧再处理?
并不!
并不!
并不!
yield return 0表示暂缓一帧,也就是让你的程序等待一帧,再继续运行。(不一定是一帧,下面会讲到如何控制等待时间)就算你把这个0换成任意的int类型的值,都是都是表示暂停一帧,从下一帧开始执行
它的效果类似于主线程单独出了一个子线程来处理一些问题,而且性能开销较小
现在你大致学会了怎么开启协程,怎么写协程了,来看看我们还能干点什么:
IEnumerator count5times()
{
yield return 0;
Debug.Log(\"1\");
yield return 0;
Debug.Log(\"2\");
yield return 0;
Debug.Log(\"3\");
yield return 0;
Debug.Log(\"4\");
yield return 0;
Debug.Log(\"5\");
}
在这个协程中,我们每隔一帧输出了一次Hello,当然你也可以改成一个循环
IEnumerator count5times()
{
for (int i = 0; i < 5; i++)
{
Debug.Log(\"i+1\");
yield return 0;
}
}
重点来了,有意思的是,你可以在这里加一个记录始末状态的变量:
public class CoroutineTest : MonoBehaviour
{
bool isDone = false;
IEnumerator count5times()
{
Debug.Log(isDone);
for (int i = 0; i < 5; i++)
{
Debug.Log(\"i+1\");
yield return 0;
}
isDone = true;
Debug.Log(isDone);
}
void Start()
{
StartCoroutine(count5times());
}
}
很容易看得出上面的代码实现了什么,也就就是我们一开始的需求,计时器
这个协程方法突出了协程一个“非常有用的,和Update()不同的地方:方法的状态能被存储,这使得方法中定义的这些变量(比如isUpdate)都会保存它们的值,即使是在不同的帧中
再修改一下,就是一个简单的协程计时器了
public class CoroutineTest : MonoBehaviour
{
IEnumerator countdown(int count, float frequency)
{
Debug.Log(\"countdown START!\");
for (int i = 0; i < count; i++)
{
for (float timer = 0; timer < frequency; timer += Time.deltaTime)
yield return 0;
}
Debug.Log(\"countdown DONE!\");
}
void Start()
{
StartCoroutine(countdown(5, 1.0f));
}
}
在上面的例子我们也能看出,和普通方法一样,协程方法也可以带参数
你甚至可以通过yield一个WaitForSeconds()更方便简洁地实现倒计时
协程计时器
public class CoroutineTest : MonoBehaviour
{
IEnumerator countdown(float sec)//参数为倒计时时间
{
Debug.Log(\"countdown START!\");
yield return new WaitForSeconds(sec);
Debug.Log(\"countdown DONE!\");
}
void Start()
{
StartCoroutine(countdown(5.0f));
}
}
好了,可能你已经注意到了,yield的用法还是很多的
在此之前,我们之前的代码yield的时候总是用0(或者可以用null),这仅仅告诉程序在继续执行前等待下一帧。现在你又学会了用yield return new WaitForSeconds(sec)来控制等待时间,你已经可以做更多的骚操作了!
协程另外强大的一个功能就是,你甚至可以yeild另一个协程,也就是说,你可以通过使用yield语句来相互嵌套协程,
public class CoroutineTest : MonoBehaviour
{
IEnumerator SaySomeThings()
{
Debug.Log(\"The routine has started\");
yield return StartCoroutine(Wait(1.0f));
Debug.Log(\"1 second has passed since the last message\");
yield return StartCoroutine(Wait(2.5f));
Debug.Log(\"2.5 seconds have passed since the last message\");
}
IEnumerator Wait(float waitsec)
{
for (float timer = 0; timer < waitsec; timer += Time.deltaTime)
yield return 0;
}
void Start()
{
StartCoroutine(SaySomeThings());
}
}
yield return StartCoroutine(Wait(1.0f));
这里的Wait指的是另一个协程,这相当于是说,“暂停执行本程序,等到直到Wait协程结束”
协程控制对象行为
根据我们上面讲的特性,协程还能像创建计时器一样方便的控制对象行为,比如物体运动到某一个位置
IEnumerator MoveToPosition(Vector3 target)
{
while (transform.position != target)
{
transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
yield return 0;
}
}
我们还可以让上面的程序做更多,不仅仅是一个指定位置,还可以通过数组来给它指定更多的位置,然后通过MoveToPosition() ,可以让它在这些点之间持续运动。
我们还可以再加入一个bool变量,控制在对象运动到最后一个点时是否要进行循环
再把上文的Wait()方法加进来,这样就能让我们的对象在某个点就可以选择是否暂停下来,停多久,就像一个正在巡逻的守卫一样 (这里没有实现,各位读者可以尝试自己写一个)
public class CoroutineTest : MonoBehaviour
{
public Vector3[] path;
public float moveSpeed;
void Start()
{
StartCoroutine(MoveOnPath(true));
}
IEnumerator MoveOnPath(bool loop)
{
do
{
foreach (var point in path)
yield return StartCoroutine(MoveToPosition(point));
}
while (loop);
}
IEnumerator MoveToPosition(Vector3 target)
{
while (transform.position != target)
{
transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);
yield return 0;
}
}
IEnumerator Wait(float waitsec)
{
for (float timer = 0; timer < waitsec; timer += Time.deltaTime)
yield return 0;
}
}
yield其他
这里列举了yield后面可以有的表达式
-
null,0,1,...... 暂缓一帧,下一帧继续执行
- WaitForEndOfFrame - the coroutine executes on the frame, after all of the rendering and GUI is complete 等待帧结束
- WaitForFixedUpdate - causes this coroutine to execute at the next physics step, after all physics is calculated 等待一个固定帧
- WaitForSeconds - causes the coroutine not to execute for a given game time period
- WWW - waits for a web request to complete (resumes as if WaitForSeconds or null)
-
StartCoroutine(Another coroutine) - in which case the new coroutine will run to completion before the yielder is resumed 等待另一个协程暂停
值得注意的是 WaitForSeconds()受Time.timeScale影响,当Time.timeScale = 0f 时,yield return new WaitForSecond(x) 将不会满足
停止协程
- StopCoroutine(string methodName);
- StopAllCoroutine();
- 设置gameobject的active为false时可以终止协同程序,但是再次设置为true后协程不会再启动。
总结一下
协程就是:你可以写一段顺序代码,然后标明哪里需要暂停,然后在指定在下一帧或者任意间后,系统会继续执行这段代码
通过协程我们可以方便的做出一个计时器,甚至利用协程控制游戏物体平滑运动
如果你刚接触协程,我希望这篇博客能帮助你了解它们是如何工作的,以及如何来使用它们
深入讲一下
待更新