Unity 프로파일러를 사용하여 게임 최적화

성능은 모든 게임의 핵심 요소이며, 게임이 아무리 훌륭하더라도 사용자 컴퓨터에서 제대로 실행되지 않으면 그다지 즐겁지 않을 것입니다.

모든 사람이 고급형 PC나 장치를 갖고 있는 것은 아니기 때문에(모바일을 대상으로 하는 경우) 전체 개발 과정에서 성능을 염두에 두는 것이 중요합니다.

게임이 느리게 실행되는 데에는 여러 가지 이유가 있습니다.

  • 렌더링(고폴리 메시, 복잡한 셰이더 또는 이미지 효과가 너무 많음)
  • 오디오(주로 잘못된 오디오 가져오기 설정으로 인해 발생함)
  • 최적화되지 않은 코드(성능을 요구하는 기능이 잘못된 위치에 포함된 스크립트)

이 튜토리얼에서는 Unity 프로파일러의 도움으로 코드를 최적화하는 방법을 보여 드리겠습니다.

프로파일러

역사적으로 Unity의 성능 디버깅은 지루한 작업이었지만 그 이후로 Profiler이라는 새로운 기능이 추가되었습니다.

프로파일러는 메모리 소비를 모니터링하여 게임의 병목 현상을 신속하게 찾아낼 수 있는 Unity의 도구로, 최적화 프로세스를 크게 단순화합니다.

Unity 프로파일러 창

나쁜 성능

나쁜 성능은 언제든지 발생할 수 있습니다. 적 인스턴스에서 작업 중이고 이를 장면에 배치하면 문제 없이 잘 작동하지만 더 많은 적을 생성할수록 fps(초당 프레임 수)를 확인할 수 있습니다. ) 떨어지기 시작합니다.

아래 예를 확인하세요.

장면에는 큐브를 좌우로 이동하고 개체 이름을 표시하는 스크립트가 연결된 큐브가 있습니다.

SC_ShowName.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
}

통계를 보면 게임이 800+fps로 실행되므로 성능에 거의 영향을 미치지 않는다는 것을 알 수 있습니다.

하지만 큐브를 100번 복제하면 어떤 일이 발생하는지 살펴보겠습니다.

FPS가 700포인트 이상 떨어졌습니다!

참고: 모든 테스트는 Vsync를 비활성화한 상태에서 수행되었습니다.

일반적으로 게임이 끊기거나 멈추거나 FPS가 120 미만으로 떨어지기 시작하면 최적화를 시작하는 것이 좋습니다.

프로파일러를 사용하는 방법?

프로파일러를 사용하려면 다음이 필요합니다.

  • 플레이를 눌러 게임을 시작하세요
  • Window -> Analysis -> Profiler로 이동하여 Profiler를 엽니다(또는 Ctrl + 7을 누름).

  • 다음과 같은 새 창이 나타납니다.

Unity 3D 프로파일러 창

  • 처음에는 (특히 모든 차트 등의 경우) 위협적으로 보일 수 있지만 우리가 살펴볼 부분은 아닙니다.
  • 타임라인 탭을 클릭하고 계층 구조로 변경합니다.

  • 3개의 섹션(EditorLoop, PlayerLoop 및 Profiler.CollectEditorStats)이 표시됩니다.

  • PlayerLoop를 확장하여 계산 능력이 소비되는 모든 부분을 확인하세요. (참고: PlayerLoop 값이 업데이트되지 않으면 프로파일러 창 상단에 있는 "Clear" 버튼을 클릭하세요.)

최상의 결과를 얻으려면 게임이 가장 지연되는 상황(또는 장소)으로 게임 캐릭터를 지시하고 몇 초 동안 기다리십시오.

  • 조금 기다린 후 게임을 중지하고 PlayerLoop 목록을 관찰합니다.

Garbage Collection Allocation을 나타내는 GC Alloc 값을 살펴봐야 합니다. 이는 구성요소에 의해 할당되었지만 더 이상 필요하지 않으며 가비지 수집에 의해 해제되기를 기다리는 메모리 유형입니다. 이상적으로 코드는 쓰레기를 생성하지 않아야 합니다(또는 가능한 한 0에 가까워야 합니다).

시간 ms도 중요한 값으로, 코드가 실행되는 데 걸린 시간을 밀리초 단위로 표시하므로 이상적으로는 이 값도 줄이는 것을 목표로 해야 합니다(값을 캐싱하고 업데이트할 때마다 성능을 요구하는 함수 호출을 피하는 등)..).

문제가 있는 부분을 더 빨리 찾으려면 GC Alloc 열을 클릭하여 값을 높은 값에서 낮은 값으로 정렬하세요.)

  • CPU 사용량 차트에서 아무 곳이나 클릭하면 해당 프레임으로 건너뛸 수 있습니다. 특히 fps가 가장 낮은 최고점을 살펴봐야 합니다.

Unity CPU 사용량 차트

프로파일러가 공개한 내용은 다음과 같습니다.

GUI.Repaint는 45.4KB를 할당하는데, 이는 꽤 많은 양이며 확장하면 더 많은 정보가 드러납니다.

  • 이는 대부분의 할당이 SC_ShowName 스크립트의 GUIUtility.BeginGUI() 및 OnGUI() 메서드에서 나오는 것을 보여주며 최적화를 시작할 수 있음을 알고 있습니다.

GUIUtility.BeginGUI()는 빈 OnGUI() 메서드를 나타냅니다(예, 빈 OnGUI() 메서드도 꽤 많은 메모리를 할당합니다).

모르는 이름을 찾으려면 Google(또는 기타 검색 엔진)을 사용하세요.

최적화해야 할 OnGUI() 부분은 다음과 같습니다.

    void OnGUI()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }

최적화

최적화를 시작해 보겠습니다.

각 SC_ShowName 스크립트는 자체 OnGUI() 메서드를 호출하는데, 이는 인스턴스가 100개 있다는 점을 고려하면 좋지 않습니다. 그렇다면 이에 대해 무엇을 할 수 있습니까? 대답은 다음과 같습니다. 각 큐브에 대해 GUI 메서드를 호출하는 OnGUI() 메서드가 포함된 단일 스크립트를 갖는 것입니다.

  • 먼저 SC_ShowName 스크립트의 기본 OnGUI()를 다른 스크립트에서 호출될 public void GUIMethod()로 대체했습니다.
    public void GUIMethod()
    {
        //Show object name on screen
        Camera mainCamera = Camera.main;
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
    }
  • 그런 다음 새 스크립트를 만들고 이름을 SC_GUIMethod로 지정했습니다.

SC_GUIMethod.cs

using UnityEngine;

public class SC_GUIMethod : MonoBehaviour
{
    SC_ShowName[] instances; //All instances where GUI method will be called

    void Start()
    {
        //Find all instances
        instances = FindObjectsOfType<SC_ShowName>();
    }

    void OnGUI()
    {
        for(int i = 0; i < instances.Length; i++)
        {
            instances[i].GUIMethod();
        }
    }
}

SC_GUIMethod는 장면의 임의 개체에 연결되고 모든 GUI 메서드를 호출합니다.

  • 우리는 100개의 개별 OnGUI() 메소드를 단 하나로 만들었습니다. 재생을 누르고 결과를 살펴보겠습니다.

  • GUIUtility.BeginGUI()는 이제 36.7KB 대신 368B만 할당하므로 크게 줄어듭니다!

그러나 OnGUI() 메서드는 여전히 메모리를 할당하고 있지만 SC_ShowName 스크립트에서 GUIMethod()만 호출한다는 것을 알고 있으므로 바로 해당 메서드를 디버깅할 것입니다.

하지만 프로파일러는 전역 정보만 표시합니다. 메소드 내부에서 정확히 무슨 일이 일어나고 있는지 어떻게 알 수 있나요?

메서드 내에서 디버깅하기 위해 Unity에는 Profiler.BeginSample이라는 편리한 API가 있습니다.

Profiler.BeginSample을 사용하면 스크립트의 특정 섹션을 캡처하여 완료하는 데 걸린 시간과 할당된 메모리 양을 확인할 수 있습니다.

  • 코드에서 Profiler 클래스를 사용하기 전에 스크립트 시작 부분에서 UnityEngine.Profiling 네임스페이스를 가져와야 합니다.
using UnityEngine.Profiling;
  • 프로파일러 샘플은 캡처 시작 부분에 Profiler.BeginSample("SOME_NAME");를 추가하고 캡처 끝 부분에 Profiler.EndSample();를 추가하여 캡처됩니다. 이것:
        Profiler.BeginSample("SOME_CODE");
        //...your code goes here
        Profiler.EndSample();

GUIMethod()의 어떤 부분이 메모리 할당을 유발하는지 모르기 때문에 Profiler.BeginSample 및 Profiler.EndSample의 각 줄을 묶었습니다. (하지만 메서드에 줄이 많으면 묶을 필요가 없습니다. 각 줄을 짝수로 분할한 다음 거기서부터 작업하세요).

다음은 프로파일러 샘플이 구현된 최종 방법입니다.

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
        Profiler.EndSample();
    }
  • 이제 Play를 누르고 프로파일러에 표시되는 내용을 확인합니다.
  • 모든 샘플이 해당 이름으로 시작하므로 편의상 프로파일러에서 "sc_show_"를 검색했습니다.

  • 흥미롭네요... 코드의 이 부분에 해당하는 sc_show_names 부분 3에 많은 메모리가 할당되고 있습니다.
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);

인터넷 검색을 통해 개체 이름을 얻으면 꽤 많은 메모리가 할당된다는 사실을 발견했습니다. 해결책은 void Start()의 문자열 변수에 개체 이름을 할당하는 것입니다. 이렇게 하면 개체가 한 번만 호출됩니다.

최적화된 코드는 다음과 같습니다.

SC_ShowName.cs

using UnityEngine;
using UnityEngine.Profiling;

public class SC_ShowName : MonoBehaviour
{
    bool moveLeft = true;
    float movedDistance = 0;

    string objectName = "";

    // Start is called before the first frame update
    void Start()
    {
        moveLeft = Random.Range(0, 10) > 5;
        objectName = gameObject.name; //Store Object name to a variable
    }

    // Update is called once per frame
    void Update()
    {
        //Move left and right in ping-pong fashion
        if (moveLeft)
        {
            if(movedDistance > -2)
            {
                movedDistance -= Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x -= Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = false;
            }
        }
        else
        {
            if (movedDistance < 2)
            {
                movedDistance += Time.deltaTime;
                Vector3 currentPosition = transform.position;
                currentPosition.x += Time.deltaTime;
                transform.position = currentPosition;
            }
            else
            {
                moveLeft = true;
            }
        }
    }

    public void GUIMethod()
    {
        //Show object name on screen
        Profiler.BeginSample("sc_show_name part 1");
        Camera mainCamera = Camera.main;
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 2");
        Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
        Profiler.EndSample();

        Profiler.BeginSample("sc_show_name part 3");
        GUI.color = Color.green;
        GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
        Profiler.EndSample();
    }
}
  • 프로파일러에 표시되는 내용을 살펴보겠습니다.

모든 샘플은 0B를 할당하므로 더 이상 메모리가 할당되지 않습니다.

추천 기사
Unity 최적화 팁
Unity에서 모바일 게임 성능 개선
Unity에서 업데이트를 활용하는 방법
최고의 성능을 위한 Unity 오디오 클립 가져오기 설정
Unity용 빌보드 생성기
Unity에서 더 나은 프로그래머가 되는 방법
게임 디자인의 기본 개념