[Unity脚本优化]Optimizing scripts in Unity games——2019以下

If you are using Unity 2019.3 or above, click here.

Introduction

When our game runs, the central processing unit (CPU) of our device carries out instructions. Every single frame of our game requires many millions of these CPU instructions to be carried out. To maintain a smooth frame rate, the CPU must carry out of its instructions within a set amount of time. When the CPU cannot carry out all of its instructions in time, our game may slow down, stutter or freeze.
当我们的游戏运行时,我们设备的中央处理器 (CPU) 会执行指令。 我们游戏的每一帧都需要执行数百万条这样的 CPU 指令。 为了保持平滑的帧速率,CPU 必须在设定的时间内执行其指令。 当 CPU 无法及时执行所有指令时,我们的游戏可能会变慢、卡顿或冻结。
Many things can cause the CPU to have too much work to do. Examples could include demanding rendering code, overly complex physics simulations or too many animation callbacks. This article focuses on only one of these reasons: CPU performance problems caused by the code that we write in our scripts.
许多事情会导致 CPU 有太多的工作要做。 例如可能包括要求苛刻的渲染代码、过于复杂的物理模拟或过多的动画回调。 本文只关注其中一个原因: 我们在脚本中编写的代码导致的 CPU 性能问题。
In this article we will learn how our scripts are turned into CPU instructions, what can cause our scripts to generate an excessive amount of work for the CPU, and how to fix performance problems that are caused by the code in our scripts.
在本文中,我们将了解如何将我们的脚本转换为 CPU 指令,什么会导致我们的脚本为 CPU 生成过多的工作,以及如何解决由脚本中的代码引起的性能问题。

Diagnosing problems with our code

Performance problems caused by excessive demands on the CPU can manifest as low frame rates, jerky performance or intermittent freezes. However, other problems can cause similar symptoms. If our game has performance problems like this, the first thing we must do is to use Unity’s Profiler window to establish whether our performance problems are due to the CPU being unable to complete its tasks in time. Once we have established this, we must determine whether user scripts are the cause of the problem, or whether the problem is caused by some other part of our game: complex physics or animations, for example.
对 CPU 的过度需求导致的性能问题可能表现为帧速率低、性能不稳定或间歇性冻结。 但是,其他问题可能会导致类似的症状。 如果我们的游戏出现这样的性能问题,我们首先要做的是使用 Unity 的 Profiler 窗口来确定我们的性能问题是否是由于 CPU 无法及时完成其任务造成的。 一旦我们确定了这一点,我们必须确定用户脚本是否是问题的原因,或者问题是否是由我们游戏的其他部分引起的:例如,复杂的物理或动画。
To learn how to use Unity’s Profiler window to find the cause of performance problems, please follow the Diagnosing Performance Problems tutorial.
要了解如何使用 Unity 的 Profiler 窗口查找性能问题的原因,请遵循诊断性能问题教程。

A brief introduction to how Unity builds and runs our game

To understand why our code may not be performing well, we first need to understand what happens when Unity builds our game. Knowing what’s going on behind the scenes will help us to make informed decisions about how we can improve our game’s performance.
要了解为什么我们的代码可能表现不佳,我们首先需要了解 Unity 构建我们的游戏时会发生什么。 了解幕后发生的事情将有助于我们就如何提高游戏性能做出明智的决定。

The build process

When we build our game, Unity packages everything needed to run our game into a program that can be executed by our target device. CPUs can only run code written in very simple languages known as machine code or native code; they cannot run code written in more complex languages like C#. This means that Unity must translate our code into other languages. This translation process is called compiling.
当我们构建游戏时,Unity 会将运行游戏所需的所有内容打包到可以由目标设备执行的程序中。 CPU 只能运行以非常简单的语言编写的代码,称为 机器代码 或本机代码;他们无法运行用 C# 等更复杂的语言编写的代码。这意味着 Unity 必须将我们的代码翻译成其他语言。这个翻译过程称为编译。
Unity first compiles our scripts into a language called Common Intermediate Language (CIL). CIL is a language that is easy to compile into a wide range of different native code languages. The CIL is then compiled to native code for our specific target device. This second step happens either when we build our game (known as ahead of time compilation or AOT compilation), or on the target device itself, just before the code is run (known as just in time compilation or JIT compilation). Whether our game uses AOT or JIT compilation usually depends on the target hardware.
Unity 首先将我们的脚本编译成一种称为 通用中间语言 (CIL) 的语言。 CIL 是一种易于编译成各种不同本机代码语言的语言。然后将 CIL 编译为我们特定目标设备的本机代码。第二步发生在我们构建游戏时(称为提前编译或AOT编译),或者在目标设备本身上,就在代码运行之前(称为 即时编译 或 JIT 编译)。我们的游戏是使用 AOT 还是 JIT 编译通常取决于目标硬件。

The relationship between the code we write and compiled code

Code that has not been compiled yet is known as source code. The source code that we write determines the structure and content of the compiled code.
尚未编译的代码称为源代码。我们编写的源代码决定了编译代码的结构和内容。
For the most part, source code that is well structured and efficient will result in compiled code that is well structured and efficient. However, it’s useful for us to know a little about native code so that we can better understand why some source code is compiled into more efficient native code.
在大多数情况下,结构良好且高效的源代码将产生结构良好且高效的编译代码。但是,对我们了解一点原生代码是很有用的,这样我们就可以更好地理解为什么有些源代码会被编译成更高效的原生代码。
Firstly, some CPU instructions take more time to execute than others. An example of this is calculating a square root. This calculation takes a CPU more time to execute than, for example, multiplying two numbers. The difference between a single fast CPU instruction and a single slow CPU instruction is very small indeed, but it’s useful for us to understand that, fundamentally, some instructions are simply faster than others.
首先,一些 CPU 指令比其他指令需要更多的时间来执行。这方面的一个例子是计算平方根。此计算需要 CPU 更多的时间来执行,例如,将两个数字相乘。单条快速 CPU 指令和单条慢速 CPU 指令之间的差异确实非常小,但有助于我们理解,从根本上说,有些指令比其他指令快。
The next thing we need to understand is that some operations that seem very simple in source code can be surprisingly complex when they are compiled to code. An example of this is inserting an element into a list. Many more instructions are needed to perform this operation than, for example, accessing an element from an array by index. Again, when we consider an individual example we are talking about a tiny amount of time, but it is important to understand that some operations result in more instructions than others.
接下来我们需要了解的是,一些在源代码中看起来非常简单的操作,在编译成代码后可能会变得异常复杂。这方面的一个例子是将元素插入到列表中。执行此操作需要更多的指令,例如,通过索引访问数组中的元素。同样,当我们考虑一个单独的示例时,我们谈论的是很少的时间,但重要的是要了解某些操作会导致比其他操作更多的指令。
Understanding these ideas will help us to understand why some code performs better than other code, even when both examples do quite similar things. Even a limited background understanding of how things work at a low level can help us to write games that perform well.
理解这些想法将帮助我们理解为什么某些代码比其他代码执行得更好,即使两个示例都做了非常相似的事情。即使是对事物在低级别如何运作的有限背景了解也可以帮助我们编写性能良好的游戏。

Run time communication between Unity Engine code and our script code :Unity Engine 代码和我们的脚本代码之间的运行时通信

It’s useful for us to understand that our scripts written in C# run in a slightly different way to the code that makes up much of the Unity Engine. Most of the core functionality of the Unity Engine is written in C++ and has already been compiled to native code. This compiled engine code is part of what we install when we install Unity.
了解我们用 C# 编写的脚本的运行方式与构成 Unity 引擎的大部分代码的运行方式略有不同,这对我们很有用。 Unity 引擎的大部分核心功能都是用 C 语言编写的,并且已经编译为本机代码。 这个编译的引擎代码是我们安装 Unity 时安装的一部分。

Code compiled to CIL, such as our source code, is known as managed code. When managed code is compiled to native code, it is integrated with something called the managed runtime. The managed runtime takes care of things like automatic memory management and safety checks to ensure that a bug in our code will result in an exception rather than the device crashing.
编译为 CIL 的代码,例如我们的源代码,称为托管代码。 当托管代码编译为本机代码时,它会与称为托管运行时的东西集成。 托管运行时负责自动内存管理和安全检查等事情,以确保我们代码中的错误会导致异常而不是设备崩溃。

When the CPU transitions between running engine code and managed code, work must be done to set up these safety checks. When passing data from managed code back to the engine code, the CPU may need to do work to convert the data from the format used by the managed runtime to the format needed by the engine code. This conversion is known as marshalling. Again, the overhead from any single call between managed and engine code is not particularly expensive, but it is important that we understand that this cost exists.
当 CPU 在运行引擎代码和托管代码之间转换时,必须进行工作以设置这些安全检查。 在将数据从托管代码传递回引擎代码时,CPU 可能需要将数据从托管运行时使用的格式转换为引擎代码所需的格式。 这种转换称为编组。 同样,托管代码和引擎代码之间的任何单个调用的开销并不是特别昂贵,但重要的是我们要了解这种成本是存在的。

The causes of poorly-performing code

Now that we understand what happens to our code when Unity builds and runs our game we can understand that when our code performs poorly, it is because it creates too much work for the CPU at run time. Let’s consider the different reasons for this.
现在我们了解了当 Unity 构建和运行我们的游戏时我们的代码会发生什么,我们可以理解,当我们的代码表现不佳时,这是因为它在运行时为 CPU 创造了太多的工作。 让我们考虑一下不同的原因。

The first possibility is that our code is simply wasteful or poorly structured. An example of this might be code that makes the same function call repeatedly when it could make the call only once. This article will cover several common examples of poor structure and show example solutions.
第一种可能性是我们的代码只是浪费或结构不良。 这方面的一个例子可能是重复调用同一函数的代码,而它只能调用一次。 本文将介绍几个常见的不良结构示例并展示示例解决方案。

The second possibility is that our code appears to be well structured, but makes unnecessarily expensive calls to other code. An example of this might be code that results in unnecessary calls between managed and engine code. This article will give examples of Unity API calls that may be unexpectedly costly, with suggested alternatives that are more efficient.
第二种可能性是我们的代码看起来结构良好,但对其他代码进行了不必要的昂贵调用。 这方面的一个例子可能是导致托管代码和引擎代码之间不必要的调用的代码。 本文将提供成本可能出乎意料的 Unity API 调用示例,并提供更高效的建议替代方案。

The next possibility is that our code is efficient but it is being called when it does not need to be. An example of this might be code that simulates an enemy’s line of sight. The code itself may perform well, but it is wasteful to run this code when the player is very far from the enemy. This article contains examples of techniques that can help us to write code that runs only when it needs to.
下一个可能性是我们的代码是高效的,但它在不需要时被调用。 这方面的一个例子可能是模拟敌人视线的代码。 代码本身可能执行得很好,但是当玩家离敌人很远的时候运行这段代码是很浪费的。 本文包含的技术示例可以帮助我们编写仅在需要时运行的代码。

The final possibility is that our code is simply too demanding. An example of this might be a very detailed simulation where a large number of agents are using complex AI. If we have exhausted other possibilities and optimized this code as much as we can, then we may simply need to redesign our game to make it less demanding: for example, faking elements of our simulation rather than calculating them. Implementing this kind of optimization is beyond the scope of this article as it is extremely dependant on the game itself, but it will still benefit us to read the article and consider how to make our game as performant as possible.
最后一种可能是我们的代码要求太高了。 这方面的一个例子可能是一个非常详细的模拟,其中大量代理正在使用复杂的人工智能。 如果我们已经用尽了其他可能性并尽可能优化了这段代码,那么我们可能只需要重新设计我们的游戏以降低要求:例如,伪造我们的模拟元素而不是计算它们。 实现这种优化超出了本文的范围,因为它非常依赖于游戏本身,但阅读本文并考虑如何使我们的游戏尽可能高性能仍然对我们有益。

Improving the performance of our code

Once we have established that performance problems in our game are due to our code, we must think carefully about how to resolve these problems. Optimizing a demanding function may seem like a good place to start, but it may be that the function in question is already as optimal as it can be and is simply expensive by nature. Instead of changing that function, there may be a small efficiency saving we can make in a script that is used by hundreds of GameObjects that gives us a much more useful performance increase. Furthermore, improving the CPU performance of our code may come at a cost: changes may increase memory usage or offload work to the GPU.
一旦我们确定游戏中的性能问题是由我们的代码引起的,我们就必须仔细考虑如何解决这些问题。优化一个要求很高的功能似乎是一个很好的起点,但有问题的功能可能已经尽可能优化,而且本质上是昂贵的。除了更改该函数之外,我们可以在数百个游戏对象使用的脚本中节省一点效率,从而为我们提供更有用的性能提升。此外,提高我们代码的 CPU 性能可能需要付出代价:更改可能会增加内存使用量或将工作卸载到 GPU。

For these reasons, this article isn’t a set of simple steps to follow. This article is instead a series of suggestions for improving our code’s performance, with examples of situations where these suggestions can be applied. As with all performance optimization, there are no hard and fast rules. The most important thing to do is to profile our game, understand the nature of the problem, experiment with different solutions and measure the results of our changes.
由于这些原因,本文不是一组简单的步骤。 相反,本文是一系列改进代码性能的建议,并提供了可以应用这些建议的情况的示例。 与所有性能优化一样,没有硬性规定。 要做的最重要的事情是分析我们的游戏,了解问题的本质,尝试不同的解决方案并衡量我们更改的结果。

Writing efficient code

Writing efficient code and structuring it wisely can lead to improvements in our game’s performance. While the examples shown are in the context of a Unity game, these general best practice suggestions are not specific to Unity projects or Unity API calls.
编写高效的代码并明智地构建它可以提高我们游戏的性能。 虽然显示的示例是在 Unity 游戏的上下文中,但这些一般最佳实践建议并不特定于 Unity 项目或 Unity API 调用。

Move code out of loops when possible

Loops are a common place for inefficiencies to occur, especially when they are nested. Inefficiencies can really add up if they are in a loop that runs very frequently, especially if this code is found on many GameObjects in our game.
循环是低效率发生的常见地方,尤其是在它们嵌套时。 如果它们在一个非常频繁地运行的循环中,效率低下确实会增加,尤其是如果在我们游戏中的许多 GameObjects 上都可以找到此代码。

In the following simple example, our code iterates through the loop every time Update() is called, regardless of whether the condition is met.
在下面的简单示例中,我们的代码在每次调用 Update() 时都会遍历循环,而不管条件是否满足。

void Update() {
    
     for (int i = 0; i < myArray.Length; i++) {
    
     if (exampleBool) {
    
     ExampleFunction(myArray[i]); } } }

With a simple change, the code iterates through the loop only if the condition is met.

void Update() {
    
     if (exampleBool) {
    
     for (int i = 0; i < myArray.Length; i++) {
    
     ExampleFunction(myArray[i]); } } }

This is a simplified example but it illustrates a real saving that we can make. We should examine our code for places where we have structured our loops poorly.
这是一个简化的示例,但它说明了我们可以实现的真正节省。 我们应该检查我们的代码,找出我们的循环结构不佳的地方。

Consider whether code must run every frame

Update() is a function that is run once per frame by Unity. Update() is a convenient place to put code that needs to be called frequently, or code that must respond to frequent changes. However, not all of this code needs to run every single frame. Moving code out of Update() so that it runs only when it needs to can be a good way to improve performance.
Update() 是 Unity 每帧运行一次的函数。 Update() 是放置需要频繁调用的代码或必须响应频繁更改的代码的方便位置。 但是,并非所有这些代码都需要运行每一帧。 将代码移出 Update() 使其仅在需要时运行是提高性能的好方法。

Only run code when things change

Let’s look at a very simple example of optimizing code so that it only runs when things change. In the following code, DisplayScore() is called in Update(). However, the value of score may not change with every frame. This means that we are needlessly calling DisplayScore().
让我们看一个非常简单的优化代码的例子,让它只在事情发生变化时运行。 在下面的代码中,DisplayScore() 在 Update() 中被调用。 但是,分数的值可能不会随着每一帧而改变。 这意味着我们不必要地调用 DisplayScore()。

private int score; 
public void IncrementScore(int incrementBy) 
{
    
     
	score += incrementBy; 
} 
void Update() 
{
    
     
	DisplayScore(score); 
}

With a simple change, we now ensure that DisplayScore() is called only when the value of score has changed.

private int score; 
public void IncrementScore(int incrementBy) 
{
    
     
	score += incrementBy; 
	DisplayScore(score); 
}

Again, the above example is deliberately simplified but the principle is clear. If we apply this approach throughout our code we may be able to save CPU resources.
同样,上面的例子是故意简化的,但原理是清楚的。 如果我们在整个代码中应用这种方法,我们可能能够节省 CPU 资源。

Run code every [x] frames

If code needs to run frequently and cannot be triggered by an event, that doesn’t mean it needs to run every frame. In these cases, we can choose to run code every [x] frames.
如果代码需要频繁运行并且不能被事件触发,这并不意味着它需要运行每一帧。 在这些情况下,我们可以选择每 [x] 帧运行一次代码。
In this example code an expensive function runs once per frame.
在这个示例代码中,一个昂贵的函数每帧运行一次。

void Update() 
{
    
     
	ExampleExpensiveFunction(); 
}

In fact, it would be sufficient for our needs to run this code once every 3 frames. In the following code, we use the modulus operator to ensure that the expensive function runs only on every third frame.
事实上,我们需要每 3 帧运行一次这段代码就足够了。 在下面的代码中,我们使用取模运算符来确保昂贵的函数只在每三帧运行一次。

private int interval = 3; 
void Update() 
{
    
     
	if (Time.frameCount % interval == 0) 
	{
    
     
		ExampleExpensiveFunction(); 
	} 
}

An additional benefit of this technique is that it’s very easy to spread costly code out across separate frames, avoiding spikes. In the following example, each of the functions is called once every 3 frames and never on the same frame.
这种技术的另一个好处是可以很容易地将昂贵的代码分散到不同的帧中,避免尖峰。 在下面的示例中,每个函数每 3 帧调用一次,并且不会在同一帧上调用。

private int interval = 3; 
void Update() 
{
    
     
	if (Time.frameCount % interval == 0) 
	{
    
     ExampleExpensiveFunction(); } 
	else if (Time.frameCount % interval == 1) 
	{
    
     AnotherExampleExpensiveFunction(); } 
}

Use caching

If our code repeatedly calls expensive functions that return a result and then discards those results, this may be an opportunity for optimization. Storing and reusing references to these results can be more efficient. This technique is known as caching.
如果我们的代码重复调用返回结果的昂贵函数然后丢弃这些结果,这可能是优化的机会。 存储和重复使用对这些结果的引用会更有效。 这种技术称为缓存。

In Unity, it is common is to call GetComponent() to access components. In the following example, we call GetComponent() in Update() to access a Renderer component before passing it to another function. This code works, but it is inefficient due to the repeated GetComponent() call.
在 Unity 中,常见的是调用 GetComponent() 来访问组件。 在下面的示例中,我们在 Update() 中调用 GetComponent() 来访问渲染器组件,然后再将其传递给另一个函数。 此代码有效,但由于重复调用 GetComponent(),效率低下。

void Update() 
{
    
     
	Renderer myRenderer = GetComponent<Renderer>(); 
	ExampleFunction(myRenderer); 
}

The following code calls GetComponent() only once, as the result of the function is cached. The cached result can be reused in Update() without any further calls to GetComponent().
下面的代码只调用一次 GetComponent(),因为函数的结果被缓存了。 缓存的结果可以在 Update() 中重用,而无需进一步调用 GetComponent()。

private Renderer myRenderer; 
void Start() 
{
    
     
	myRenderer = GetComponent<Renderer>(); 
} 
void Update() 
{
    
     
	ExampleFunction(myRenderer); 
}

We should examine our code for cases where we make frequent calls to functions that return a result. It is possible that we could reduce the cost of these calls by using caching.
我们应该检查我们的代码是否经常调用返回结果的函数。 我们有可能通过使用缓存来降低这些调用的成本。

Use the right data structure

How we structure our data can have a big impact on how our code performs. There is no single data structure that is ideal for all situations, so to get the best performance in our game we need to use the right data structure for each job.
我们如何构建数据会对代码的执行方式产生重大影响。没有适合所有情况的单一数据结构,因此为了在我们的游戏中获得最佳性能,我们需要为每个作业使用正确的数据结构。
To make the right decision about which data structure to use, we need to understand the strengths and weaknesses of different data structures and think carefully about what we want our code to do. We may have thousands of elements that we need to iterate over once per frame, or we may have a small number of elements that we need to frequently add to and remove from. These different problems will be best solved by different data structures.
为了正确决定使用哪种数据结构,我们需要了解不同数据结构的优缺点,并仔细考虑我们希望我们的代码做什么。我们可能有数千个元素需要每帧迭代一次,或者我们可能有少量元素需要经常添加和删除。这些不同的问题最好通过不同的数据结构来解决。
Making the right decisions here depends on our knowledge of the subject. The best place to start, if this is a new area of knowledge, is to learn about Big O Notation. Big O Notation is how algorithmic complexity is discussed, and understanding this will help us to compare different data structures. This article is a clear and beginner-friendly guide to the subject. We can then learn more about the data structures available to us, and compare them to find the right data solutions for different problems. This MSDN guide to collections and data structures in C#) gives general guidance on choosing appropriate data structures and provides links to more in-depth documentation.
在这里做出正确的决定取决于我们对该主题的了解。如果这是一个新的知识领域,最好的起点是学习大 O 表示法。 Big O Notation 是如何讨论算法复杂性的,理解这一点将有助于我们比较不同的数据结构。本文是该主题的清晰且适合初学者的指南。然后,我们可以更多地了解我们可用的数据结构,并比较它们以找到针对不同问题的正确数据解决方案。此 MSDN 指南(关于 C# 中的集合和数据结构)提供了有关选择适当数据结构的一般指导,并提供了指向更深入文档的链接。
A single choice about data structures is unlikely to have a large impact on our game. However, in a data-driven game that involves a great many of such collections the results of these choices can really add up. An understanding of algorithmic complexity and the strengths and weaknesses of different data structures will help us to create code that performs well.
关于数据结构的单一选择不太可能对我们的游戏产生重大影响。但是,在涉及大量此类集合的数据驱动游戏中,这些选择的结果确实可以加起来。了解算法复杂性以及不同数据结构的优缺点将有助于我们创建性能良好的代码。

Minimize the impact of garbage collection

Garbage collection is an operation that occurs as part of how Unity manages memory. The way that our code uses memory determines the frequency and CPU cost of garbage collection, so it’s important that we understand how garbage collection works.
垃圾收集是作为 Unity 管理内存的一部分而发生的操作。 我们的代码使用内存的方式决定了垃圾收集的频率和 CPU 成本,因此了解垃圾收集的工作原理很重要。
In the next step, we’ll cover the topic of garbage collection in depth, and provide several different strategies for minimizing its impact.
在下一步中,我们将深入讨论垃圾收集的主题,并提供几种不同的策略来最小化其影响。

Use object pooling

It’s usually more costly to instantiate and destroy an object than it is to deactivate and reactivate it. This is especially true if the object contains start up code, such as calls to GetComponent() in an Awake() or Start() function. If we need to spawn and dispose of many copies of the same object, such as bullets in a shooting game, then we may benefit from object pooling.
实例化和销毁对象通常比停用和重新激活它更昂贵。 如果对象包含启动代码,例如在 Awake() 或 Start() 函数中调用 GetComponent(),则尤其如此。 如果我们需要生成和处理同一对象的多个副本,例如射击游戏中的子弹,那么我们可能会从对象池中受益。
Object pooling is a technique where, instead of creating and destroying instances of an object, objects are temporarily deactivated and then recycled and reactivated as needed. Although well known as a technique for managing memory usage, object pooling can also be useful as a technique for reducing excessive CPU usage.
对象池是一种技术,它不是创建和销毁对象的实例,而是暂时停用对象,然后根据需要回收和重新激活对象。 尽管作为一种管理内存使用的技术而广为人知,但对象池也可用作减少过多 CPU 使用的技术。
A full guide to object pooling is beyond the scope of this article, but it’s a really useful technique and one worth learning. This tutorial on object pooling on the Unity Learn site is a great guide to implementing an object pooling system in Unity.
对象池的完整指南超出了本文的范围,但它是一种非常有用的技术,值得学习。 Unity Learn 站点上有关对象池的本教程是在 Unity 中实现对象池系统的绝佳指南。

Avoiding expensive calls to the Unity API

Sometimes the calls our code makes to other functions or APIs can be unexpectedly costly. There could be many reasons for this. What looks like a variable could in fact be an accessor.) that contains additional code, triggers an event or makes a call from managed code to engine code.
有时,我们的代码对其他函数或 API 的调用可能会出乎意料地昂贵。 这可能有很多原因。 看起来像变量的东西实际上可能是访问器。)它包含附加代码、触发事件或从托管代码调用引擎代码。

In this section we will look at a few examples of Unity API calls that are more costly than they may appear. We will consider how we might reduce or avoid these costs. These examples demonstrate different underlying causes for the cost, and the suggested solutions can be applied to other similar situations.
在本节中,我们将看一些 Unity API 调用的示例,它们的成本比它们看起来的要高。 我们将考虑如何减少或避免这些成本。 这些示例展示了成本的不同根本原因,建议的解决方案可以应用于其他类似情况。

It’s important to understand that there is no list of Unity API calls that we should avoid. Every API call can be useful in some situations and less useful in others. In all cases, we must profile our game carefully, identify the cause of costly code and think carefully about how to resolve the problem in a way that’s best for our game.
重要的是要了解没有我们应该避免的 Unity API 调用列表。 每个 API 调用在某些情况下都可能有用,而在其他情况下则不太有用。 在所有情况下,我们都必须仔细分析我们的游戏,找出代码成本高昂的原因,并仔细考虑如何以最适合我们游戏的方式解决问题。

SendMessage()

SendMessage() and BroadcastMessage() are very flexible functions that require little knowledge of how a project is structured and are very quick to implement. As such, these functions are very useful for prototyping or for beginner-level scripting. However, they are extremely expensive to use. This is because these functions make use of reflection. Reflection is the term for when code examines and makes decisions about itself at run time rather than at compile time. Code that uses reflection results in far more work for the CPU than code that does not use reflection.
SendMessage() 和 BroadcastMessage() 是非常灵活的函数,它们几乎不需要了解项目的结构,并且可以快速实施。 因此,这些函数对于原型设计或初学者级脚本非常有用。 然而,它们的使用非常昂贵。 这是因为这些函数利用了反射。 反射是代码在运行时而不是在编译时检查并做出关于自身的决定的术语。 使用反射的代码比不使用反射的代码为 CPU 带来更多的工作。

It is recommended that SendMessage() and BroadcastMessage() are used only for prototyping and that other functions are used wherever possible. For example, if we know which component we want to call a function on, we should reference the component directly and call the function that way. If we do not know which component we wish to call a function on, we could consider using Events or Delegates.
建议将 SendMessage() 和 BroadcastMessage() 仅用于原型设计,并尽可能使用其他函数。 例如,如果我们知道要在哪个组件上调用函数,我们应该直接引用该组件并以这种方式调用该函数。 如果我们不知道要在哪个组件上调用函数,我们可以考虑使用事件或委托。

Find()

Find() and related functions are powerful but expensive. These functions require Unity to iterate over every GameObject and Component in memory. This means that they are not particularly demanding in small, simple projects but become more expensive to use as the complexity of a project grows.
Find() 和相关函数功能强大但价格昂贵。 这些函数需要 Unity 遍历内存中的每个 GameObject 和 Component。 这意味着它们在小型、简单的项目中的要求并不特别高,但随着项目复杂性的增加,使用起来会变得更加昂贵。

It’s best to use Find() and similar functions infrequently and to cache the results where possible. Some simple techniques that may help us to reduce the use of Find() in our code include setting references to objects using the Inspector panel where possible, or creating scripts that manage references to things that are commonly searched for.
最好不经常使用 Find() 和类似函数,并尽可能缓存结果。 一些可以帮助我们减少在代码中使用 Find() 的简单技术包括在可能的情况下使用 Inspector 面板设置对对象的引用,或者创建脚本来管理对通常搜索的事物的引用。

Transform

Setting the position or rotation of a transform causes an internal OnTransformChanged event to propagate to all of that transform’s children. This means that it’s relatively expensive to set a transform’s position and rotation values, especially in transforms that have many children.
设置变换的位置或旋转会导致内部 OnTransformChanged 事件传播到该变换的所有子级。这意味着设置变换的位置和旋转值相对昂贵,尤其是在具有许多子变换的变换中。

To limit the number of these internal events, we should avoid setting the value of these properties more often than necessary. For example, we might perform one calculation to set a transform’s x position and then another to set its z position in Update(). In this example, we should consider copying the transform’s position to a Vector3, performing the required calculations on that Vector3 and then setting the transform’s position to the value of that Vector3. This would result in only one OnTransformChanged event.
为了限制这些内部事件的数量,我们应该避免过于频繁地设置这些属性的值。例如,我们可能会执行一次计算来设置变换的 x 位置,然后在 Update() 中执行另一次计算来设置其 z 位置。在此示例中,我们应该考虑将变换的位置复制到 Vector3,对该 Vector3 执行所需的计算,然后将变换的位置设置为该 Vector3 的值。这将只导致一个 OnTransformChanged 事件。

Transform.position is an example of an accessor that results in a calculation behind the scenes. This can be contrasted with Transform.localPosition. The value of localPosition is stored in the transform and calling Transform.localPosition simply returns this value. However, the transform’s world position is calculated every time we call Transform.position.
Transform.position 是访问器的一个示例,它会在后台进行计算。这可以与 Transform.localPosition 进行对比。 localPosition 的值存储在变换中,调用 Transform.localPosition 只返回该值。但是,每次调用 Transform.position 时都会计算变换的世界位置。

If our code makes frequent use of Transform.position and we can use Transform.localPosition in its place, this will result in fewer CPU instructions and may ultimately benefit performance. If we make frequent use Transform.position, we should cache it where possible.
如果我们的代码频繁使用 Transform.position 并且我们可以使用 Transform.localPosition 代替它,这将导致更少的 CPU 指令并最终可能提高性能。如果我们经常使用 Transform.position,我们应该尽可能缓存它。

Update()

Update(), LateUpdate() and other event functions look like simple functions, but they have a hidden overhead. These functions require communication between engine code and managed code every time they are called. In addition to this, Unity carries out a number of safety checks before calling these functions. The safety checks ensure that the GameObject is in a valid state, hasn’t been destroyed, and so on. This overhead is not particularly large for any single call, but it can add up in a game that has thousands of MonoBehaviours.
Update()、LateUpdate() 和其他事件函数看起来很简单,但它们都有隐藏的开销。这些函数每次调用时都需要引擎代码和托管代码之间的通信。除此之外,Unity 在调用这些函数之前会执行一些安全检查。安全检查确保 GameObject 处于有效状态、未被销毁等。这种开销对于任何单个调用都不是特别大,但它可以在具有数千个 MonoBehaviours 的游戏中加起来。

For this reason, empty Update() calls can be particularly wasteful. We may assume that because the function is empty and our code contains no direct calls to it, the empty function will not run. This is not the case: behind the scenes, these safety checks and native calls still happen even when the body of the Update() function is blank. To avoid wasted CPU time, we should ensure that our game does not contain empty Update() calls.
出于这个原因,空的 Update() 调用可能特别浪费。我们可以假设因为函数是空的并且我们的代码不包含对它的直接调用,所以空函数不会运行。情况并非如此:在幕后,即使 Update() 函数的主体为空白,这些安全检查和本机调用仍然会发生。为了避免浪费 CPU 时间,我们应该确保我们的游戏不包含空的 Update() 调用。

If our game has a great many active MonoBehaviours with Update() calls, we may benefit from structuring our code differently to reduce this overhead. This Unity blog post on this subject goes into much more detail on this topic.
如果我们的游戏有大量带有 Update() 调用的活动 MonoBehaviours,我们可能会受益于以不同的方式构建代码以减少这种开销。这篇关于这个主题的 Unity 博客文章[Unity脚本优化] Unity magic methods 更详细地介绍了这个主题。

Vector2 and Vector3

We know that some operations simply result in more CPU instructions than other operations. Vector math operations are an example of this: they are simply more complex than float or int math operations. Although the actual difference in the time taken for two such calculations is tiny, at sufficient scale such operations can impact performance.
我们知道,某些操作只会导致比其他操作更多的 CPU 指令。 矢量数学运算就是一个例子:它们比浮点或整数数学运算更复杂。 尽管两次此类计算所用时间的实际差异很小,但在足够大的规模下,此类操作会影响性能。

It’s common and convenient to use Unity’s Vector2 and Vector3 structs for mathematical operations, especially when dealing with transforms. If we perform many frequent Vector2 and Vector3 math operations in our code, for example in nested loops in Update() on a great many GameObjects, we may well be creating unnecessary work for the CPU. In these cases we may be able to make a performance saving by performing int or float calculations instead.
使用 Unity 的 Vector2 和 Vector3 结构进行数学运算是常见且方便的,尤其是在处理变换时。 如果我们在代码中执行许多频繁的 Vector2 和 Vector3 数学运算,例如在大量 GameObjects 上的 Update() 中的嵌套循环中,我们很可能会为 CPU 创建不必要的工作。 在这些情况下,我们可以通过执行 int 或 float 计算来节省性能。

Earlier in this article, we learned that the CPU instructions required to perform a square root calculation are slower than those used for, say, simple multiplication. Both Vector2.magnitude and Vector3.magnitude are examples of this, as they both involve square root calculations. Additionally, Vector2.Distance and Vector3.Distance use magnitude behind the scenes.
在本文前面,我们了解到执行平方根计算所需的 CPU 指令比用于简单乘法的指令要慢。 Vector2.magnitude 和 Vector3.magnitude 都是这样的例子,因为它们都涉及平方根计算。 此外,Vector2.Distance 和 Vector3.Distance 在幕后使用magnitude。

If our game makes extensive and very frequent use of magnitude or Distance, it may be possible for us to avoid the relatively expensive square root calculation by using Vector2.sqrMagnitude and Vector3.sqrMagnitude instead. Again, replacing a single call will result in only a tiny difference, but at a sufficiently large scale it may be possible to make a useful performance saving.
如果我们的游戏广泛且非常频繁地使用magnitude或Distance,我们可以通过使用 Vector2.sqrMagnitude 和 Vector3.sqrMagnitude 来避免相对昂贵的平方根计算。 同样,替换单个调用只会导致微小的差异,但在足够大的规模下,可能会节省有用的性能。

Camera.main

Camera.main is a convenient Unity API call that returns a reference to the first enabled Camera component that is tagged with “Main Camera”. This is another example of something that looks like a variable but is in fact an accessor. In this case, the accessor calls an internal function similar to Find() behind the scenes. Camera.main therefore suffers from the same problem as Find(): it searches through all GameObjects and Components in memory and can be very expensive to use.
Camera.main 是一个方便的 Unity API 调用,它返回对第一个启用的带有“主相机”标签的相机组件的引用。 这是另一个看起来像变量但实际上是访问器的例子。 在这种情况下,访问器在后台调用类似于 Find() 的内部函数。 因此,Camera.main 遇到了与 Find() 相同的问题:它搜索内存中的所有游戏对象和组件,并且使用起来可能非常昂贵。

To avoid this potentially expensive call, we should either cache the result of Camera.main or avoid its use altogether and manually manage references to our cameras.
为了避免这种潜在的昂贵调用,我们应该缓存 Camera.main 的结果,或者完全避免使用它并手动管理对我们相机的引用。

Other Unity API calls and further optimizations

We have considered a few common examples of Unity API calls that may be unexpectedly costly, and learned about the different reasons behind this cost. However, this is by no means an exhaustive list of ways to improve the efficiency of our Unity API calls.
我们已经考虑了一些常见的 Unity API 调用示例,这些示例可能会出乎意料地昂贵,并了解了这种成本背后的不同原因。 但是,这绝不是提高 Unity API 调用效率的方法的详尽列表。

This article on performance in Unity is a wide-ranging guide to optimization in Unity that contains a number of other Unity API optimizations that we may find useful. Additionally, that article goes into considerable depth about further optimizations that are beyond the scope of this relatively high-level and beginner-friendly article.
这篇关于 Unity 性能的文章是一个广泛的 Unity 优化指南,其中包含许多我们可能会发现有用的其他 Unity API 优化。 此外,该文章深入探讨了超出这篇相对高级且对初学者友好的文章范围之外的进一步优化。

Running code only when it needs to run

There’s a saying in programming: “the fastest code is the code that doesn’t run”. Often, the most efficient way to solve a performance problem is not to use an advanced technique: it is simply to remove code that doesn’t need to be there in the first place. Let’s look at a couple of examples to see where we could make to make this sort of saving.
编程中有句谚语:“最快的代码是不会运行的代码”。通常,解决性能问题的最有效方法不是使用高级技术:它只是删除一开始就不需要的代码。让我们看几个例子,看看我们可以在哪里进行这种节省。

Culling

Unity contains code that checks whether objects are within the frustum of a camera. If they are not within the frustum of a camera, code related to rendering these objects does not run. The term for this is frustum culling.
Unity 包含检查对象是否在相机的截锥体内的代码。如果它们不在相机的截锥体内,则与渲染这些对象相关的代码不会运行。这个术语是截锥体剔除。

We can take a similar approach to the code in our scripts. If we have code that relates to the visual state of an object, we may not need to execute this code when the object cannot be seen by the player. In a complex Scene with many objects, this can result in considerable performance savings.
我们可以对脚本中的代码采取类似的方法。如果我们有与对象的视觉状态相关的代码,当玩家看不到对象时,我们可能不需要执行此代码。在具有许多对象的复杂场景中,这可以显着节省性能。

In the following simplified example code, we have an example of a patrolling enemy. Every time Update() is called, the script controlling this enemy calls two example functions: one related to moving the enemy, one related to its visual state.
在下面的简化示例代码中,我们有一个巡逻敌人的示例。每次调用 Update() 时,控制这个敌人的脚本都会调用两个示例函数:一个与移动敌人有关,一个与它的视觉状态有关。

void Update() 
{ 
	UpdateTransformPosition(); 
	UpdateAnimations(); 
}

In the following code, we now check whether the enemy’s renderer is within the frustum of any camera. The code related to the enemy’s visual state runs only if the enemy is visible.
在下面的代码中,我们现在检查敌人的渲染器是否在任何相机的截锥体内。 与敌人的视觉状态相关的代码只有在敌人可见时才会运行。

private Renderer myRenderer;
void Start() 
{ 
	myRenderer = GetComponent<Renderer>(); 
} 
void Update() 
{ 
	UpdateTransformPosition(); 
	if (myRenderer.isVisible) 
	{ 
		UpateAnimations(); 
	} 
}

Disabling code when things are not seen by the player can be achieved in a few ways. If we know that there certain objects in our scene are not visible at a particular point in the game, we can manually disable them. When we are less certain and need to calculate visibility, we could use a coarse calculation (for example, checking if the object behind the player), functions such as OnBecameInvisible() and OnBecameVisible(), or a more detailed raycast. The best implementation depends very much on our game, and experimentation and profiling are essential.
当玩家看不到事物时禁用代码可以通过几种方式实现。 如果我们知道场景中的某些对象在游戏中的某个特定点不可见,我们可以手动禁用它们。 当我们不太确定并且需要计算可见性时,我们可以使用粗略的计算(例如,检查播放器后面的对象是否)、诸如 OnBecameInvisible() 和 OnBecameVisible() 之类的函数,或者更详细的光线投射。 最好的实现很大程度上取决于我们的游戏,实验和分析是必不可少的。

Level of detail

Level of detail, also known as LOD, is another common rendering optimization technique. Objects nearest to the player are rendered at full fidelity using detailed meshes and textures. Distant objects use less detailed meshes and textures. A similar approach can be used with our code. For example, we may have an enemy with an AI script that determines its behavior. Part of this behavior may involve costly operations for determining what it can see and hear, and how it should react to this input. We could use a level of detail system to enable and disable these expensive operations based on the enemy’s distance from the player. In a Scene with many of these enemies, we could make a considerable performance saving if only the nearest enemies are performing the most expensive operations.
细节层次,也称为 LOD,是另一种常见的渲染优化技术。使用详细的网格和纹理以完全保真度渲染离玩家最近的对象。远处的物体使用不太详细的网格和纹理。我们的代码可以使用类似的方法。例如,我们可能有一个带有决定其行为的 AI 脚本的敌人。这种行为的一部分可能涉及昂贵的操作,以确定它可以看到和听到什么,以及它应该如何对这个输入做出反应。我们可以使用详细程度系统来根据敌人与玩家的距离来启用和禁用这些昂贵的操作。在有许多这样的敌人的场景中,如果只有最近的敌人执行最昂贵的操作,我们可以节省相当多的性能。

Unity’s CullingGroup API allows us to hook into Unity’s LOD system to optimize our code. The Manual page for the CullingGroup API contains several examples of how this might be used in our game. As ever, we should test, profile and find the right solution for our game.

Unity 的 CullingGroup API https://www.bilibili.com/read/cv13696296允许我们连接到 Unity 的 LOD 系统以优化我们的代码。 CullingGroup API 的手册页包含几个如何在我们的游戏中使用它的示例。与以往一样,我们应该测试、分析并为我们的游戏找到正确的解决方案。
实例

Conclusion

We’ve learned how what happens to the code we write when our Unity game is built and run, why our code can cause performance problems and how to minimize the impact of expensive on our game. We’ve learned about a number of common causes of performance problems in our code, and considered a few different solutions. Using this knowledge and our profiling tools, we should now be able to diagnose, understand and fix performance problems related to the code in our game.
我们已经了解了在构建和运行 Unity 游戏时我们编写的代码会发生什么变化,为什么我们的代码会导致性能问题,以及如何将代价高昂对游戏的影响降至最低。 我们已经了解了代码中性能问题的一些常见原因,并考虑了一些不同的解决方案。 使用这些知识和我们的分析工具,我们现在应该能够诊断、理解和修复与游戏代码相关的性能问题。

猜你喜欢

转载自blog.csdn.net/fztfztfzt/article/details/122785544
今日推荐