异步
Invoke
Invoke方法
public void MonoBehaviour.Invoke(string methodName, float time);
public void MonoBehaviour.InvokeRepeating(string methodName, float time, float repeatRate);
public void MonoBehaviour.CancelInvoke();
public void MonoBehaviour.CancelInvoke(string methodName);
public bool MonoBehaviour.IsInvoking(string methodName);
Invoke是一组将函数交给Unity架构运作的方法,使用Invoke调用的函数是脱离组件生命周期的。
Invoke委派了在物理时间经过time秒后调用methodName方法。
InvokeRepeating委派了物理时间经过time秒后调用methodName方法,随后每repeatRate秒再调用一次。
CancelInvoke可以停止由本脚本委派的所有Invoke函数和InvokeRepeating函数,如果加上参数,则取消对应类型的函数。
IsInvoking可以返回本脚本是否已经委派了至少一个Invoke函数。
以下是使用Invoke实现灯光闪烁的效果的例子:
public class LightBlink : MonoBehaviour
{
Light light;
private void Start()
{
light = GetComponent<Light>();
TurnOnLight();
}
private void TurnOnLight()
{
light.enabled = true;
Invoke("TurnOffLight",2);
}
private void TurnOffLight()
{
light.enabled = false;
Invoke("TurnOnLight",2);
}
}
以下是使用Invoke实现使物体逐渐缩小的效果的例子:
public class ShrinkEffect : MonoBehaviour
{
private void Start()
{
InvokeRepeating("Shrink", 0, 0.1f);
Invoke("StopShrink",5);
}
private void Shrink()
{
transform.localScale *= 0.95f;
}
private void StopShrink()
{
CancelInvoke("Shrink");
}
}
如果Invoke时不能找到需要找到的函数,不会抛出运行时异常,但会在Console面板输出一条信息。
Invoke的最大缺点在于被委派的函数只能是无参数函数。但由于Invoke的调用是脱离生命周期的,所以当组件enable为false或物体activeSelf为false时,Invoke可以继续执行,相反的,协程是不可以的。如果需要在SetActive(true)前进行一些与本体无关的额外操作而需要推迟SetActive(true)时(如登场时的光效动画),可以使用Invoke代替Coroutine。
Coroutine
协程和线程
协程和多线程不同,协程由Unity框架提供,在组件生命周期中被执行,协程的处理在物理循环或逻辑循环之后,具体在何时处理由协程的构建方式决定;而线程则由操作系统调度。所有涉及组件生命周期的功能都应该使用协程而不是线程实现。
协程和线程都可以达成表面上的并行效果,但协程的切片是开发者手动进行的,可以预期切片的次数和位置,以及每个切片在每一帧的运行时间,可以保证在功能A所有语句运行完后再执行功能B的语句;多线程中切片由操作系统控制,线程是交错运行的,难以保证语句的顺序。
协程始终属于同一个游戏线程,所以使用大量协程时并不能充分利用硬件多核特性,而多线程可以。
使用线程而不是协程的情况:
(1) 在不用任何插件的情况下实现网络通讯或http请求。
(2) 加载外部文件,调用其他软件(比如内付调用支付软件)。
(3) 十分耗时的,在单线程中难以保证流畅度的运算(大规模查找和排序等)。
迭代器IEnumerator
我们通过迭代器来实现程序的切片,使用不同yield语句可以将切片分配给不同的处理方式。程序在运行到yield语句时会"挂起",等待调节合适时再继续运行。
使用StartCoroutine来开启一个协程:
public Coroutine MonoBehaviour.StartCorountine(IEnumerator routine);
public Coroutine MonoBehaviour.StartCorountine(string methodName; object value = null);
协程开启后会由本组件的生命周期执行,开启协程之后的语句不会受协程暂停。
在组件A中使用StartCoroutine调用组件B中的迭代器开启协程,这个协程将会由组件A的生命周期执行。
构造一个迭代器,我们需要声明一个返回值为IEnumerator的方法,然后通过yield return语句来标记需要挂起的位置和唤醒的模式,下图中的组件展示了协程的逻辑和执行顺序:
public class CoroutineExample : MomoBehaviour
{
private void Start()
{
Debug.Log("Invoke corountine.");
StartCoroutine(RoutineExample());
Debug.Log("Finish invoke coroutine.");
}
private IEnumerator RoutineExample()
{
Debug.Log("Coroutine Start.");
yield return new WaitForSeconds(3);
Debug.Log("Coroutine End");
}
}
上述代码中yield return new WaitForSeconds(3)可以将协程挂起,在3秒后再执行后续代码。
yield return的方式不同,挂起的方式也不同
yield return 0; yield return null; yield return;
这三种方法可以将协程挂起,在下一个逻辑帧Update后继续执行后续代码。它们不会受timeScale影响。
yield return new WaitForFixedUpdate();
将协程挂起,在下一个物理帧FixedUpdate后继续执行后续代码。会受timeScale影响。
yield return new WaitForSeconds(float time);
将协程挂起,在物理循环经过time秒后继续执行后续代码。会受timeScale影响。
yield return new WaitForSecondsRealtime(float time);
将协程挂起,在逻辑循环经过time秒后继续执行后续代码。不会受timeScale影响。
yield return new WaitForEndOfFrame();
将协程挂起,在该逻辑帧的渲染结束后继续执行后续代码。
yield return new StartCoroutine(IEnumerator routine);
将该协程挂起,开启目标协程,待目标协程结束后,继续执行该协程的后续代码。
yield return new WaitUntil(()=>bool condition);
每个逻辑帧Update之后检测一次,当Lambda表达式返回ture时,继续执行后续代码。
yield return new WaitUntil(()=>bool condition);
每个逻辑帧Update之后检测一次,当Lambda表达式返回false时,继续执行后续代码。
yield break;
结束协程,等同于普通函数中的return。
注:生命周期中的一些事件可以改为IEnumerator来使用协程调用,如Start和Awake等。
中断协程
中断协程时,所有该组件上正中被挂起的协程会被移除。中断协程的方法包括:
public void MonoBehaviour.StopAllCoroutines();
public void MonoBehaviour.StopCoroutine(string methodName);
public void MonoBehaviour.StopCoroutine(Coroutine routine);
其中,使用string作为参数的函数,不能用于停止不是由string作为参数开启的协程。
我们可以在开启协程时将其保存下来以方便将来定向的将其停止:
public class StopCoroutineExample : MonoBehaviour
{
public IEnumerator Start()
{
Coroutine routine = StartCoroutine(LogMessage());
yield return new WaitForSeconds(1.5f);
StopCoroutine(routine);
Debug.Log("Coroutine end manually.");
}
public IEnumerator LogMessage()
{
Debug.Log("Coroutine start.");
yield return new WaitForSeconds(3);
Debug.Log("Coroutine end automatically.");
}
}
协程的优势
使用协程有效的避免了一些垃圾变量和多余函数,让代码更优雅。我们使用协程重新实现了上文中使用Invoke的两个效果。
使用协程实现灯光闪烁的效果:
public class LightBlink : MonoBehaviour
{
Light light;
private void Start()
{
Coroutine controller = StartCoroutine(LightBlinking);
}
private IEnumerator LightBlinking()
{
for(;;)
{
light.enabled = true;
yield return new WaitForSeconds(2);
light.enabled = false;
yield return new WaitForSeconds(2);
}
}
}
使用协程使物体逐渐缩小:
public class ShrinkEffect : MonoBehaviour
{
private void Start()
{
StartCoroutine(Shrink(5,0.1f));
}
private IEnumerator Shrink(float time; float rate)
{
for(float t = 0; t <= time; t += rate)
{
transform.localScale *= 0.95f;
yield return new WaitForSeconds(rate);
}
}
}