Move code out of loops when possible
중복된 루프사용은 자제하라.
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]);
}
}
}
Only run code when things change
실행 코드를 반복적으로 실행하는 것은 바꿔라.
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 the score has changed.
private int score;
public void IncrementScore(int incrementBy)
{
score += incrementBy;
DisplayScore(score);
}
Run code every [x] frames
코드가 자주 사용된다고 하지만 꼭 매 프레임마다 실행시킬 필요는 없다.
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.
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 프레임일 경우 3개로 분리하여 함수 사용이 가능하다.
private int interval = 3;
void Update()
{
if(Time.frameCount % interval == 0)
{
ExampleExpensiveFunction();
}
else if(Time.frameCount % 1 == 1)
{
AnotherExampleExpensiveFunction();
}
}
Use caching
매 프레임마다 실행하는 비싼 함수는 초반에 캐싱하여 과부하를 줄인다.
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().
private Renderer myRenderer;
void Start()
{
myRenderer = GetComponent<Renderer>();
}
void Update()
{
ExampleFunction(myRenderer);
}
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.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
G.C를 최소화하여 향상한다.
- Use object pooling (오브젝트 풀링 사용)
- Avoiding expensive calls to the Unity API (비싼 Unity API를 사용하지 않는다.)
- SendMessage() (옆의 함수는 사용하는걸 권장하지 않는다.)
- Find() (작은 프로젝트는 사용해도 상관없지만, 프로젝트가 크다면 권장하지 않는다.)
- 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.To limit the number of these internal events, we should avoid setting the value of these properties more often than necessary.
Transform의 위치 혹은 방향을 설정할때 내부적으로 OnTranformChanged 이벤트가 transfrom's의 자식만큼 실행된다. 필연적으로 많은 자식이 있다면, 비싼 실행 비용을 동반한다. 이러한 행위는 자제해야한다.
If we make frequent use Transform.position, we should cache it where possible.만약 필연적으로 위치값을 받아야한다면, 캐싱하여 공통적으로 사용하자. - Update() (지속적 프레임 업데이트 낭비를 줄이자)
- Vector2 and Vector3
(Vector2보다 Vector3의 계산 비용이 부담된다. 너의 플랫폼이 Vector2에 맞다면, 혹은 사용이 가능하다면 Vector2 사용을 권장한다.) - Camera.main (4번과 같은 의미. 그러므로 캐싱을 사용하여 공통적으로 관리하는걸 권장한다.)
Running code only when it needs to run
Culling
카메라에 보이지 않는 렌더링까지 실행시킬 필요는 없다.
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 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.
플레이어에게 보이지 않을 때 코드를 비활성화하는 방법은 몇 가지입니다.
씬(scene)의 특정 개체가 게임의 특정 지점에서 보이지 않는 경우 수동으로 비활성화할 수 있습니다. 우리가 덜 확실하고 가시성을 계산해야 할 때, 우리는 대략적인 계산(예를 들어, 플레이어 뒤에 있는 물체의 확인), OnBe와 같은 함수들을 사용할 수 있다.Invisible() 및 On Be Visible() 또는 더 자세한 레이캐스트가 표시됩니다. 최고의 구현은 우리의 게임에 매우 많이 달려 있으며, 실험과 프로파일링은 필수적이다.
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.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.We’ve learned 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 expensiveness 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.
LOD라고도 알려진 상세 수준은 또 다른 일반적인 렌더링 최적화 기법이다.
가까울 수록 완벽한 랜더링 퀄리티를 보여주고, 멀어질수록 상대적으로 증감하는 랜더링 퀼리티를 보여주는 기법을 사용한다.
예를 들어, 우리는 그것의 행동을 결정하는 AI 스크립트를 가진 적을 가질 수 있다. 이 동작의 일부는 보고 들을 수 있는 것과 이 입력에 어떻게 반응해야 하는지를 결정하기 위한 비용이 많이 드는 작업을 포함할 수 있다. 우리는 플레이어와의 거리에 따라 이러한 값비싼 작전을 활성화하고 비활성화하기 위해 세부 시스템을 사용할 수 있다. 이러한 적이 많은 장면에서 가장 가까운 적만 가장 비싼 작업을 수행할 경우 상당한 성능 절감을 달성할 수 있습니다.
Unity의 CullingGroup API를 사용하면 Unity의 LOD 시스템에 연결하여 코드를 최적화할 수 있습니다. CullingGroup API의 Manual 페이지에는 이 기능이 게임에서 어떻게 사용되는지에 대한 몇 가지 예가 포함되어 있습니다. 항상 그렇듯이, 우리는 우리의 게임에 적합한 솔루션을 테스트하고 프로파일링하고 찾아야 합니다.Unity 게임이 구축되어 실행될 때 작성하는 코드에 어떤 일이 발생하는지, 코드가 성능 문제를 일으킬 수 있는 이유 및 비용이 게임에 미치는 영향을 최소화하는 방법에 대해 알아보았습니다. 우리는 코드에서 성능 문제의 여러 가지 일반적인 원인에 대해 배웠고 몇 가지 다른 해결책을 고려했습니다. 이제 이 지식과 프로파일링 도구를 사용하여 게임의 코드와 관련된 성능 문제를 진단하고 이해하고 수정할 수 있습니다.
Reducing the impact of garbage collection
G.C의 조절로 기능 향상
- G.C 작동 빈도 조절
- G.C 작동 시간 조절
- 트리거를 통한 작동 시간 조작
=> 힙 영역의 요소를 최소화하여 빈도와 시간을 줄이고, 3번은 어려운 경우지만 고스팩 개발자의 경우 충분히 시도할 가치가 있다.
Reducing the amount of garbage created
G.C 작동 빈도 조절
Caching
캐싱 조절
힙 할당으로 이어지는 함수를 반복적으로 호출한 다음 결과를 폐기하면 불필요한 가비가 발생한다.
대신 이러한 객체에 대한 참조를 저장하고 재사용해야 합니다. 이 기술을 캐싱이라고 합니다.
void OnTriggerEnter(Collider other)
{
Renderer[] allRenderers = FindObjectsOfType<Renderer>();
ExampleFunction(allRenderers);
}
이 경우 함수 사용시 배열이 일회성으로 생성되어 많은 힙 할당이 이루어질 수 있다.
private Renderer[] allRenderers;
void Start()
{
allRenderers = FindObjectsOfType<Renderer>();
}
void OnTriggerEnter(Collider other)
{
ExampleFunction(allRenderers);
}
Don’t allocate in functions that are called frequently
빈번히 사용되는 함수를 할당하여 사용하지마라.
MonoBehaviour에서 사용되는 함수에 힙 할당은 신중해야한다.
void Update()
{
ExampleGarbageGeneratingFunction(transform.position.x);
}
private float previousTransformPositionX;
void Update()
{
float transformPositionX = transform.position.x;
if (transformPositionX != previousTransformPositionX)
{
ExampleGarbageGeneratingFunction(transformPositionX);
previousTransformPositionX = transformPositionX;
}
}
또 다른 방법으로 프레임을 나누거나 시간을 나누어 불필요한 할당을 줄인다.
void Update()
{
ExampleGarbageGeneratingFunction();
}
private float timeSinceLastCalled;
private float delay = 1f;
void Update()
{
timeSinceLastCalled += Time.deltaTime;
if (timeSinceLastCalled > delay)
{
ExampleGarbageGeneratingFunction();
timeSinceLastCalled = 0f;
}
}
Clearing collections
collections 관리하기
collections을 할당할 경우 힙영역에 포함된다. 빈도수를 높일 수록 G.C의 부화를 야기할 수 있다.
void Update()
{
List myList = new List();
PopulateList(myList);
}
다음 예제에서는 컬렉션이 생성되거나 컬렉션의 크기를 뒤에서 조정해야 하는 경우에만 할당이 수행됩니다. 이것은 발생하는 쓰레기의 양을 크게 줄인다.
private List myList = new List();
void Update()
{
myList.Clear();
PopulateList(myList);
}
Object pooling
오브젝트풀링 쓰기.
https://en.wikipedia.org/wiki/Object_pool_pattern
Object pool pattern - Wikipedia
From Wikipedia, the free encyclopedia Jump to navigation Jump to search The object pool pattern is a software creational design pattern that uses a set of initialized objects kept ready to use – a "pool" – rather than allocating and destroying them on
en.wikipedia.org
Common causes of unnecessary heap allocations
불필요한 힙 할당의 일반적인 원인.
Strings
Strings은 C#에서 참조 유형이다. 즉, 만들고 폐기할 경우 가비지가 생성된다. Strings은 많이 사용하는 형식이기에 많은 비효율을 만들 수 있다.
String도 C#에선 불변하므로 처음 생성된 후에는 값을 변경할 수 없다. 문자열을 조작할 때, C# 내부에서 새로운 문자열을 생성하여 복사하고, 이전 String은 폐기한다. 이것은 G.C에 쌓이게 된다.
https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/strings/
문자열 - C# 프로그래밍 가이드
C# 프로그래밍에서 문자열에 대해 알아봅니다. 문자열 선언과 초기화, 문자열 개체의 불변성 및 문자열 이스케이프 시퀀스에 대한 정보를 확인하세요.
docs.microsoft.com
이러한 문제를 해결하기 위해
- 동일한 문자열 값을 두 번 이상 사용하는 경우 문자열을 한 번 생성하고 값을 캐시해야 합니다
- 자주 업데이트되고 연결된 문자열을 포함하는 텍스트 구성 요소가 있는 경우 두 개의 텍스트 구성 요소로 분리하는 것을 고려할 수 있습니다.
- 런타임에 문자열을 빌드해야 한다면 String Builder 클래스를 사용해야 합니다. StringBuilder 클래스는 할당 없이 문자열을 빌드하도록 설계되었으며 복잡한 문자열을 연결할 때 생성되는 가비지의 양을 절약할 수 있습니다.
- 디버깅 호출은 삭제해야 합니다.
비효율적인 문자열 사용을 통해 불필요한 가비지를 생성하는 코드의 예를 살펴보자.
public Text timerText;
private float timer;
void Update()
{
timer += Time.deltaTime;
timerText.text = "TIME:" + timer.ToString();
}
public Text timerHeaderText;
public Text timerValueText;
private float timer;
void Start()
{
timerHeaderText.text = "TIME:";
}
void Update()
{
timerValueText.text = timer.toString();
}
Unity function calls
Unity 자체든 플러그인이든 우리가 직접 작성하지 않은 코드를 호출할 때마다 쓰레기가 생성될 수 있다는 것을 알아야 합니다. 일부 Unity 함수 호출은 힙 할당을 생성하므로 불필요한 가비지가 생성되지 않도록 주의해야 합니다. 항상 그렇듯이, 게임을 신중하게 프로파일링하고, 쓰레기가 어디서 생성되는지 확인하고, 어떻게 처리할지에 대해 신중하게 생각하는 것이 가장 좋습니다. 어떤 경우에는 함수의 결과를 캐시하는 것이 현명할 수 있고, 다른 경우에는 함수를 덜 자주 호출하는 것이 현명할 수 있으며, 다른 경우에는 다른 함수를 사용하기 위해 코드를 리팩터링하는 것이 최선일 수 있다.
그렇기는 하지만 힙 할당을 유발하는 Unity 함수의 일반적인 몇 가지 예를 살펴보고 이를 가장 잘 처리하는 방법을 생각해 보겠습니다.
1. 배열에 대한 참조를 캐싱.
void ExampleFunction()
{
for (int i = 0; i < myMesh.normals.Length; i++)
{
Vector3 normal = myMesh.normals[i];
}
}
void ExampleFunction()
{
Vector3[] meshNormals = myMesh.normals;
for (int i = 0; i < meshNormals.Length; i++)
{
Vector3 normal = meshNormals[i];
}
}
2. GameObject.name 또는 GameObject.tag에서 찾을 수 있습니다. 이 두 가지 모두 새 문자열을 반환하는 접근자로, 이 함수를 호출하면 가비지가 생성됩니다.
private string playerTag = "Player";
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.tag == playerTag;
}
private string playerTag = "Player";
void OnTriggerEnter(Collider other)
{
bool isPlayer = other.gameObject.CompareTag(playerTag);
}
Coroutines
유니티에서 제공하는 Coroutines 에 관련하여 효율적인 관리가 필요하다.
yield return 0;
이렇게 진행할 시, 0값이 할당 된다. 이는 G를 생성한다.
yield return null;
while (!isComplete)
{
yield return new WaitForSeconds(1f);
}
반복문 실행 시, 새로운 Seconds에 대한 할당값이 지정된다. 이를, 캐싱하여 사용하자.
WaitForSeconds delay = new WaitForSeconds(1f);
while (!isComplete)
{
yield return delay;
}
Struct
구조체를 사용하기 보단, Class를 사용하자.
Timing garbage collection
마지막으로, 우리는 스스로 쓰레기 수거를 촉발하기를 바랄지도 모른다. 힙 메모리가 할당되었지만 더 이상 사용되지 않는 경우(예: 자산을 로드할 때 코드가 가비지를 생성한 경우) 가비지 수집 중지 상태가 플레이어에게 영향을 미치지 않는 경우(예: 로딩 화면이 계속 표시되는 동안) 다음 코드를 사용하여 가비지 수집을 요청할 수 있습니다.
System.GC.Collect();
'정리 > UNITY' 카테고리의 다른 글
StringToHash (0) | 2022.05.21 |
---|---|
InvokeRepeating (반복 재생 기능) (0) | 2022.05.21 |
Unity 성능 향상을 위한 권장사항 (Microsoft) (0) | 2022.04.28 |
Menu_UI 기능 구현 (0) | 2022.04.25 |
IL2CPP (0) | 2022.03.19 |