官术网_书友最值得收藏!

Update, Coroutines, and InvokeRepeating

Another habit that's easy to fall into is to call something repeatedly in an Update() callback way more often than is needed. For example, we may start with a situation like this:

void Update() {
ProcessAI();
}

In this case, we're calling some custom ProcessAI() subroutine every single frame. This may be a complex task, requiring the AI system to check some grid system to figure out where it's meant to move or determine some fleet maneuvers for a group of spaceships or whatever our game needs for its AI.

If this activity is eating into our frame rate budget too much, and the task can be completed less frequently than every frame with no significant drawbacks, then a good trick to improve performance is to simply reduce the frequency that ProcessAI() gets called:

private float _aiProcessDelay = 0.2f;
private float _timer = 0.0f;

void Update() {
_timer += Time.deltaTime;
if (_timer > _aiProcessDelay) {
ProcessAI();
_timer -= _aiProcessDelay;
}
}

In this case, we've reduced the Update() callback's overall cost by only invoking ProcessAI() about five times every second, which is an improvement over the previous situation, at the expense of code that can take a bit of time to understand at first glance, and a little extra memory to store some floating-point data. Although, at the end of the day we're still having Unity call an empty callback function more often than not.

This function is a perfect example of a function we could convert into a Coroutine to make use of their delayed invocation properties. As mentioned previously, Coroutines are typically used to script a short sequence of events, either as a one-time, or repeated, action. They should not be confused with threads, which would run on a completely different CPU core in a concurrent manner, and multiple threads can be running simultaneously. Instead, Coroutines run on the main thread in a sequential manner such that only one Coroutine is handled at any given moment, and each Coroutine decides when to pause and resume via yield statements. The following code is an example of how we might rewrite the above Update() callback in the form of a Coroutine:

void Start() {
StartCoroutine(ProcessAICoroutine ());
}

IEnumerator ProcessAICoroutine () {
while (true) {
ProcessAI();
yield return new WaitForSeconds(_aiProcessDelay);
}
}

The preceding code demonstrates a Coroutine that calls ProcessAI(), then pause at the yield statement for the given number of seconds (the value of  _aiProcessDelay) before the main thread resumes the Coroutine again. At which point, it will return to the start of the loop, call ProcessAI(), pause on the yield statement again, and repeat forever (via the while(true) statement) until asked to stop.

The main benefit of this approach is that this function will only be called as often as dictated by the value of _aiProcessDelay, and it will sit idle until that time, reducing the performance hit inflicted in most of our frames. However, this approach has its drawbacks.

For one, starting a Coroutine comes with an additional overhead cost relative to a standard function call (around three times as slow), as well as some memory allocations to store the current state in memory until it is invoked the next time. This additional overhead is also not a one-time cost because Coroutines often constantly call yield, which inflicts the same overhead cost again and again, so we need to ensure that the benefits of a reduced frequency outweighs this cost.

In a test of 1,000 objects with empty Update() callbacks, it took 1.1 milliseconds to process, whereas 1,000 Coroutines yielding on WaitForEndOfFrame (which has identical frequency to Update() callbacks) took 2.9 milliseconds. So, the relative cost is almost three times as much.

Secondly, once initialized, Coroutines run independent of the triggering MonoBehaviour Component’s Update() callback and will continue to be invoked regardless of whether the Component is disabled or not, which can make them unwieldy if we’re performing a lot of GameObject construction and destruction.

Thirdly, the Coroutine will automatically stop the moment the containing GameObject is made inactive for whatever reason (whether it was set inactive or one of its parents was) and will not automatically restart if the GameObject is set active again.

Finally, by converting a method into a Coroutine, we may have reduced the performance hit inflicted during most of our frames, but if a single invocation of the method body causes us to break our frame rate budget, then it will still be exceeded no matter how rarely we call the method. Therefore, this approach is best used for situations where we are only breaking our frame rate budget because of the sheer number of times the method is called in a given frame, not because the method is too expensive on its own. In those cases, we have no option but to either dig into and improve the performance of the method itself or reduce the cost of other tasks to free up the time it needs to complete its work.

There are several yield types available to us when generating Coroutines. WaitForSeconds is fairly self-explanatory; the Coroutine will pause at the yield statement for a given number of seconds. It is not exact an exact timer, however, so expect a small amount of variation when this yield type actually resumes.

WaitForSecondsRealTime is another option and is different to WaitForSeconds only in that it uses unscaled time. WaitForSeconds compares against scaled time, which is affected by the global Time.timeScale property while WaitForSecondsRealTime is not, so be careful about which yield type you use if you’re tweaking the time scale value (for example, for slow motion effects).

There is also WaitForEndOfFrame, which would continue at the end of the next Update(), and then there’s WaitForFixedUpdate, which would continue at the end of the next FixedUpdate(). Lastly, Unity 5.3 introduced WaitUntil and WaitWhile, where we provide a delegate function, and the Coroutine will pause until the given delegate returns true or false, respectively. Note that the delegates provided to these yield types will be executed for each Update() until they return the Boolean value needed to stop them, which makes them very similar to a Coroutine using WaitForEndOfFrame in a while-loop that ends on a certain condition. Of course, it is also important that the delegate function we provide is not too expensive to execute.

Delegate functions are incredibly useful constructs in C# that allows us to pass local methods around as arguments to other methods and are typically used for callbacks. Check out the MSDN C# Programming Guide for more information on delegates at https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/.

The way that some Update() callbacks are written could probably be condensed down into simple Coroutines that always call yield on one of these types, but we should be aware of the drawbacks mentioned previously. Coroutines can be tricky to debug since they don’t follow normal execution flow; there's no caller in the callstack we can directly blame as to why a Coroutine triggered at a given time, and if Coroutines perform complex tasks and interact with other subsystems, then they can result in some impossibly difficult bugs because they happened to be triggered at a moment that some other code didn't expect, which also tend to be the kinds of bugs that are painstakingly difficult to reproduce. If you do wish to make use of Coroutines, the best advice is to keep them simple and independent of other complex subsystems.

Indeed, if our Coroutine is simple enough that it can be boiled down to a while-loop that always calls yield on WaitForSeconds, or WaitForSecondsRealtime as in the above example, then we can usually replace it with an InvokeRepeating() call, which is even simpler to set up and has a slightly smaller overhead cost. The following code is functionally equivalent to the previous implementation that used a Coroutine to regularly invoke a ProcessAI() method:

void Start() {
InvokeRepeating("ProcessAI", 0f, _aiProcessDelay);
}

An important difference between InvokeRepeating() and Coroutines is that InvokeRepeating() is completely independent of both the MonoBehaviour and GameObject's state. The only two ways to stop an InvokeRepeating() call is to either call CancelInvoke() which stops all InvokeRepeating() callbacks initiated by the given MonoBehaviour (note that they cannot be canceled inpidually) or to destroy the associated MonoBehaviour or its parent GameObject. Disabling either the MonoBehaviour or GameObject does not stop InvokeRepeating().

A test of 1,000 InvokeRepeating() calls was processed in about 2.6 milliseconds; slightly faster than 1,000 equivalent Coroutine yield calls, which took 2.9 milliseconds.

That covers most of the useful information related to the Update() callback. Let’s look into some other useful scripting tips.

主站蜘蛛池模板: 阜南县| 罗源县| 鲁甸县| 淮北市| 博罗县| 彭山县| 永平县| 海晏县| 西安市| 金昌市| 巩留县| 和田县| 和田县| 昌宁县| 新津县| 香格里拉县| 海口市| 大姚县| 成武县| 福州市| 白沙| 白玉县| 晋中市| 买车| 柳江县| 佛学| 蒙山县| 黎城县| 皮山县| 根河市| 渝北区| 万源市| 突泉县| 宜春市| 崇礼县| 十堰市| 石河子市| 榆社县| 安吉县| 易门县| 荃湾区|