포스트

모바일 퍼포먼스 최적화

위 글은 유니티에서 공식으로 제공하는 E Book을 기반으로 제가 번역, 공부하며 정리한 자료를 글로 남긴 것입니다.

정리

PROFILING

  1. 성능이 안 좋다면, 원인을 정확한 곳에서 찾아라. Profiling을 통해 정확히 어디가 원인인지, 어디서 병목이 생기는지 찾아야 한다.

    1. Profiling은 개발의 초기 단계에서부터, 그리고 자주 해야 한다.
    2. TargetDevice에서 테스트는 필수.
    3. Editor와 Build 결과는 성능이 동일하지 않는 경우가 대부분.
    4. Actual Target Device에서 Profiler를 연결할 때에는 Build Settings -> Development Build & Autoconnect Profiler를 체크할 것
    5. Build Settings -> Deep Profiling Profiler에서 스크립트에 대해 좀 더 자세히 관측할 수 있도록 하는 설정. 오버헤드가 상당히 걸리는 작업.
    6. ProfilerMarker를 사용해서 자신의 스크립트 중 병목발생 지점을 찾는 방법.
    7. Unity Profiler -> Timeline or Hierarchy view를 적극적으로 활용할 것 Timeline : 서로 다른 스레드에서 활동이 어떻게 관련되어 있는지 시각화 Hierarchy : ProfileMarkers의 계층 구조
    8. Profile Analyzer
    9. 모바일의 경우에는 이용 가능한 시간의 약 65%만을 사용하여 프레임 간의 냉각시간을 허용해야 한다. 일반적인 프레임 예산은 30fps에서 프레임단 약 22ms, 60fps에서는 프레임당 11ms 정도.
    10. 모바일 기기의 온도가 뜨거우면, Profiler가 실제보다 성능이 좋지않다고 인식할 수도 있다. 일반적으로 Profiling Session은 짧게 갖고, 기기를 10~15분 정도 시원하게 유지하고 다시 Profiling하는 것을 권장.
    11. 모바일에서는 항상 성능이 급락할 수 있음을 감안하고 개발해야 함.

Memory

  1. Unity C#은 자동 메모리 관리 기능이 존재한다. Garbage Collector
    1. 주기적으로 관리되지 않는 상태를 파악하고 할당을 해제한다.
    2. 요구할 때 (on demand) 혹은 새로운 Scene을 Load할 때, native objects와 resources를 할당 해제할 때.
    3. 그러나 이 기능이 실행되는 동안은, Heap 내의 모든 Object를 검사하기 때문에 게임이 버벅거리거나 느리게 실행될 수 있다. Memory in Unity
  2. Memory Profiler

ADAPTIVE PERFORMANCE

  1. Unity와 Samsung의 Adaptive Performance
    1. 기기의 열 및 전력 상태를 모니터링 할 수 있는 API.
    2. 이를 활용하여 장시간 원활한 실행을 유지할 수있도록 동적으로 세부수준(LOD) bias를 줄일 수 있음.
    3. 자동 모드도 제공함.
      • 자동 모드에서 사용하는 주요 지표
        1. 이전 프레임에 기반한 원하는 프레임률
        2. 기기의 온도 수준
        3. Thermal Event에 근접한 기기
        4. 기기의 CPU 혹은 GPU Bound
  2. 예제
    1. Package Manager > Adaptive Performance > Samples

Scene의 메인 카메라를 찾을 때는 Camera.main을 적극 사용할 것. 이전 버전에서는 오히려 값비싼 방법이었다. (내부 동작이 복잡했기 때문) 2022 LTS부터는 해당사항 없음

GetComponent<>() 대신 TryGetComponent<>()를 사용할 것. GetComponent<>()는 성능 저하를 큰 폭으로 야기 성능이나 예외에 대해서 컨트롤하기 용이


PROGRAMMING AND CODE ARCHITECTURE

  • Awake 뒤에 Start가 하는 순서는 보장되어 있다.
    • 그러나 서로 다른 스크립트들의 Awake는 순서가 보장 X
      • 무엇이 먼저 한다고 보장할 수가 없다.
  • Update 의 뒤에 오는 LateUpdate
    • LateUpdate는 어디에 사용?
      • 캐릭터를 따라가는 Camera를 만들 때.
        • Camera의 위치 보정을 Update에서 하면 순서를 보장할 수가 없기 때문.
          • 여기서 Jittering 발생 가능.
  • FixedUpdate는 물리 연산과 주기가 동일
    • 그래서 물리적인 상호작용은 여기 안에 구현하는것을 권장한다.
  • AwakeOnEnable
    • Awake는 무조건 최초의 Update의 직전에
      • 런타임에 오브젝트가 Instantiate되는 경우에는 이 순서를 보장하지 않는다
      • 정확히는 한 프레임 내에서 그 순서를 보장하지 않는다.

매 프레임마다 동작하는 코드는 최소화하라

  • 갱신의 주기를 큰 프레임단위로 지정하고, 이를 이용해서 연산량을 줄이는 것도 방법.
    1
    2
    3
    4
    5
    6
    7
    8
    
    private int interval = 3; 
    void Update() 
    {
      if (Time.frameCount % interval == 0) 
      { 
          ExampleExpensiveFunction(); 
      } 
    }
    

비어있는 유니티 이벤트 함수는 만들지마라

  • Awake, Update와 같은 유니티 이벤트 함수는 그 몸체에 어떠한 코드도 없다해도, 호출이 일어난다. 그래서 사용하지 않는 빈 유니티 이벤트는 오버헤드를 줄이기 위해 지우자.

Debug Log는 지울것

  • 이는 빌드에 포함되면 실제로도 호출되는 함수고, 이로 인해 오버헤드가 발생한다.
  • 다음은 Conditional attribute를 활용하는 방법
    1
    2
    3
    4
    5
    6
    7
    8
    
    public static class Logging 
    { 
      [System.Diagnostics.Conditional(ENABLE_LOG)] 
      static public void Log(object message) 
      { 
          UnityEngine.Debug.Log(message); 
      } 
    }
    
  • Player Settings에서 설정 image

string 대신 hash 값을 사용할 것

  • string을 사용하는건 사실 비싼 작업
    • GC가 돌아야 하기 때문.
  • Animator의 Parameter에 접근할 때에도

적절한 자료구조를 선택할 것

런타임에 컴포넌트를 추가하는 것을 피하라

  • 상당한 성능 비용을 요하는 작업
    • 필수 컴포넌트 검사, Serialization, … 검사할 것이 많다.
  • 대신 컴포넌트가 미리 붙어있는 Prefab을 사용하는 것을 권장.

GameObject와 컴포넌트를 캐싱할 것.

  • 런타임 중의 GetComponent<T> 함수도 높은 비용의 함수
    • GameObject.Find 계열의 함수도 높은 비용의 함수
  • 가급적이면 Awake, Start와 같이 초기에 한번 호출되는 함수에서 한번 받아오고, 이를 캐싱해서 사용하는 것을 권장
  • Camera.main은 제외.

Object Pool을 사용할 것.

  • InstantiateDestroy 대신 Object Pool을 사용할 것.
    • InstantiateDestroy는 GC를 부른다…
  • Unity의 API도 지원한다. 찾아볼 것.
  • Unity Learn about Object Pooling

ScriptableObjects을 사용할 것.

  • 스크립팅 가능한 에셋.
    • Monobehavior와는 다른
  • 가령 몬스터를 만든다고 가정하면 체력, 최대 체력, 공격력, 이동속도, 이런 속성들은 자주, 여러번 사용될 것
    • 처음에 기본 값이 정해지면, 이를 참조하는 여러 인스턴스가 생길 것이다. (몹의 갯수만큼)
    • 이를 Monobehavior로 만들면 복제가 일어나서,
      • 몹이 많아지면, 더더욱 많은 인스턴스가 메모리에 존재할 것이고, 많은 메모리 용량을 차지하겠다.
    • 그러나 Scriptable Object를 사용하면, 값을 복제하지 않고 이를 참조하기 때문에
      • 메모리 관리 측면에서 훨씬 효율적인 방법이 되겠다.
    • 데이터를 저장하고 불러오는 측면에서도, Json과 XML과는 달리 ScriptableObject는 Serization되어있는 데이터이기 때문에, 파싱과 같은 과정을 거치지않고, 그에 따른 오버헤드도 발생하지 않는다.
  • 이에 대해서 다루는 e-Book

Project Configuration

Accelerometer Frequency는 줄이거나 끌 것.

  • 가속도계…?
    • 자이로센서같은 설정…?
  • 물리 상호작용의 비중이 적거나 없다면, 이 설정은 줄이거나 끄는 것이 성능에 더 좋다.
  • 드라마틱한 효과는 없지만, 간단한 설정이니 고려해보자.

불필요한 Player 혹은 Quality settings는 비활성화할 것.

AutoGraphics API

  • 사용하지 않는 플랫폼에 대한 API는 끌것. 이는 사용하지 않는 Shader 의 과도한 변형을 생성하는 걸 막는다.
  • 애플리케이션이 지원하고자 하는것이 아니라면, 구형 CPU에 대한 Target Architectures도 비활성화 할 것.
  • Quality 설정에서도 불필요한 Quality Level도 비활성화할 것.

불필요한 물리도 비활성화할 것.

  • 물리를 사용하지 않는 게임이라면, Auto SimulationAuto Sync Transforms를 비활성화 시킬 것.
    • 이는 게임이 느려질 수 있는 요인이 된다.

적절한 frame rate를 선택하라

  • Application.targetFrameRate 로 설정 가능
  • 모바일은 발열 관리를 해야하기 때문에, 이를 유동적으로 설정하는 방법도 고려할 수 있겠다.
    • 예를들면 로비에서는 30으로 설정하고, 전투영역에서는 60으로 올리는 방법.

깊은 계층구조는 피하라

  • 계층 구조가 깊어질수록 Transform 비용이 늘어난다.
  • Parent를 바꿔주는, Reparenting연산은 특히 비용이 크다. 런타임에서는 빈번히 사용하는 건 피할 것.
  • 휴머노이드 캐릭터에 대해서는 Rig tab > Optimize Game Objects를 체크해줄 것
    • 일반적인 다른 GameObject Hierarchy와는 다른 자료구조를 사용해 Transform연산을 최적화하는 옵션.
    • 장비를 쥐어준다던지 하는 경우에는 예외처리를 활용할 것.

Transfom은 두번보다 한번만 건드릴것.

  • transform.position에 접근하는 코드 하나, transform.rotation에 접근하는 코드 하나 이렇게 따로따로 접근해 조정하기보다, Transform.SetPositionAndRotation함수를 사용할 것.
    • 이것이 연산량을 줄인다. 최적화에 도움.
      1
      2
      
      GameObject.Instantiate(prefab, parent); 
      GameObject.Instantiate(prefab, parent, position, rotation);
      
  • Instantiate하는 경우에도, 위의 경우보다 아래쪽의 방식으로 위치와 회전을 한번에 설정하는 방법을 권장.
    • 이것도 연산량을 줄이는 방법. 최적화에 도움.

Vsync는 켜져있다고 가정하라.

  • 모바일 플랫폼은 half-frame을 렌더링하지 않는다. 에디터에서 Vsync를 비활성화해도(Project Settings > Quality), Vsync는 하드웨어 수준에서 활성화된다. GPU가 충분히 빠르게 새로고침할 수 없으면, 현재 프레임은 유지되어 실질적으로 fps가 감소한다.

Assets

Import textures correctly

대부분의 메모리는 텍스처에 사용될 가능성이 높으므로, 여기에서의 import 설정은 매우 중요하다.

Max Size를 줄이자
  • 시각적으로 수용 가능한 결과를 생성하는 최소 설정을 사용하자.
  • 이는 비파괴적이며 텍스처 메모리를 빠르게 줄일 수 있다.
  • 원본 해상도를 고해상도로 만들고, 플랫폼마다 적절한 Size를 맞춰주자.
2의 거듭제곱(POT; Power of Two)를 사용하자
  • Unity는 모바일 텍스처 압축 포맷(PVRCT 또는 ETC)에 대해 2의 거듭제곱 차원의 텍스처를 요구한다.
  • 리소스를 만들 때, 2의 거듭제곱 수에 맞춰서 만들자.

텍스처 아틀라스 사용

  • 여러 텍스처를 하나의 텍스처에 배치하면 Draw Call을 줄이고 렌더링 속도를 높일 수 있다.
    • 이는 Batching과 관련.
  • Unity 스프라이트 아틀라스나 타사의 텍스처 패커를 사용하여 텍스처를 아틀라스화하자.

읽기/쓰기 활성화 옵션은 끄자

  • 활성화되면, 이 옵션은 CPU와 GPU가 접근 가능한 메모리 양쪽에 복사본을 생성하여 텍스처의 메모리 사용량을 두 배로 증가시킨다. 대부분의 경우, 이를 비활성화 상태로 유지하자.
  • 런타임에 텍스처를 생성하는 경우, Texture2D.Apply를 호출할 때 makeNoLongerReadabletrue로 설정하여 이를 강제하자.

필요하지 않은 Mip Map 비활성화

  • 화면 상에서 일정한 크기를 유지하는 텍스처에는 Mip Map이 필요하지 않다.
  • 예를 들어, 2D 스프라이트와 UI 그래픽에는 Mip Map을 비활성화하자. (카메라로부터의 거리가 변하는 3D 모델에 대해서는 Mip Map을 활성화 상태로 유지할 것)

Compress Texture

  • 동일한 모델과 텍스처를 사용하는 두 가지 예를 고려해 보자. 왼쪽 설정은 오른쪽 설정보다 거의 8배의 메모리를 소비하며, 시각적 품질에서 큰 이점을 제공하지 않는다.

image 압축되지 않은 텍스처는 더 많은 메모리를 요구한다.

iOS와 Android 모두에 대해 적응형 스케일러블 텍스처 압축(ATSC; Adaptive Scalable Texture Compression) 을 사용하자. 개발 중인 대다수의 게임은 ATSC 압축을 지원하는 최소 사양 장치를 타깃으로 한다.

유일한 예외는 다음과 같다:

  • A7 장치 이하를 타깃으로 하는 iOS 게임 (예: iPhone 5, 5S 등) - PVRTC 사용
  • 2016년 이전 장치를 타깃으로 하는 Android 게임 - ETC2 (Ericsson Texture Compression) 사용

PVRTC와 ETC와 같은 압축 포맷의 품질이 충분히 높지 않고, 타깃 플랫폼에서 ASTC가 완전히 지원되지 않는 경우, 32비트 텍스처 대신 16비트 텍스처를 사용해 보라.

플랫폼별 권장 텍스처 압축 포맷에 대한 자세한 정보는 매뉴얼을 참조하라.

Adjust mesh import settings

텍스처와 마찬가지로, 메쉬는 주의 깊게 가져오지 않으면 과도한 메모리를 소비할 수 있다. 메쉬의 메모리 소비를 최소화하기 위해서는:

  • 메쉬 압축 : 공격적인 압축은 디스크 공간을 줄일 수 있다. (단, 런타임 시의 메모리는 영향을 받지 않는다). 메쉬 양자화는 부정확성을 초래할 수 있으므로, 모델에 맞는 압축 수준을 실험해 보자.

  • 읽기/쓰기 비활성화 : 이 옵션을 활성화하면 메쉬가 메모리에 중복되어, 하나의 복사본이 시스템 메모리에, 다른 하나가 GPU 메모리에 유지된다. 대부분의 경우, 이를 비활성화해야 한다. (Unity 2019.2 및 이전 버전에서는 이 옵션이 기본적으로 선택되어 있다)

  • 리그와 블렌드쉐이프 비활성화 : 메쉬가 골격이나 블렌드쉐이프 애니메이션을 필요로 하지 않는다면, 가능한 한 이 옵션들을 비활성화하라.

  • 가능하다면, 노멀과 탄젠트 비활성화 : 메쉬의 metarial 이 노멀이나 탄젠트를 필요로 하지 않는다는 것이 확실하다면, 추가 절약을 위해 이 옵션들의 선택을 해제하라.

Check your polygon counts

고해상도 모델은 더 많은 메모리 사용과 잠재적으로 긴 GPU 시간을 의미한다. 배경 기하학이 50만 개의 폴리곤이 필요한가? 선택한 DCC 패키지에서 모델을 줄이는 것을 고려하라. 카메라의 시점에서 보이지 않는 폴리곤을 삭제하라. 고밀도 메쉬 대신에 텍스처와 노멀 맵을 사용해 세밀한 디테일을 추가하라.

Automate your import settings using the AssetPostprocessor

AssetPostprocessor를 사용하면 import 파이프라인에 연결하여 Asset 을 가져오기 전이나 가져오는 동안 스크립트를 실행할 수 있다. 이는 프리셋과 유사한 방식으로 코드를 통해 모델, 텍스처, 오디오 등을 가져오기 전후에 설정을 사용자 정의하도록 유도한다. GDC 2023에서 진행된 “게임 생성의 모든 단계에 대한 기술적 팁” 토크에서 이 과정에 대해 자세히 알아볼 수 있다.

Unity DataTools

Unity DataTools는 Unity에서 제공하는 오픈 소스 도구 모음으로, Unity 프로젝트에서 데이터 관리 및 직렬화 능력을 향상시키는 것을 목표로 한다. 사용되지 않는 애셋 식별, 애셋 의존성 탐지, 빌드 크기 축소와 같은 프로젝트 데이터를 분석하고 최적화하는 기능을 포함한다.

Use the Addressable Asset System

Addressable Asset System은 “주소” 또는 별칭으로 AssetBundle을 로딩함으로써 콘텐츠를 관리하는 간소화된 방법을 제공한다. 이 통합 시스템은 로컬 경로나 원격 콘텐츠 전송 네트워크(CDN)에서 비동기적으로 로드한다.

코드가 아닌 자산(모델, 텍스처, 프리팹, 오디오, 심지어 전체 장면까지)을 AssetBundle로 분할하면, 이를 다운로드 가능한 콘텐츠(DLC)로 분리할 수 있다.

image

그런 다음, Addressables를 사용하여 모바일 애플리케이션을 위한 더 작은 초기 빌드를 생성하라. Cloud Content Delivery를 통해 게임 콘텐츠를 호스트하고 플레이어가 게임을 진행함에 따라 콘텐츠를 전달할 수 있다.

image

Addressable Asset System이 어떻게 자산 관리의 고통을 덜어줄 수 있는지 보려면 여기를 클릭하세요.


Graphics and GPU Optimization

각 프레임마다 유니티는 렌더링해야 할 오브젝트를 결정한 후, Draw Call을 생성한다. Draw Call그래픽스 API에 오브젝트(예: 삼각형)를 그리도록 요청하는 호출이며, Batch는 여러 Draw Call을 함께 실행하는 그룹이다. 프로젝트가 복잡해짐에 따라 GPU의 작업 부하를 최적화하는 파이프라인이 필요하다.. 유니버설 렌더 파이프라인(URP)은 렌더링을 위한 세 가지 옵션을 지원한다. Forward, Forward+, 그리고 Deferred.

Forward Rendering은 single pass에서 모든 조명을 평가하며, 일반적으로 모바일 게임의 기본값으로 권장된다. Unity 2022 LTS에 도입된 Forward+는 오브젝트별이 아닌 공간적으로 조명을 컬링함으로써 표준 Forward 렌더링을 개선한다. 이는 프레임을 렌더링할 때 사용할 수 있는 조명의 전체 수를 크게 증가시킨다. Deferred 모드는 많은 동적 조명 소스를 사용하는 게임과 같은 특정 경우에 적합한 선택. 콘솔과 PC에서 사용되는 동일한 물리 기반 조명과 Material이 휴대폰이나 태블릿에서도 확장될 수 있다.

다음 표는 URP에서 제공하는 세 가지 렌더링 옵션을 비교한 것.

기능ForwardForward+Deferred
오브젝트당 실시간 조명 최대 수9무제한;
카메라당 제한이 적용됨
무제한
픽셀별 법선 인코딩인코딩 없음 (정확한 법선 값)인코딩 없음
(정확한 법선 값)
두 가지 옵션:
— G-버퍼에서 법선의 양자화 (정확도 저하, 성능 향상)
— 팔면체 인코딩
(정확한 법선, 모바일 GPU에서 성능에 큰 영향이 있을 수 있음)
자세한 내용은 G-버퍼에서 법선 인코딩을 참조하세요.
MSAA
(멀티샘플 안티앨리어싱)
지원지원지원하지 않음
버텍스 조명지원지원하지 않음지원하지 않음
카메라 스태킹지원지원제한적으로 지원:
Unity는 기본 카메라만 Deferred 경로로 렌더링하며, 모든 오버레이 카메라는 Forward 렌더링 경로로 렌더링함

Built-in Render Pipeline을 기반으로 한 프로젝트를 URP로 전환하는 방법에 대해 알아보려면, e-book Introduction to the Universal Render Pipeline for advanced Unity creators를 참조

GPU optimization

그래픽 렌더링 최적화를 위해선, 대상 하드웨어 (Target H/W)의 한계를 이해하고 GPU 프로파일링 방법을 알아야 한다. 프로파일링은 최적화가 효과적인지 확인, 검증하는데 도움이 된다. 다음의 몇가지 사례들을 파악해보자.

Benchmark the GPU

프로파일링을 할 때는 벤치마크로 시작하는것이 유용하다. 이는 특정 GPU에서 기대할 수 있는 프로파일링 결과를 알려준다. GFXBench에서 다양한 업계 표준 GPU 및 그래픽 카드 벤치마크 목록을 확인할 수 있다.

Watch the rendering statistics

게임 뷰의 오른쪽 상단에 있는 Stats 버튼을 클릭해보자. 이 창은 Play 모드 동안 애플리케이션의 실시간 렌더링 정보를 보여준다. 이 데이터를 사용하여 성능을 최적화할 수 있다.

  • Fps : 초당 프레임 수
  • CPU Main : 한 프레임을 처리하는 총 시간(모든 창에 대해 에디터를 업데이트하는 시간 포함)
  • CPU Render : Game View의 한 프레임을 렌더링하는 총 시간
  • Batches : 함께 그릴 Draw Call 그룹
  • Tris (삼각형) 및 Verts (정점) : 메쉬 지오메트리
  • SetPass calls : 화면에 게임 오브젝트를 렌더링하기 위해 Unity가 셰이더 패스를 전환해야 하는 횟수; 각 패스는 추가적인 CPU 오버헤드를 도입할 수 있다.

참고: 에디터 내 fps는 빌드 성능과 다르다. 가장 정확한 결과를 얻으려면 빌드를 프로파일링할 것을 권장한다. 벤치마킹할 때는 초당 프레임 수보다 밀리초 단위의 프레임 시간이 더 정확한 지표.

Draw Call이란 게임 엔진(예: Unity)이 그래픽 API(OpenGL, Direct3D, Vulkan 등)에 그래픽 렌더링 명령을 전달하는 과정.

Command Buffer란 그래픽 명령을 저장하고 실행하는 데 사용되는 메모리 버퍼

그래픽 명령들의 비용은 명령마다 각기 다르다. 특히 Shader와 관련있는 명령의 비용은 대체로 높은 편이다.

Batch란 드로우 콜(Draw Call) + 갖가지 렌더 상태 변경(Render State Change) 등

Use draw call batching

게임 오브젝트를 그리기 위해 Unity는 그래픽 API(예: OpenGL, Vulkan, 또는 Direct3D)에 Draw Call을 발행한다. 각 Draw Call은 CPU 리소스를 많이 사용한다. Draw Call 간의 상태 변화, 예를 들어 Material 전환은 CPU 측에서 성능 오버헤드를 초래할 수 있다. PC와 콘솔 하드웨어는 많은 Draw Call을 처리할 수 있지만, 각 콜의 오버헤드는 여전히 충분히 높아 이를 줄이려는 노력이 필요하다. 모바일 장치에서는 Draw Call 최적화가 매우 중요하다. 이를 위해 Draw Call Batching을 사용할 수 있다. Draw Call Batching은 이러한 상태 변화를 최소화하고 오브젝트 렌더링의 CPU 비용을 줄여준다. Unity는 여러 가지 기술을 사용하여 여러 오브젝트를 더 적은 batch로 결합할 수 있다.

  • SRP Batching : HDRP 또는 URP를 사용하는 경우, 파이프라인 에셋의 Advanced 설정에서 SRP Batcher를 활성화하자. 호환되는 셰이더를 사용할 때, SRP Batcher는 Draw Call 간의 GPU 설정을 줄이고 Material 데이터를 GPU 메모리에 캐시한다한다. 이를 통해 CPU 렌더링 시간을 크게 단축할 수 있다. SRP 배칭을 개선하려면 최소한의 키워드를 사용하여 Shader Variants를 줄이자. 프로젝트가 이 렌더링 워크플로우를 어떻게 활용할 수 있는지에 대한 자세한 내용은 SRP 문서 참조. image

  • GPU Instancing: 동일한 오브젝트(예: 동일한 메쉬와 Material을 가진 건물, 나무, 잔디 등)가 많은 경우, GPU 인스턴싱을 사용한다. 이 기술은 그래픽 하드웨어를 사용하여 이들을 배치한다. GPU 인스턴싱을 활성화하려면 프로젝트 창에서 재질을 선택한 후, 인스펙터에서 Enable Instancing을 체크한다.

  • 정적 배칭(Static Batching): 움직이지 않는 지오메트리에 대해, 유니티는 동일한 Material을 공유하는 모든 메쉬에 대해 Draw Call을 줄일 수 있다. 이는 동적 배칭보다 효율적일 수 있지만, 메모리 사용량이 늘어날 수 있다. 모든 이동하지 않는 메쉬를 인스펙터에서 Batching Static으로 표시하자. Unity는 빌드 시 모든 정적 메쉬를 하나의 큰 메쉬로 결합한다. StaticBatchingUtility를 사용하면 런타임에 직접 이러한 정적 배치를 생성할 수도 있다. (예: 움직이지 않는 부분으로 구성된 절차적 레벨을 생성한 후).

  • 동적 배칭(Dynamic Batching): 작은 메쉬의 경우, Unity는 CPU에서 버텍스를 그룹화하고 변환한 다음 한 번에 모두 그린다. 참고: 300개 이하의 버텍스와 900개의 총 버텍스 속성을 가진 low-poly 메쉬가 충분히 많지 않다면 이 기능을 사용하지 말자. 그렇지 않으면 작은 메쉬를 배치하려고 CPU 시간을 낭비하게 된다.

몇 가지 간단한 규칙으로 배칭을 최대화할 수 있다:

  • 씬에서 가능한 한 적은 텍스처를 사용하자. 적은 텍스처는 적은 수의 고유한 material을 필요로 하며, 이는 배치하기가 더 쉬워진다. 또한, 가능한 곳에서는 texture atlas를 사용해 여러 개의 작은 텍스처를 하나의 큰 텍스처로 결합하여 Draw Call을 줄일 수 있도록 하자. image

  • 항상 가능한 큰 아틀라스 크기로 라이트맵을 굽자. 적은 라이트맵은 적은 재질 상태 변경을 필요로 하지만, 메모리 사용량을 관리하자. (라이트맵의 크기는 Target Device에 따라 달라질 것)
  • material을 실수로 인스턴스화하지 않도록 주의하자. 스크립트에서 Renderer.material에 접근하면 material이 복제되고 새 복사본에 대한 참조를 반환한다. 이는 이미 해당 material을 포함하는 기존 batch를 깨뜨리게 된다. 배치된 오브젝트의 재질에 접근하려면 Renderer.sharedMaterial을 사용하자.
  • 최적화 중에는 프로파일러나 렌더링 통계를 사용하여 정적 및 동적 배치 수와 전체 Draw Call 수를 주의 깊게 살펴보자.

자세한 내용은 Draw Call Batching 문서 참조.

Use the Frame Debugger

Frame Debugger는 각 프레임이 개별 Draw Call로 어떻게 구성되는지 보여준다. 이는 셰이더 속성을 문제 해결하는 데 매우 유용한 도구이며 게임이 어떻게 렌더링되는지 분석하는 데 도움이 된다.

Draw Call 고려사항들

  • 배칭 효율
  • 머티리얼 Material
  • 스태틱 배칭 Static Batching
  • 다이나믹 배칭 Dynamic Batching
  • GPU 인스턴싱 GPU Instancing
  • SRP 배처 SRP Batcher
  • 라이트맵 Lightmap
  • Renderer.SharedMaterial vs Renderer.material
  • Frame Debugger

URP 에셋 관련 사항들

  • 해상도를 조절하는 방법의 차이
    • Screen.SetResolution
    • Render Scale Screen.SetResolution은 전체 화면의 해상도를 조절하고, Render Scale은 3D 씬의 렌더링 해상도를 조절한다. 이때 UI는 기본적으로 영향을 받지 않는다.
  • URP 파이프라인 에셋
    • 퀄리티
    • 라이팅 Lighting > Additional Lights 설정은 성능을 고려한다면 Per Vertex로, 시각적인 퀄리티를 우선한다면 Per Pixel로.
    • 쉐도우
    • 포스트프로세싱 이렇게 URP 파이프라인 에셋에는 퀄리티와 성능을 Trade Off 할 수 있는 설정들이 많다.

GPU 바운드 고려사항들

  • 쉐이더 비용 고려
  • 오버드로우
  • 포스트프로세싱
  • MeshRenderer vs SkinnedMeshRenderer
  • 텍스쳐 압축
    • ASTC
    • PVRTC
    • ETC

라이팅 고려사항들

  • 동적 광원 수 줄이기
  • 광원 레이어 활용
  • Realtime GI 불필요시 끄기
  • Baked 사용:
    • 라이트맵 (큰 배경 오브젝트)
    • 라이트프로브 (작은 소품 오브젝트)
    • 리플렉션 프로브 (메모리 주의)
  • 실시간 그림자:
    • MeshRenderer 및 광원별

Rendering Path

Forward Rendering

  • 전통적인 렌더링 기법
  • 동적 라이트 처리에는 취약
    • Draw Call
    • Overdraw
  • URP에서는 개선
    • 오브젝트 당 최대 8개
  • MSAA 지원

Forward+ Rendering

  • Forward에서 개선된 형태
  • 오브젝트 당 라이트 제약 없음
  • 타일 연산 오버헤드
  • MSAA 지원
  • VR 미지원

Deferred

  • 자유로운 동적 라이트
  • Vertex Lighting 지원 X
  • 모바일에서의 대역폭 이슈
  • URP에서는 개선
    • 그러나 여전히 모바일에서 권장되는 방식은 아님
  • MSAA 불가

카메라 최적화 고려사항들

  • 멀티 카메라 지양
    • 카메라 별 컬링 후 레이어 필터링을 거친다.
    • 한 Frustum 안에 오브젝트가 많아질수록 컬링 비용이 높아진다.
  • 카메라 Stacking이 필요한 경우에는, 카메라 영역이 서로 겹치지 않게 하자.
  • 멀티 카메라는 시네머신 권장.
  • Culling
    • 프러스텀 컬링은 기본적으로 유니티가 한다.
    • 오클루전 컬링 (실내 환경에서 효율적일 수 있다. 효율성을 따지고 사용여부를 결정)
    • LOD
  • Clipping Far와 Fog distance
  • 탑다운 뷰 게임에서는 Skybox 불필요

Baked Lighting

  • Realtime Lighting & Shadow 연산은 비싸다. e.g.) Shadowmap
  • Lightmap
    • static object에 적용
    • 저장형태 : Texture 2D
      • 메모리 이슈 발생 가능성 있음
    • Subtractive, Baked Indirect, Shadowmask
  • Light Probe
    • 주로 dynamic object 대응 용도 (static도 가능)
    • Diffuse term만 가능 (specular term 불가)
    • 저장 형태 : 개당 float x 30 + α (저렴) -> Adaptive Probe Volume
  • Reflection Probe
    • Specular term을 위한 데이터를 저장
    • 저장형태 : 개당 Cubemap Texture
    • 메모리 이슈 발생 가능성이 크다.

Adaptive Probe Volume 기존의 Light Probe Groups은 개별 Probe들을 수동으로 배치해야 했고, 이로 인해 많은 작업량과 작업 시간을 요한다. 더불어 어디에 얼마나 Probe를 배치하는것에 대해 작업자에게 끝없이 고민하게 만든다. Adaptive Probe Volume은 이를 대체할 수 있는 볼륨 기반 시스템이다.

  • 볼륨 기반 시스템
  • 자동으로 Probe가 배치되어 빠른 Interaction이 가능해진다.
  • 지오메트리 정보에 기반해 자동으로 배치한다.

또. Light Probe Groups을 사용하는 방법은 per object lighting으로 인해 큰 물체가 어두운 영역과 밝은 영역 사이를 이동할 때의 라이트 표현이 부정확하고 튈 때가 있으나, Adaptive Probe Volume은 per pixel lighting 방식을 활용해 개별 픽셀별로 빛을 계산한다.

  • 개별 픽셀별로 빛을 계산한다.
  • 하나의 큰 오브젝트 안에서도 자연스러운 연출이 가능해진다. Per Pixel 과 Per Vertex 중에서 선택할 수 있다. Per Pixel이 비교적 더 높은 퀄리티를 보여줄 수 있지만, 성능 이슈가 발생할 수 있다. 모바일 환경에서는 Per Vertex의 사용을 권장한다.

Avoid too many dynamic lights

Forward 렌더링을 사용할 때 모바일 애플리케이션에 너무 많은 동적 조명을 추가하지 않는 것이 중요하다. 동적 메쉬에는 커스텀 셰이더 효과와 라이트 프로브를 고려하고, 정적 메쉬에는 베이크된 조명을 사용한다. URP 및 Built-in Render Pipeline의 실시간 조명에 대한 특정 한계는 이 Feature Comparison Table를 참조.

모바일 애플리케이션에서 Forward Rendering을 사용할 때 너무 많은 동적 조명을 추가하지 않도록 주의하는 것이 중요하다. 동적 메쉬에는 커스텀 셰이더 효과나 Light Probe와 같은 대안을 고려하고, 정적 메쉬에는 Baked Lighting을 사용하는 것이 좋다. URP와 Built-in Render Pipeline의 실시간 조명에 대한 특정 제한 사항은 이 기능 비교 표를 참조하자.

Disable Shadows

MeshRendererLight마다 그림자 생성을 비활성화할 수 있다. 가능한 한 그림자를 비활성화하여 Draw Call을 줄이자. 캐릭터 아래에 간단한 메쉬나 쿼드에 블러 처리된 텍스처를 적용해 가짜 그림자를 만들 수도 있다. 또는, 커스텀 셰이더를 사용해 Blob Shadow를 생성하는 방법도 있다.

image

Bake your lighting into Lightmaps

정적 지오메트리에 Global Illumination(GI) 을 사용해 극적인 조명을 추가하자. 오브젝트를Contribute GI로 설정하여, 고품질 조명을 라이트맵(Lightmap) 형태로 저장할 수 있다.

image

베이크된 그림자와 조명은 런타임 시 성능 저하 없이 렌더링될 수 있다. Progressive CPUGPU Lightmapper를 사용하면 글로벌 일루미네이션의 베이킹 속도를 가속화할 수 있다.

Pasted image 20240903195034 메모리 사용량을 제한하기 위해 Lightmapping 설정(Windows > Rendering > Lighting Settings)과 Lightmap 크기를 조정하자.

Unity에서 Lightmapping 을 시작하려면, 매뉴얼 가이드와 이 조명 최적화에 관한 기사를 참조하자.

Baked Lightmap은 Texture

  • 메모리 이슈가 발생할 수 있다.
  • Draw Call Batching에도 영향을 줄 수 있다.
  • Directionality 사용 유무
  • Shadowmask 사용 유무
  • 해상도
  • 맵의 크기
  • 등등 여러 고려사항이 발생한다.

User Light Layers

복잡한 씬에서 여러 조명을 사용할 때, 오브젝트를 레이어로 구분한 다음, 각 조명의 영향을 특정 Culling Mask에 한정하자.

image 레이어를 사용하여 조명의 영향을 특정 Culling Mask로 제한할 수 있다.

Use Light Probes for moving objects

Light Probes는 씬의 빈 공간에 대한 베이크된 조명 정보를 저장하면서, 고품질의 조명(직접 조명과 간접 조명 모두)을 제공한다. Light Probes는 Spherical Harmonics 를 사용하며, 이는 동적 조명에 비해 빠르게 계산된다. 이 방식은 일반적으로 베이크된 라이트맵핑을 받을 수 없는 움직이는 오브젝트에 특히 유용하다.

image Light Probe Group에 Light Probes를 레벨 전체에 걸쳐 배치한다.

Light Probes는 정적 메쉬에도 적용할 수 있다. MeshRenderer 컴포넌트에서 Receive Global Illumination 드롭다운을 찾아, Lightmaps에서 Light Probes로 전환하자.

레벨의 주요 지오메트리에는 라이트맵핑을 계속 사용하고, 더 작은 디테일에는 프로브를 사용하자. Light Probe 조명은 적절한 UV가 필요하지 않으므로, 메쉬 언래핑(Unwrapping) 단계를 생략할 수 있다. 또한, 프로브는 라이트맵 텍스처를 생성하지 않기 때문에 디스크 공간을 절약할 수 있다.

씬 오브젝트에 Light Probes를 선택적으로 조명하는 방법에 대한 자세한 내용은 “Static Lighting with Light Probes” 블로그 포스트를 참조하자. Unity에서 조명 워크플로우에 대해 더 알고 싶다면 Making believable visuals in Unity를 읽어보자.

Use Level of Detail (LOD)

오브젝트가 멀어지면 Level of Detail (LOD) 를 통해 더 간단한 메쉬와 Material, 셰이더를 사용하도록 전환하여 GPU 성능을 향상시킬 수 있다.

Pasted image 20240903200024 LOD Group을 사용하는 Mesh의 예시

image 다양한 해상도로 모델링된 소스 메쉬들

Use Occlusion Culling to remove hidden objects

다른 오브젝트 뒤에 숨겨진 오브젝트는 여전히 렌더링되어 리소스를 소모할 수 있다. Occlusion Culling을 사용하여 이를 제거하자.

카메라 뷰 밖의 오브젝트에 대해 Frustum Culling은 자동으로 이루어지지만, Occlusion Culling은 베이크된 과정이 필요하다. 오브젝트를 Static Occluders 또는 Occludees로 설정한 다음, Window > Rendering > Occlusion Culling을 통해 베이킹하자. 모든 씬에 필수적인 것은 아니지만, 특정 경우에 Culling을 사용하면 성능을 향상시킬 수 있으므로, Occlusion Culling을 활성화하기 전후로 프로파일링하여 성능이 향상되었는지 확인하는 것이 좋다.

Working with Occlusion Culling 튜토리얼을 참고하여 자세한 정보를 확인하자.

Avoid mobile native resolution

휴대폰과 태블릿은 점점 더 발전하여, 최신 기기들은 매우 높은 해상도를 자랑한다. Screen.SetResolution(width, height, false) 를 사용하여 출력 해상도를 낮추고 성능을 회복하자. 여러 해상도를 프로파일링하여 품질과 속도 간의 최적의 균형을 찾아보자.

Limit use of cameras

각 활성화된 카메라는 유의미한 작업을 수행하든 아니든 일정한 오버헤드를 발생시킨다. 렌더링에 필요한 카메라 컴포넌트만 사용하도록 하자. 저사양 모바일 플랫폼에서는 각 카메라가 최대 1ms의 CPU 시간을 소모할 수 있다.

Keep shaders simple

Universal Render Pipeline에는 모바일 플랫폼에 최적화된 경량의 Lit 및 Unlit 셰이더가 여러 개 포함되어 있다. 셰이더 변형을 가능한 한 최소화하려고 노력하자. 셰이더 변형은 런타임 메모리 사용량에 큰 영향을 미칠 수 있다. 기본 URP 셰이더가 요구에 맞지 않는다면, Shader Graph를 사용하여 Material의 외관을 커스터마이즈할 수 있다. 여기에서 Shader Graph를 사용하여 셰이더를 시각적으로 구축하는 방법을 알아보자. Lit 보다는 Simple Lit이 성능 상에 더 이점을 가져다 줄 수 있고, Simple Lit보다도 적절하게 간소화한 Custom Lit

image Shader Graph를 사용하여 커스텀 셰이더를 만들자

Minimize overdraw and alpha blending

불필요한 투명 또는 반투명 이미지를 그리지 말고, 거의 보이지 않는 이미지나 효과를 겹치지 않도록 하자. 모바일 플랫폼은 이로 인해 발생하는 오버드로우(overdraw)와 알파 블렌딩에 큰 영향을 받는다. RenderDoc 그래픽 디버거를 사용하여 오버드로우를 확인할 수 있다. 또한, Rendering Debugger를 활용하여 다양한 조명, 렌더링, 및 Material 속성을 시각화할 수 있다. 이러한 시각화를 통해 렌더링 문제를 식별하고 씬과 렌더링 구성을 최적화할 수 있다.

Limit post-processing effects

글로우와 같은 전체 화면 포스트 프로세싱 효과는 성능을 크게 저하시킬 수 있다. 타이틀의 아트 방향에서 신중하게 사용하자.

Be careful with Renderer material

스크립트에서 Renderer.material 에 접근하면 해당 Material 이 복제되고 새 복사본에 대한 참조가 반환된다. 이로 인해 이미 해당 재질을 포함하는 기존 batch가 깨질 수 있다. 배치된 오브젝트의 재질에 접근하려면 Renderer.sharedMaterial 을 사용하자.

Optimize SkinnedMeshRenderers

스키닝된 메쉬를 렌더링하는 것은 비용이 많이 든다. Batching의 대상에서도 제외된다. SkinnedMeshRenderer를 사용하는 모든 오브젝트가 실제로 이를 필요로 하는지 확인하자. 만약 게임 오브젝트가 일부 시간 동안만 애니메이션이 필요하다면, BakeMesh 기능을 사용하여 스키닝된 메쉬를 정적인 자세로 고정한 후, 런타임에 더 간단한 MeshRenderer로 교체하자.

Minimize Reflection Probes

Reflection Probe는 현실적인 반사를 생성할 수 있지만, 배치(batch) 측면에서 매우 비용이 많이 들 수 있다. 런타임 성능을 개선하기 위해 저해상도 큐브맵, 컬링 마스크, 그리고 텍스처 압축을 사용하자. 메모리 이슈가 발생할 가능성이 크다.

System Metrics Mali

System Metrics Mali 패키지를 활용하여 ARM GPU를 사용하는 장치에서 저수준의 시스템 또는 하드웨어 성능 메트릭에 접근할 수 있다. 이를 통해 Unity Profiler에서 저수준 GPU 메트릭을 모니터링하고, Recorder API를 사용해 런타임에서 저수준 GPU 메트릭에 접근하며, 지속적인 통합 테스트 실행으로 성능 테스트를 자동화할 수 있다.

image Mali System Metrics Profiler Module


User Interface

Unity는 두 가지 UI 시스템을 제공한다: 이전의 Unity UI와 새로운 UI Toolkit 이다. UI Toolkit은 권장되는 UI 시스템이 될 예정이며, 최대 성능과 재사용성을 위해 설계되었다. 이 워크플로우와 저작 도구는 표준 웹 기술에서 영감을 받아 제작되었으므로, 이미 웹 페이지 디자인 경험이 있는 UI 디자이너와 아티스트에게 친숙할 것이다.

그러나 Unity 2022 LTS 기준으로 UI ToolkitUnity UIImmediate Mode GUI (IMGUI) 가 지원하는 일부 기능을 포함하지 않는다. Unity UIIMGUI는 특정 사용 사례에 더 적합하며, 레거시 프로젝트를 지원하기 위해 필요하다. 자세한 내용은 Unity의 UI 시스템 비교 문서를 참조하라.

UGUI performance optimization tips

Unity UI (UGUI) 는 종종 성능 문제의 원인이 된다. Canvas 컴포넌트는 UI 요소를 위한 메쉬를 생성하고 업데이트하며, GPU에 드로우 콜을 발행한다. 이 과정은 비용이 많이 들 수 있으므로, UGUI를 사용할 때는 다음 요소들을 염두에 두자.

Divide your Canvases

하나의 큰 Canvas에 수천 개의 요소가 있는 경우, 단일 UI 요소를 업데이트하면 전체 Canvas가 업데이트되어 잠재적으로 CPU 스파이크를 발생시킬 수 있다.

UGUI의 여러 Canvas를 지원하는 기능을 활용하자. UI 요소들을 업데이트 빈도에 따라 나누어 배치하자. 정적인 UI 요소는 별도의 Canvas에 유지하고, 동시에 업데이트되는 동적인 요소는 더 작은 서브 캔버스에 배치하자.

Nested Canvas를 활용해보자. Canvas의 산하에 Canvas를 배치할 수 있다. 이 경우에 Canvas의 업데이트는 각각 별도로 일어난다. 업데이트가 적은 Canvas를 상위에, 업데이트가 잦은 Canvas를 하위에 두도록 하자.

또한, Batching 효율을 위해 각 Canvas 내의 모든 UI 요소가 동일한 Z 값, 재질(Material), 텍스처를 사용하도록 하자.

Hide invisible UI elements

게임 내에서 간헐적으로만 나타나는 UI 요소(예: 캐릭터가 피해를 입을 때만 나타나는 체력 바)가 있을 수 있다. 보이지 않는 UI 요소가 활성 상태일 경우, 여전히 드로우 콜을 사용하고 있을 수 있다. 보이지 않는 UI 컴포넌트는 명시적으로 비활성화하고, 필요할 때 다시 활성화하자.

Canvas의 가시성만 꺼야 하는 경우, 전체 GameObject를 비활성화하기보다는 Canvas 컴포넌트를 비활성화하는 것이 좋다. 이렇게 하면 다시 활성화할 때 메쉬와 버텍스를 재구성해야 하는 상황을 방지할 수 있다.

Canvas를 끄기 vs GameObject를 끄기

  • UI가 빈번하게 보였다 사라지는 경우
    • Canvas만 비활성화하는 것이 성능적으로 더 유리하다. Rebuild 오버헤드를 줄일 수 있으며, 빠르게 UI를 다시 표시할 수 있다.
  • 오랫동안 UI가 보이지 않는 경우 또는 복잡한 UI
    • GameObject를 비활성화하는 것이 더 나을 수 있다. 모든 처리를 중지하고 리소스를 절약할 수 있기 때문이다.

Rect Transform에 대해

  • RectTransform도 Transform을 상속받기 때문에 오브젝트의 변경이 계층적으로 영향을 미친다.
  • Re-Parenting 연산도 성능 이슈가 발생할 수 있는 여지가 있다.

Pixel Perfect UI에서의 Pixel Perfect는 성능에 영향이 크다. image

  • Rect Transform 변경 시 모든 정점을 재계산
  • 움직이는 요소이 있는 경우에는 치명적인 성능 하락을 야기할 수 있다.

UI Object Pool UI에서 Object Pooling을 적용하면 자주 생성/삭제되는 UI 요소를 미리 생성하고 재사용하여 Canvas Rebuild로 인한 성능 저하를 줄일 수 있다. 이는 UI 요소를 새로 만들지 않고 활성화/비활성화하여 관리하며, 메모리와 성능을 효율적으로 최적화하는 데 도움이 된다. 특히 동적 리스트스크롤 뷰와 같은 UI에서 큰 성능 이점을 제공한다.

UI에서 Animator 사용 시 성능 최적화 고려사항 UI에 Animator를 사용할 때는 애니메이션 중 Canvas Rebuild가 자주 발생해 성능 저하를 일으킬 수 있으며, 여러 UI 요소에 적용된 Animator는 드로우 콜 증가와 배치 문제를 유발할 수 있다. 이를 방지하려면 자주 변경되는 UI 요소를 별도의 Canvas로 분리하고, 애니메이션이 끝난 후 Animator를 비활성화하여 불필요한 연산을 줄이는 방법을 고려해야 한다. 덧붙여 다음의 사항들도 고려할 수 있다. :

  • 항상 변경되는 동적인 UI에만 Animator 사용
  • Shader Graph로 대체할 수 있는 경우인지 고려할 것
  • 이벤트성 반응 버튼과 같은 UI라면 트위닝 시스템의 사용을 고려할 것

Limit GraphicRaycasters and disable Raycast Target

화면 터치나 클릭과 같은 입력 이벤트는 GraphicRaycaster 컴포넌트가 필요하다. 이 컴포넌트는 단순히 화면의 각 입력 지점을 순회하며, 해당 지점이 UI의 RectTransform 내에 있는지 확인한다.

계층 구조의 최상위 Canvas에서 기본 GraphicRaycaster를 제거하자. 대신, GraphicRaycaster를 상호작용이 필요한 개별 요소들(Button, ScrollRect 등)에만 추가하자.

image 기본적으로 활성화된 Ignore Reversed Graphics를 비활성화하자

또한, 필요하지 않은 모든 UI 텍스트와 이미지에서 Raycast Target을 비활성화하자. UI가 많은 요소로 구성된 복잡한 구조라면, 이러한 작은 변화들이 불필요한 연산을 줄이는 데 도움이 될 수 있다.

image 가능하면 Raycast Target을 비활성화하자.

Avoid Layout Groups

Layout Groups비효율적으로 업데이트되므로, 필요한 경우에만 사용하자. 콘텐츠가 동적이지 않다면 Layout Groups를 사용하지 말고, 비율에 맞춘 레이아웃을 위해 Anchors를 사용하는 것이 좋다. 또는, Layout Group 컴포넌트가 UI를 설정한 후 비활성화하는 커스텀 코드를 작성할 수 있다.

동적인 요소Layout Groups(Horizontal, Vertical, Grid)을 사용해야 하는 경우, 성능을 향상시키기 위해 중첩 사용을 피하자.

image Layout Groups는 특히 중첩될 경우 성능을 저하시킬 수 있다.

Avoid numerous overlaid elements

많은 UI 요소를 레이어링하는 것(예: 카드 배틀 게임에서 카드가 쌓이는 경우)은 오버드로우를 발생시킨다. 코드로 레이어된 요소들을 런타임에 합쳐서 더 적은 요소와 배치로 만들도록 커스터마이즈하자.

Use multiple resolutions and aspect ratios

모바일 기기들이 매우 다양한 해상도와 화면 크기를 사용함에 따라, 기기별로 최상의 경험을 제공하기 위해 UI의 대체 버전을 만들어야 한다.

Device Simulator를 사용하여 다양한 지원 기기에서 UI를 미리 볼 수 있다. 또한, XCodeAndroid Studio에서 가상 기기를 생성할 수도 있다.

image Device Simulator를 사용하여 다양한 화면 형식을 미리 확인하자.

When using a fullscreen UI, hide everything else

일시 정지 화면이나 시작 화면이 씬의 모든 것을 가리는 경우, 3D 씬을 렌더링하는 카메라를 비활성화하자. 마찬가지로, 상단 Canvas 뒤에 숨겨진 배경 Canvas 요소들도 비활성화하자.

전체 화면 UI에서는 Application.targetFrameRate 를 낮추는 것도 고려하자. 60fps로 업데이트할 필요는 없기 때문이다.

Assign the Camera to World Space and Camera Space Canvases

Event 또는 Render Camera 필드를 비워두면 Unity가 자동으로 Camera.main을 할당하게 되는데, 이는 불필요하게 비용이 많이 든다.

가능하다면 Canvas RenderModeScreen Space – Overlay로 설정하는 것을 고려하자. 이 모드는 카메라를 필요로 하지 않는다.

image World Space Render Mode를 사용할 때는 Event Camera를 반드시 설정하자.

UI Toolkit performance optimization tips

UI ToolkitUnity UI보다 향상된 성능을 제공하며, 최대 성능과 재사용성을 위해 설계되었고, 표준 웹 기술에서 영감을 받은 워크플로우와 저작 도구를 제공한다. UI Toolkit의 주요 이점 중 하나는 UI 요소를 위해 특별히 설계된 고도로 최적화된 렌더링 파이프라인을 사용한다는 점이다.

다음은 UI Toolkit을 사용하여 UI 성능을 최적화하기 위한 일반적인 권장 사항이다:

  • 효율적인 레이아웃 사용: 효율적인 레이아웃이란, UI 요소를 수동으로 배치하고 크기를 조정하는 대신, UI Toolkit에서 제공하는 Flexbox와 같은 레이아웃 그룹을 사용하는 것을 의미한다. 레이아웃 그룹은 레이아웃 계산을 자동으로 처리하여 성능을 크게 향상시킬 수 있다. 이들은 UI 요소가 지정된 레이아웃 규칙에 따라 올바르게 배열되고 크기가 조정되도록 보장한다. 효율적인 레이아웃을 활용함으로써 수동 레이아웃 계산의 오버헤드를 피하고, 일관되고 최적화된 UI 렌더링을 달성할 수 있다.

  • Update에서 비용이 많이 드는 작업 피하기: Update 메서드에서 수행되는 작업을 최소화하자. 특히 UI 요소 생성, 조작, 또는 계산과 같은 무거운 작업을 최소화하는 것이 중요하다. 이러한 작업은 가능한 한 초기화 단계에서 수행하며, Update 메서드는 매 프레임 호출되기 때문에 신중하게 관리해야 한다.

  • 이벤트 처리 최적화: 이벤트 구독에 신경을 쓰고, 더 이상 필요하지 않은 경우 구독을 해제하자. 과도한 이벤트 처리는 성능에 영향을 미칠 수 있으므로, 필요한 이벤트에만 구독하도록 하자.

  • 스타일 시트 최적화: 스타일 시트에서 사용되는 스타일 클래스와 선택자의 수에 유의하자. 많은 규칙이 포함된 대형 스타일 시트는 성능에 영향을 줄 수 있다. 스타일 시트를 간결하게 유지하고, 불필요한 복잡성을 피하자.

  • 프로파일링 및 최적화: Unity의 프로파일링 도구를 사용하여 UI의 성능 병목 지점을 식별하고, 비효율적인 레이아웃 계산이나 과도한 리드로우와 같은 최적화가 필요한 영역을 찾자. Profiler 안에 기본적으로 UI, UI Details는 꺼져있는데, 이를 켜서 Batch Breaking Reason, Batch 당 렌더 요소를 볼 수 있다.

  • 타겟 플랫폼에서 테스트: UI 성능을 최적의 상태로 유지하기 위해 타겟 플랫폼에서 테스트하자. 성능은 하드웨어 성능에 따라 달라질 수 있으므로, UI를 최적화할 때 타겟 플랫폼을 고려하자.

성능 최적화는 반복적인 과정임을 기억하자. UI 코드가 원활하고 효율적으로 실행되도록 지속적으로 프로파일링하고, 측정하며, 최적화하자.


Audio

오디오는 일반적으로 성능의 병목 지점이 되지 않지만, 메모리를 절약하기 위해 여전히 최적화할 수 있다.

가능한 경우 사운드 클립을 모노로 만들자

  • 3D 공간 오디오를 사용하는 경우, 사운드 클립을 모노 (단일 채널) 로 제작하거나 ‘Force To Mono’ 설정을 활성화하라. 런타임에서 위치적으로 사용되는 다채널 사운드는 모노 소스로 평탄화되어 CPU 비용을 증가시키고 메모리를 낭비하게 된다.
  • 모바일 게임에서는 ‘Force To Mono’ 설정을 비활성화하는 것을 추천.

image

가능한 경우 원본 압축되지 않은 WAV 파일을 사용하라.

압축 포맷(예: MP3 또는 Vorbis 같은)을 사용하는 경우, Unity는 빌드 시간 동안 그것을 해제 압축하고 다시 압축한다. 이로 인해 손실이 두 번 발생하여 최종 품질이 저하된다.

클립을 압축하고 압축 비트레이트를 줄이자.

압축을 사용하여 클립의 크기와 메모리 사용량을 줄이자.

  • 대부분의 소리에는 Vorbis를 사용하라 (또는 반복되지 않는 소리에는 MP3를 사용).
  • 짧고 자주 사용되는 소리 (예: 발자국, 총소리) 에는 ADPCM을 사용하라. 이는 압축되지 않은 PCM에 비해 파일 크기를 줄이지만, 재생 중 디코딩이 빠르다.

모바일 장치에서의 사운드 효과는 최대 22,050 Hz여야 한다. 낮은 설정을 사용해도 최종 품질에 미치는 영향은 일반적으로 최소한이다; 직접 귀로 듣고 판단할 수 있다.

적절한 Load Type을 선택하라.

설정은 클립 크기에 따라 달라진다.

  • 작은 클립(< 200kb)Decompress on Load 를 사용해야 한다. 이는 사운드를 raw 16비트 PCM 오디오 데이터로 해제 압축함으로써 CPU 비용과 메모리를 사용하지만, 짧은 소리에만 바람직하다.
  • 중간 크기 클립(>= 200kb)Compressed in Memory 상태로 두어야 한다.
  • 큰 파일(배경 음악)Streaming 으로 설정되어야 한다. 그렇지 않으면, 전체 Asset 이 한 번에 메모리로 로드된다.

음소거된 AudioSource를 메모리에서 언로드하자.

음소거 버튼을 구현할 때, 볼륨을 0으로 설정하는 것만으로는 충분하지 않다. 플레이어가 이를 자주 켜고 끄지 않아야 한다면, AudioSource 컴포넌트를 Destroy 하여 메모리에서 언로드할 수 있다.


Animation

Unity의 Mecanim 시스템은 상당히 정교하다. 가능하면, 다음 설정을 사용하여 모바일에서의 사용을 제한하라.

Generic Rig 대 Humanoid Rig 사용하기

기본적으로, Unity는 애니메이션 모델을 제네릭 리그로 가져오지만, 개발자들은 종종 캐릭터를 애니메이션할 때 인간형 리그로 전환한다.

인간형 리그는 역운동학과 애니메이션 리타게팅을 매 프레임 계산하기 때문에 동등한 제네릭 리그보다 CPU 시간을 30-50% 더 소비한다, 심지어 사용하지 않을 때에도. 인간형 리그의 특정 기능이 필요하지 않다면, 대신 제네릭 리그를 사용하라.

애니메이터의 과도한 사용을 피하라

애니메이터는 주로 인간형 캐릭터에 사용되지만 종종 단일 값 (예: UI 요소의 알파 채널) 을 애니메이션하는 데 사용된다. 특히 UI 요소와 결합하여 애니메이터를 과도하게 사용하는 것을 피하라. 가능하면 모바일용으로는 레거시 애니메이션 컴포넌트를 사용하라.

간단한 애니메이션(예: DOTween)을 위해 트윈 함수를 만들거나 타사 라이브러리를 사용하는 것을 고려하라.

애니메이터는 잠재적으로 비용이 많이 든다.


Physics

Unity의 내장 물리 엔진(Nvidia PhysX for 3D, Box2D for 2D)는 모바일에서 비용이 많이 들 수 있다. 다음 팁은 초당 프레임 수를 더 많이 확보하는 데 도움이 될 수 있다.

설정 최적화하기

PlayerSettings에서 가능한 한 Prebake Collision Meshes 를 체크하라.

image

물리 설정 (Project Settings > Physics) 을 수정하는 것도 잊지 마라. 가능한 한 Layer Collision Matrix 를 단순화하라.

Auto Sync Transforms 를 비활성화하고 Reuse Collision Callbacks 를 활성화하라.

Simplify colliders

메쉬 콜라이더는 비용이 많이 들 수 있다. 더 복잡한 메쉬 콜라이더를 원래 모양을 대략적으로 나타낼 수 있는 더 단순한 기본형 또는 메쉬 콜라이더로 대체하라.

image

Rigidbody를 물리 메소드를 사용하여 이동하라

MovePosition이나 AddForce와 같은 클래스 메소드를 사용하여 Rigidbody 객체를 이동하라. Transform 컴포넌트를 직접 변환하는 것은 물리 세계의 재계산을 초래할 수 있으며, 이는 복잡한 장면에서 비용이 많이 들 수 있다. Update가 아닌 FixedUpdate에서 물리 바디를 이동하라.

Update 에서 Rigidbody 관련 연산을 하거나, FixedUpdate에서 Transform에 변환을 가하면 불필요한 중복 연산이나, 오버헤드가 발생할 수 있다.

Fixed Timestep을 조정하라.

프로젝트 설정의 기본 Fixed Timestep은 0.02 (50Hz) 다. 이를 목표 프레임율에 맞게 변경하라 (예를 들어, 30fps의 경우 0.03).

그렇지 않으면, 런타임에 프레임율이 떨어지면 Unity가 한 프레임 내에서 FixedUpdate 를 여러 번 호출하게 되어, 물리 연산이 많은 콘텐츠에서 CPU 성능 문제를 일으킬 수 있다. 이는 반복적으로 악순환을 일으킬 수 있다. (프레임이 떨어져서, 단위 프레임당 물리연산이 늘고, 이 때문에 또 다시 프레임이 떨어지고…)

Maximum Allowed Timestep 은 프레임율이 떨어질 때 물리 계산과 FixedUpdate 이벤트가 사용할 수 있는 시간을 제한한다. 이 값을 낮추는 것은 물리 연산의 정확도를 낮출 수 있고, 물리와 애니메이션의 속도를 늦출 수 있지만, 성능 문제가 발생했을 때 프레임율에 미치는 영향도 줄인다.

image 목표 프레임율에 맞게 Fixed Timestep을 수정하고, 성능 이슈를 줄이기 위해 Maximum Allowed Timestep을 낮추자.

Physics Debugger 로 시각화하라.

Physics Debug 창(Window > Analysis > Physics Debugger) 을 사용하여 문제가 되는 콜라이더나 불일치를 해결하는 데 도움을 줄 수 있다. 이는 어떤 GameObject들이 서로 충돌할 수 있는지를 색상 코드로 표시하는 지표를 보여준다.

image


WorkFlow and Collaboration

Unity에서 애플리케이션을 구축하는 것은 많은 개발자가 관련될 수 있는 큰 작업이다. 프로젝트가 팀에 최적화되어 설정되어 있는지 확인하라.

Version Control 사용하기

모든 이들이 특정한 형태의 버전 제어를 사용해야 한다. Editor Settings 에서 Asset Serialization ModeForce Text 로 설정되어 있는지 확인하라.

외부 버전 제어 시스템(예: Git)을 사용하는 경우, Version Control 설정에서 Mode 가 Visible Meta Files 로 설정되어 있는지 확인하라.

Unity에는 장면과 프리팹을 병합하기 위해 특별히 사용되는 내장 YAML(사람이 읽을 수 있는 데이터 직렬화 언어) 도구가 있다. 자세한 정보는 Unity 문서에서 Smart Merge를 참조하라.

버전 제어는 팀의 일부로서 작업하는 데 필수적이다. 버그와 나쁜 리비전을 추적하는 데 도움을 줄 수 있다. 마일스톤과 릴리스를 관리하기 위해 브랜치와 태그를 사용하는 것과 같은 좋은 관행을 따르라.

Unity 게임 개발을 위한 추천 버전 제어 솔루션인 Plastic SCM을 확인해 추가 버전 관리 지원을 받아보라.

큰 장면을 분할하라.

큰 단일 Unity 장면은 협업에 잘 맞지 않는다. 레벨을 여러 개의 작은 장면으로 나누어서 아티스트와 디자이너가 단일 레벨에서 효과적으로 협업할 수 있게 하여, 충돌의 위험을 최소화하라.

런타임에 프로젝트는 SceneManager.LoadSceneAsync 를 호출하여 LoadSceneMode.Additive 파라미터 모드를 전달함으로써 장면을 추가적으로 로드할 수 있음을 유의하라.

사용하지 않는 리소스 제거하기

타사 플러그인 및 라이브러리와 함께 제공되는 사용되지 않는 Asset 을 주의하라. 많은 플러그인과 라이브러리에는 내장된 테스트 Asset 과 스크립트가 포함되어 있으며, 이를 제거하지 않으면 빌드의 일부가 된다. 프로토타이핑에서 남은 필요하지 않는 리소스를 모두 제거하라.

참고한 자료

Unity_E-Book_Optimize_your_game_performance_for_mobile

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.