Update、Coroutines 和InvokeRepeating
另一个很容易养成的习惯是在 Update0回调中以超出需要的频率重复调用某段代码。例如,开始时情形如下:
void Update(){
ProcessAI();
}
本例在每一帧中调用某个自定义ProcessAI()子例程。这可能是一个复杂的任务,需要人工智能系统检查某个网格系统,以找出它要移动的目的地,或者为组宇宙飞船决定一些瞬时策略,或者完成人工智能系统需要完成的其他行为。如果这个活动占用了太多的帧率预算,且任务完成的频率低于没有明显缺陷的每一帧,那么提高性能的一个好方法就是直接减少ProcessAI()的调用频率:
private float _aiProcessDelay=0.2f;
private float _timer =0.0f;
void Update(){
_timer += Time.deltaTime;
if(_timer>_aiProcessDelay){
ProcessAI();
_timer -=_aiProcessDelay;
}
}
本例中,每秒仅调用 ProcessAI()5次,减少了Update()回调的总成本,这改进了之前的情形,但代价是可能要花一点时间了解代码,还需要一些额外的内存来存储浮点数据。最终 Unity 仍要调用一个空的回调函数。
这个函数是一个完美的示例,可以将它转换成协程,从而利用其延迟的调用属性。如前所述,协程通常用于编写短事件序列的脚本,可以是一次性的,也可以是重复的操作。它们不应该与线程混淆,线程以并发方式在完全不同的CPU内核上运行,而且多个线程可以同时运行。相反,协程以顺序的方式在主线程上运行,在任何给定时刻只处理一个协程,每个协程通过yield语句决定何时暂停和继续。下面的代码说明可以协程形式重写以上的Update()回调:
void start()(
StartCoroutine(ProcessAICoroutine());
}
IEnumerator ProcessAICoroutine(){
while(true){
ProcessAI();
yield return new WaitForSeconds( _aiProcessDelay);
}
}
上述代码演示了一个协程调用ProcessAI(),在yield 语句中暂停给定秒数(aiProcessDelay的值),然后主线程再次恢复该协程。此时,它将返回循环的开始,调用 ProcessAI(),再次在 yield 语句处暂停,并一直重复下去(通过 while(true)语句),直到要求停止为止。
这种方法的主要好处是,这个函数只调用_aiProcessDelay值指示的频率,在此之前它一直处于空闲状态,从而减少对大多数帧的性能影响。然而,这种方法有其缺点。首先,与标准函数调用相比,启动协程会带来额外的开销成本(大约是标准函数调用的3倍),还会分配一些内存,将当前状态存储在内存中,直到下一次调用它。这种额外的开销也不是一次性的成本,因为协程经常不断地调用yield,这会-次又一次地造成相同的开销成本,所以需要确保降低频率的好处大于此成本。
注意:在对1000个带有空Update()回调的对象的测试中,处理时间为1.1 毫秒,而 WaitForEndOfFrame 上生成的 1000个协程(与 Update()回调的频率相同)耗时 2.9 毫秒。所以,后者的相对成本几乎是前者的3倍.
其次一旦初始化,协程的运行独立于 MonoBehaviour 组件中 Update()回调的触发,不管组件是否禁用,都将继续调用协程。如果执行大量的GameObjec构建和析构操作,协程可能会显得很笨拙。
再次,协程会在包含它的GameObject变成不活动的那一刻自动停止,不管出于什么原因(无论它被设置为不活动的还是它的一个父对象被设置为不活动的)。如果 GameObject再次设置为活动的,协程不会自动重新启动。最后,将方法转换为协程,可减少大部分帧中的性能损失,但如果方法体的单次调用突破了帧率预算,则无论该方法的调用次数有多少,都将超出预算。因此,这种方法最适用于如下情况:由于在给定的帧中调用该方法的次数太多而导致帧率超出预算,而不是因为该方法本身开销太大。这些情况下,开发人员别无选择,只能深入研究并改进方法本身的性能,或者减少其他任务的成本,将时间让给该方法,来完成其工作。
在生成协程时,有几种可用的 yield 类型。WaitForSeconds 这种类型容易理解,协程在 yield 语句上暂停指定的秒数。但是,它并不是一个精确的计时器,所以当这个 yield 类型恢复执行时,可能会有少量的变化。
WaitForSecondsRealTime 是另一种类型,与WaitForSeconds 的唯一区别在于,它使用未缩放的时间。WaitForSeconds与缩放的时间进行比较,缩放的时间受到全局 Time.timeScale 属性的影响,而 WaitForSecondsRealTime 则与缩放时间无关。因此,如果要调整时间缩放值(例如,对于慢动作效果),请注意使用合适的 yield 类型。
还有 WaitForEndOfFrame 选项,它在下一个 Update()回调结束时继续WaitForFixedUpdate 选项在下一个 FixedUpdate()调用结束时继续。最后,Unity 5.3引入了 WaitUntil 和 WaitWhile,在这两个函数中,提供了一个委托函数,协程根据给定的委托返回true或false分别暂停或继续。请注意,为这些yield 类型提供的委托将对每个 Update()执行一次,直到返回停止它们所需的布尔值,因此非常类似于在 while 循环过程(在某个条件下结束)中使用 WaitForEndOfFrame 的协程当然,同样重要的是,所提供的委托函数执行起来开销不会太大。
提示:委托函数是 C#中非常有用的结构,允许将本地方法作为参数传递给其他方法,通常用于回调。有关委托的更多信息,请参阅MSDNC#编程指南(参考网站 2.3)。
某些 Update()回调的编写方式可以简化为简单的协程,这些协程总是在其中一种类型上调用 yield,但应该注意前面提到的缺点。协程很难调试,因为它们不遵循正常的执行流程:在调用栈上找不到一个调用者可以直接负责在给定的时间触发协程。如果协程执行复杂的任务,与其他子系统交互,就会导致一些很难察觉的缺陷,因为这些缺陷在其他代码不希望的时刻触发,这些缺陷也往往是极其难重现的类型。因此,如果希望使用协程,最好使它们尽可能简单,且独立于其他复杂的子系统。
事实上,如果上面示例中的协程很简单,可以归结为一个while循环,总是在 WaitForSeconds 或 WaitForSecondsRealtime上调用yield,则通常可以替换成 InvokeRepeating()调用,它的建立更简单,开销成本略小。下面的代码在功能上与前面使用协程定期调用ProcessAI()方法的实现方案相同:
void start(){
InvokeRepeating("ProcessAl",0f,aiProcessDelay);
}
InvokeRepeating()和协程之间的一个重要区别是,InvokeRepeating()完全独立于MonoBehaviour和 GameObject的状态。停止 InvokeRepeating()调用的两种方法:第一种方法是调用 CancelInvoke(),它停止由给定的 MonoBehaviour(注意,它们不能单独取消)发起的所有ImnvokeRepeating()回调;第二种方法是销毁关联的MonoBehaviour 或它的父 GameObject。禁用 MonoBehaviour 或 GameObject 都不会停止 InvokeRepeating()。
注意:处理包含1000个InvokeRepeatingO调用的测试大约需要2.6 毫秒,略快于 1000 个同等的协程 yield 调用,它需要 2.9 毫秒。
勇哥注:以上内容出自《 Unity游戏优化(第3版)》
另外,分享者还有其它更多的内容,见知乎的文章列表:
https://www.zhihu.com/people/nutshell-93-27
下面是unity中的InvokeRepeating的扫盲介绍:
在 Unity 中,InvokeRepeating 是一个用于在 MonoBehaviour 类中调用的方法,它允许你按照指定的时间间隔重复调用一个指定的方法。这个方法非常有用,当你需要在游戏中实现周期性行为时,比如角色的心跳动画、敌人的巡逻路径、自动射击等。
使用方法
InvokeRepeating 的基本语法如下:
void InvokeRepeating(string methodName, float delay, float repeatRate);
methodName:这是一个字符串参数,表示你想要重复调用的方法的名称。注意,这个名称必须与你 MonoBehaviour 类中定义的方法名称完全匹配(包括大小写)。
delay:这是一个浮点数参数,表示在调用 InvokeRepeating 之后,首次调用 methodName 方法之前的延迟时间(以秒为单位)。
repeatRate:这是一个浮点数参数,表示之后每次调用 methodName 方法之间的时间间隔(以秒为单位)。
示例
以下是一个简单的示例,展示了如何使用 InvokeRepeating:
using UnityEngine; public class Example : MonoBehaviour { void Start() { // 首次调用 PrintMessage 方法前延迟 2 秒,之后每 1 秒调用一次 InvokeRepeating("PrintMessage", 2.0f, 1.0f); } void PrintMessage() { Debug.Log("This message is printed repeatedly."); } // 如果你想要在某个时刻停止重复调用,可以使用 CancelInvoke 方法 void StopRepeating() { CancelInvoke("PrintMessage"); } }
注意事项
参数匹配:methodName 必须与类中定义的方法名称完全匹配,包括大小写。
方法签名:被调用的方法不能有任何参数,也不能有返回值(即必须是 void 类型)。
取消调用:如果你需要在某个时刻停止重复调用,可以使用 CancelInvoke 方法,并传递你想要取消的方法名称。

