Unity optimization - script optimization strategy 1

    Hello, everyone, this is Qiqi. Today I will introduce to you some optimization strategies in Unity scripts.

Table of contents

1. The fastest way to obtain components

2. Remove empty callback definitions

3. Cache component reference

4. Sharing calculation output

5. Update, Coroutines and InvokeRepeating


1. The fastest way to obtain components

There are several variations of the GetComponent() method with different performance costs, so be careful calling the highest version of the method.

The 3 available overloaded versions are GetComponent(string), GetComponent<T>() and GetComponent(typeof(T)). Since these methods undergo some optimizations every year, the most efficient version depends on the version of Unity used. In all subsequent versions of Unity5, it is better to use the GetComponent<T>() variant.

Finally found through testing that GetComponent<T>() is a little faster than GetComponent(typeof(T)), while GetComponent(string ) is significantly slower than the other two methods. Therefore, we should make sure to never use the GetComponent(string) method. Unless absolutely necessary.

2. Remove empty callback definitions

The main point of writing scripts in Unity is to write callback functions in classes that inherit from MonoBehaviour and Unity will call them when necessary. The four most commonly used callbacks are Awake(), Start(), Update() and FixedUpdate().

Awake() is called when the MonoBehaviour is first created. Start() is called shortly after Awake() but before the first Update(). During scene initialization, each MonoBehaviour component's Awake() callback is called before the Start() callback.

After that, Update() is called repeatedly every time the rendering pipeline renders a new image.

Finally, FixedUpdate() is called at fixed intervals before the physics engine is updated.

When a MonoBehaviour is first instantiated in the scene, Unity will add any defined callbacks to a list of function pointers and call this list at key moments. However, even if the function body is empty, Unity will add it to the list. The core Unity engine doesn't realize that these function bodies are empty, it only knows that the method is defined and it has to get the method and then call it when necessary. If you scatter these empty definitions throughout your code base, a small amount of CPU will be wasted.

The fix is ​​simple; remove the empty callback definition. Finding such empty definitions in a scalable code base can be difficult, but if you use some basic regular expressions (regex for short) you should be able to find empty callback definitions relatively easily.

The following regex expression should search out empty Update() definitions in your code:

void\s*Update\s*?\(\s*?\)\s*?\n*?{\n*?\s*?\}

This regex checks the standard method definition of the Update() callback while containing more than whitespace and newline characters that may be spread throughout the method definition.

Of course, the above approach can also find non-boilerplate Unity callbacks, such as OnGUI(), OnEndable(), OnDestroy(), and LateUpdate(). The only difference is that Start() and Update() are customized in the new sample.

In Unity scripts, the most common source of performance issues is when you do the following and misuse the Update() callback

  • Repeatedly calculating values ​​that rarely or never change
  • Too many components compute a result that can be shared
  • Performing work far more frequently than necessary

Here are some tips for directly solving these problems: 

3. Cache component reference

When writing scripts in Unity, repeatedly calculating a value is a common mistake, especially when using the GetComponent() method. If it is inside Update(), then this problem will be more serious. A better approach is to get references to the required data during initialization and save them until they are needed.

Caching component references in this way means you don't have to re-fetch them every time you need them, which saves some CPU overhead each time. The price is a small amount of additional memory consumption.

The same trick works for any chunk of data where calculations are decided upon at runtime. There is no need to ask the CCPU to recalculate the same value every time Update() is executed as it can be stored in memory for future reference.

4. Sharing calculation output

You can save performance overhead by having multiple objects share the results of certain calculations; of course, this only works if those calculations all produce the same result. This situation is usually easy to spot, but difficult to refactor, so exploiting it will be very implementation dependent.

Examples include finding objects in a scene, reading data from files, parsing data (such as XML or JSON), finding content in large lists or deep dictionaries of information, calculating paths for a set of AI objects, complex mathematical trajectories, rays Tracking etc.

Every time you perform an expensive operation, consider whether you call it from multiple places but always get the same output. If so, refactoring is wise. The biggest cost is usually just sacrificing a bit of code simplicity, although passing values ​​may incur some additional overhead.

Note that it is often easy to fall into the habit of hiding large, complex functions in a base class and then defining a derived class that uses that function, completely forgetting about the overhead of that function because we rarely look at the modified code again. It's best to use the Unity Profiler to point out how many times this expensive function might be called, and as usual, don't pre-optimize those functions unless it's proven to be a performance issue. No matter how expensive it is, as long as it doesn't exceed performance limits, it's not really a performance issue.

5. Update, Coroutines and InvokeRepeating

Another habit that's easy to get into is calling a certain piece of code in the Update() callback more often than necessary. For example, the starting situation is as follows:

void Update(){
   processAI();
}

processAI() may be a complex task, ProcessAI() is called in every frame. If this activity takes up too much of the framerate budget and the task is completed less frequently than every frame without obvious defects, then a good way to improve performance is to simply call ProcessAI() less frequently.

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

This improvement reduces the overall cost of Update() callbacks, requiring some additional memory to store floating point data. But in the end Unity still has to call an empty callback function.

This function is a perfect example of converting it into a coroutine, taking advantage of its delayed calling properties. Coroutines are typically used to script short sequences of events, either one-off or repeated operations. They should not be confused with threads, which run concurrently on completely different CPU cores, and multiple threads can run simultaneously. Instead, coroutines run on the main thread in a sequential manner, so that only one coroutine is processed at any given moment, and each coroutine decides when to pause and resume via a yield statement. The following code illustrates that the above Update() callback can be rewritten in the form of a coroutine.

 void Update()
    {
        StartCorountine(ProcessAICoroutine);
    }
    IEnumerator ProcessAICoroutine()
    {
        while (true)
        {
            ProcessAI();
            yeld return new WaitForSeconds(_aiProcessDelay);
        }
    }

The benefit of this approach is that this function is only called the number of times indicated by the _aiProcessDelay value, before which it remains idle, thus reducing the performance impact on most frames. However, this approach has its drawbacks.

First, launching a coroutine incurs additional overhead costs compared to a standard function call (approximately three times the cost of a standard function call), as well as allocating some memory to store the current state in memory until the next time it is called. This additional overhead is not a one-time cost, because the coroutine often calls yield continuously, which will cause the same overhead cost again and again, so you need to ensure that the benefits of reducing the frequency outweigh this cost.

Second, once initialized, the coroutine runs independently of the firing of the Uptdate() callback in the MonoBehaviour component, and the coroutine will continue to be called regardless of whether the component is disabled. If you perform a lot of GameObject construction and destruction operations, it may be clumsy to write.

Again, the coroutine will automatically stop the moment its containing GameObject becomes inactive, for whatever reason (whether it is set to inactive or one of its parents is set to inactive). If the GameObject is made active again, the coroutine will not be automatically restarted.

Finally, converting the method into a coroutine can reduce the performance loss in most frames, but if a single call of the method body exceeds the frame rate budget, no matter how few times the method is called, it will over the budget. Therefore, this method is best suited for situations where the frame rate exceeds budget due to calling the method too many times in a given frame. , not because the method itself is too expensive. In this case, we have no choice but to delve deeper and improve the performance of the method itself, or reduce the cost of other tasks and give time to the method to complete its work.

There are several yield types available when generating coroutines. WaitForSeconds is easy to understand; the coroutine pauses for the specified number of seconds on a yield statement. However, it is not an accurate timer, so there may be a small change when the yield type resumes execution.

WaitForSecondsRealTime is another option, the only difference with WaitForSeconds is that it uses unscaled time. WaitForSeconds is compared to scaled time, which is affected by the global Time.timeScale property. WaitForSecondsRealTime is not, so if you want to adjust the time scaling value, be careful about which yield type you use.

There is also the option WaitForEndOfFrame, which continues at the end of the next Update(), and WaitForFixedUpdate, which continues at the end of the next FixedUpdate(). Finally, Unity5.3 introduced WaitUntil and WaitWhile. In these two functions, a delegate function is provided, and the coroutine returns true or false to pause or continue respectively according to the given delegate. Note that the delegates provided for these yield types will be executed once for each Update() until they return the boolean value required to stop them, so they are very similar to coroutines using WaitForEndOfFrame during a while loop. Of course, it is also important that the provided delegate functions are not too expensive to execute.

Delegate functions are a very useful construct in C# that allow local methods to be passed as parameters to other methods, often used for callbacks

 Some Update() callbacks can be written in a way that can be reduced to simple coroutines that always call yield on one of the types, but the disadvantages provided earlier should be noted. Coroutines are difficult to debug, so they do not follow the normal flow of execution; there is no caller on the call stack. You can directly blame why a coroutine fires at a given time. If the coroutine performs complex tasks and interacts with other subsystems, it will lead to some defects that are difficult to detect because they fire at times that other code does not expect. These defects It is also often a type that is extremely difficult to reproduce. If you want to use coroutines, it's best to keep them as simple as possible and independent of other complex subsystems.

In fact, if the coroutine in the above example is simple and can be boiled down to a while loop that always calls yield on WaitForSeconds or WaitForSecondsRealTime, it can usually be replaced with an InvokeRepeating() call, which is simpler to set up and has slightly less overhead. Small. The following code is functionally the same as the previous implementation of using a coroutine to call the ProcessAI() method periodically:

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

An important difference between InvokeRepeating() and coroutines is that InvokeRepeating() is completely independent of the state of the MonoBehaviour and GameObject. Two ways to stop InvokeRepeating() calls:

  • Calls CancelInvode() which stops all InvokeRepeating() callbacks initiated by the given MonoBehaviour
  • Destroy the associated MonoBehaviour or its parent GameObject. Disabling neither MonoBehaviour nor GameObject stops InvokeRepeating()

Guess you like

Origin blog.csdn.net/m0_63024355/article/details/134574054