글 작성자: Doublsb

이번 포스팅은 유니티 공부를 할 때 항상 신세를 지고 있는 알쓸유잡 콘텐츠를 공부하고 기록해보는 글이 되겠다.

특히나 이번 영상은 최적화였기 때문에, 프로파일러 안의 숫자를 하나라도 더 줄이면 쾌감을 느끼는 편인 나는 어쩔 수 없이 글을 쓸 수밖에 없었다. 음... 변태같은 소리는 뒤로 하고 본론으로 들어가자.


소개

https://www.youtube.com/live/-IrpQzZi_p8?si=tz7lyopfIHhbdPyX

 

https://blog.unity.com/engine-platform/updated-2022-lts-best-practice-guides

 

Updated 2022 LTS Best Practice Guides for optimizing games | Unity Blog

Each guide includes actionable tips across profiling tools, programming and code architecture, working with assets, render pipelines, UI, and much more. There are also many new and updated links to additional documentation and other resources if you want t

blog.unity.com

이번에 새로 나온 E-Book을 한국어로 번역해서 소개하고 먹여주는 영상이다.

 

2022 LTS에서 적용되는 내용을 설명하고 있으므로, 현재 프로젝트에서 2022 LTS를 사용하고 있는 나에게 딱 맞는 영상이다. 현재 홈페이지에서 2021 LTS는 레거시라고 공언하고 있고, 2024년 중반에 지원이 종료된다고 하니 기존 프로젝트들은 업그레이드를 해봐야겠다.

 

영상에 나오지 않고 문서에서만 나오는 내용도 있는데, 이것도 같이 정리해 보겠다.


프로파일링

  • 정확한 문제를 파악하고 해결해라
  • 초기에, 자주, 타겟 디바이스에서 프로파일링하라
  • iOS에서는 Xcode랑 Instruments이 최고니까 써라
  • 링크에서 유니티에서 제공하는 프로파일링 도구를 확인해봐라
  • 프로파일링 워크플로우는 이 E-Book을 확인해봐라
  • 프로파일러 마커를 사용해서 부분적 프로파일링을 해 봐라
using Unity.Profiling;

public class MySystemClass
{
    static readonly ProfilerMarker s_PreparePerfMarker = new ProfilerMarker("MySystem.Prepare");
    static readonly ProfilerMarker s_SimulatePerfMarker = new ProfilerMarker(ProfilerCategory.Ai, "MySystem.Simulate");

    public void UpdateLogic()
    {
        s_PreparePerfMarker.Begin();
        // ...
        s_PreparePerfMarker.End();

        using (s_SimulatePerfMarker.Auto())
        {
            // ...
        }
    }
}

 

원하는 코드 앞뒤에 ProfilerMarker를 사용하면 함수 내에서 부분적인 프로파일링을 진행할 수 있다.

 

  • 프로파일러에서 타겟 디바이스에 연결해 프로파일링을 할 수 있다
  • 딥 프로파일링 옵션을 쓰면 모든 함수 콜의 시작과 끝을 확인할 수 있다
  • 프로파일러에서 Hierarchy 옵션을 쓰면 함수 호출 순서대로 볼 수 있고, 정렬할 수도 있다
  • 프로파일러 애널라이저를 쓰면 두 개의 시나리오를 비교할 수 있다
  • 프로파일러 애널라이저에서는 특정 프레임을 확인하는 것뿐만 아니라 영역을 잡아 확인할 수 있다

  • FPS를 ms로 변환하는 방법을 알고 타임 버짓을 세팅하자
    • 30 FPS => 1000 ms / 30 FPS => 33.333ms
    • 60 FPS => 1000 ms / 60 FPS => 16.666ms
    • 타임 버짓은 일반적으로 ms로 설정하기 때문에, FPS를 ms로 환산하는 것을 기억해두면 좋다
  • 모바일에서는 디바이스 발열 방지를 위해 65%의 버짓을 세팅하는 것을 추천한다

  • GPU-bound인지 CPU-bound인지 알아보려면, 다음 함수들을 눈여겨보자
    • Gfx.WaitForCommands가 자주 보인다면, CPU 병목이다
    • Gfx.WaitForPresent가 자주 보인다면, GPU 병목이다

메모리

  • 문서를 보고 유니티의 메모리 레이어를 이해해보자
  • 메모리 프로파일러 패키지를 잘 사용해보자
  • 가비지 컬렉터 스파이크를 일으킬 수 있는 것들을 조심하자
    • String
      • string 기반 데이터인 Json이나 XML은 지양하자
      • 스크립터블 오브젝트나 MessagePack, Protobuf 사용을 지향하자
    • 유니티 function 호출
      • GameObject.tag는 새 string을 반환하므로 가비지가 된다
      • GameObject.CompareTag를 사용하자
    • 박싱/언박싱
      • 값 타입에서 참조 타입으로 변환할 때 가비지가 생기니까 언제든 조심하자
      • 제네릭을 사용해서 회피하자
    • 코루틴
      • new WaitForSeconds를 캐싱해서 재사용하자
      • 가급적 UniTask 사용을 지향하는것도 괜찮은듯
      • 영상에는 없지만, 이전 프로젝트에서는 아래의 코드를 사용했다
                public static class YieldInstructionCache
                {
                    public static readonly WaitForEndOfFrame WaitForEndOfFrame = new WaitForEndOfFrame();
                    public static readonly WaitForFixedUpdate WaitForFixedUpdate = new WaitForFixedUpdate();
                    private static readonly Dictionary<float, WaitForSeconds> waitForSeconds = new Dictionary<float, WaitForSeconds>();
        
                    public static WaitForSeconds WaitForSeconds(float seconds)
                    {
                        WaitForSeconds wfs;
                        if (!waitForSeconds.TryGetValue(seconds, out wfs))
                            waitForSeconds.Add(seconds, wfs = new WaitForSeconds(seconds));
                        return wfs;
                    }
                }​
    • LINQ & 정규 표현식
      • 박싱/언박싱이 많이 일어나므로 자주 호출되는 함수에서는 지양하자
  • Use incremental GC(점진적 가비지 콜렉션) 옵션을 쓰면 닷넷 세대 방식 쓰는 것마냥 GC 스파이크를 줄일 수 있다

어댑티브 퍼포먼스

삼성과 유니티의 합작인 어댑티브 퍼포먼스를 사용하면 아래와 같은 기능을 사용할 수 있다.

그 말은... 안드로이드만... 되잖아...!

  • 이전 프레임 기준의 원하는 프레임 속도 제어 가능
  • 디바이스 온도 레벨 체크 가능
  • 써멀 이벤트 체크 가능
  • CPU 병목인지 GPU 병목인지 체크 가능

프로그래밍과 코드 구조

  • 프로파일러의 PlayerLoop에 표시되는 내용이 사용자 코드 부분이다
  • 아래의 메서드를 사용해 해시를 캐싱해서 string 할당을 피하자
    • Animator.StringToHash
    • Shader.PropertyToID
  • 런타임 중 AddComponent가 비싼 이유는 중복/필요한 컴포넌트를 체크하는 과정 때문이다
  • MonoBehaviour에 값을 저장하기보다는 스크립터블 오브젝트를 사용해라
    • MonoBehaviour는 GameObject가 필요하므로 추가적인 오버헤드가 필요하다
    • 스크립터블 오브젝트는 GameObject나 Transform이 없으므로 더 경제적이다
  • 나머지 내용은 기초적이라 적지 않았다

프로젝트 설정

  • Accelerometer Frequency(가속 센서 프로세싱)가 필요없다면 비활성화해라
  • 지원하지 않을 Auto Graphics API는 제거해라
  • 지원하지 않을 Target Architectures는 제거해라
  • Quality 세팅에서, 필요 없는 Quality levels는 제거해라
  • 물리를 사용하지 않는다면 Auto Simulation과 Auto Sync Transforms 옵션은 꺼라

  • 문서를 보고 하이어라이키 구조를 최적화해라
    • Transform이 변환될 때마다 모든 부모와 자식은 그 정보를 받아야 한다
    • 루트에 오브젝트를 생성하면 부모에 정보를 알릴 필요가 없어진다
  • 인스턴스화 할 때 위치와 회전 설정은 한꺼번에 해라
    • GameObject.Instantiate(prefab, parent) 하고 위치/회전을 또 설정하지 말자
    • GameObject.Instantiate(prefab, parent, position, rotation) 으로 한꺼번에 하자
  • VSync(수직 동기화) 꺼라

에셋

텍스쳐

  • Max Size를 설정해라
  • POT 크기로 만들어라
  • 아틀라싱해라
  • Read/Write 옵션은 필요 없으면 꺼라
  • 런타임에 텍스쳐를 생성하는 경우 makeNoLongerReadable을 true로 설정하면 Read/Write 옵션이 꺼진다
  • 밉맵이 필요없으면 꺼라
  • 음청난 옛날 기기 지원할거 아니면 ASTC 포맷 써라
    • iPhone 5, 5S 등등 => PVRTC
    • Android 2016 이전 디바이스 => ETC2
  • 타겟 플랫폼이 ASTC 포맷을 지원하지 않으면 32-bit 대신 16-bit 텍스쳐 써라

 

메쉬

  • 이상하게 보이지 않을 정도로만 압축 레벨 높여서 써라
  • Read/Write 옵션 필요 없으면 꺼라
  • rigs and BlendShapes 옵션 필요 없으면 꺼라
  • 머티리얼에서 normals, tangents 사용 안하면 옵션 꺼라
  • 폴리곤 개수 체크해서 너무 많다면 3D 모델을 다듬어봐라

 

기타

  • Unity DataTools 써서 데이터를 분석하고 최적화해보자 (뭐지 처음 본다)
  • 어드레서블 시스템 써서 비동기로 에셋을 로드하자

그래픽 및 GPU 최적화

  • URP의 세가지 렌더링 옵션을 확인하고 고려하라
    • Forward
      • 오브젝트별로 라이팅을 계산
      • 모바일에 적합함
    • Forward+
      • Forward에 타일 개념을 적용한 기법
      • 타일 단위로 라이팅을 계산
      • 오브젝트 당 실시간 라이팅 개수 무제한
      • 버텍스 라이팅 지원 안됨
    • Deferred
      • 라이팅을 지연시킨 렌더링 방식
      • 파이프라인 맨 끝단에서 한번에 라이팅을 처리
      • 오브젝트 당 실시간 라이팅 개수 무제한
      • GPU가 Multi Render Targets(MRT) 기능을 지원해야 함
      • 씬에 등장하는 모든 오브젝트들의 Geometry 정보를 MRT에 넣어야 하므로 메모리 겁나 잡아먹음
      • 모바일에는 적합하지 않음
  • Stats 탭을 확인해 렌더링 정보를 확인하라
  • SRP Batcher를 사용하라
  • 동일한 메쉬, 머티리얼을 사용하는 오브젝트가 많다면 GPU instancing을 사용하라
  • 움직이지 않는 메쉬를 한꺼번에 모아 그리는 Static batching을 고려하라
  • 작은 메쉬들을 한꺼번에 모아 그리기 위해 Dynamic batching을 사용하라
  • 스크립트에서 Renderer.material을 사용하면 머티리얼을 복사해서 반환하기 때문에 배칭이 깨질 수 있다
    배칭된 머티리얼에 접근하려면 Renderer.SharedMaterial을 사용해라
  • 프레임 디버거를 사용해서 드로우 콜을 확인해라
  • 너무 많은 동적 라이팅은 피해야 한다
  • 그림자가 필요 없으면 렌더러에서 Cast Shadows는 꺼라
  • 움직이지 않는 개체들은 라이트맵을 구워라
  • 다수의 라이트를 사용한다면 레이어를 나눠서 컬링 마스크를 설정해라
  • 동적 라이팅을 사용하기보다는 라이트 프로브를 설정해서 구워라
  • 카메라와의 거리에 따른 GPU 최적화를 위해 LOD를 사용하라
  • 안 보이는 오브젝트를 제거하기 위해 오클루전 컬링을 사용하라
  • Screen.SetResolution을 사용해서 해상도를 최적화해라
  • 카메라 개수를 줄여라
  • 쉐이더는 심플하게 만들어라
  • 렌더러 디버거를 사용해서 오버드로우와 알파 블렌딩을 최소화해라
  • 너무 많은 포스트 프로세싱 이펙트를 사용하지 마라
  • SkinnedMeshRenderer는 비싸니까, BakeMesh 메서드를 사용하든 다른 MeshRenderer로 바꾸든 해라
  • System Metrics Mali 패키지를 사용하면 낮은 수준의 GPU로 프로파일링 해볼 수 있다 (!!!)

UI

  • UI ToolKit 하쉴? 이라고 되어 있다... 일단 문서는 UGUI 기준으로 써 있다.
  • 갱신 코스트를 줄이기 위해 캔버스를 나눠라
  • 화면에 보이지 않는 UI도 드로우콜을 먹는다
  • 캔버스를 꺼야 할 때에는 게임오브젝트를 끄지 말고, 캔버스 컴포넌트를 꺼라
  • Graphic Raycaster에서 트리거되는 마스크를 설정해라
  • 레이캐스트 하지 않는다면 Raycast Target은 꺼라
  • 레이아웃 그룹은 지양해라
  • 재사용 가능한 스크롤뷰나 그리드뷰를 써라
  • World Space와 Camera Space의 Camera를 비워 두면 유니티에서는 Camera.main을 할당하니 주의하자 (?!)

오디오

  • 개쩌는 양방향 오디오를 만들 게 아니라면 스테레오 말고 모노를 쓰자
  • wav 확장자 쓰는 게 좋다
  • 대부분의 사운드에는 Vorbis를 사용해라
  • 자주 사용되는 사운드에는 ADPCM을 사용해라
  • 작은 용량 (< 200kb)에는 Decompress on Load를 사용해라
  • 중간 용량 (>= 200kb)에는 Compressed in Memory를 사용해라
  • 큰 용량 (BGM)에는 Streaming을 사용해라
  • 오디오소스의 Mute는 볼륨을 0으로 세팅하는 것 뿐이므로, 안 쓸거면 그냥 Destroy해라

애니메이션

  • humanoid 리깅은 연산을 더 먹으니 generic 리깅을 쓰는 게 좋다
  • UI에는 애니메이션을 쓰지 말고 DOTween을 쓰는 게 좋다

물리

  • 가능하면 Prebake Collision Meshes를 체크해라
  • 충돌 매트릭스를 단순화해라
  • Auto Sync Transforms는 해제하라
  • Reuse Collision Callbacks는 체크해라
  • 메쉬 콜라이더는 비싸니까 더 심플한 콜라이더를 써라
  • Rigidbody 오브젝트에서는 MovePosition이나 AddForce로 물체를 이동해라
    Transform에 접근해서 포지션을 이동시키면 물리 연산을 다시 해야 한다
  • 물리 움직임은 Update보다는 FixedUpdate에서 해라
  • Fixed Timestep를 타겟 프레임에 맞춰 설정해라 
    • 위에서 언급했던 fps=>ms 변환 공식에 따르면 된다
    • 유니티의 기본값은 0.02ms인데, 이는 곧 50fps이다
    • 30fps => 0.03, 60fps => 0.016
  • 피직스 디버거를 써서 충돌 가능 여부를 가시적으로 파악하라

워크플로우와 협업

  • 버전 컨트롤을 위해 Asset Serialization을 Force Text로 설정하라
  • Git을 사용한다면 Version Control을 Visible Meta Files로 설정하라
  • 거대한 씬을 작은 여러 개의 씬으로 나눠서 충돌을 피해라
  • 플러그인이나 외부 에셋을 사용할 때 필요 없는 Resources를 제거해라

여담

  • Camera.main은 2022 LTS에서는 캐싱하므로 값싸다 (충격)
  • URP에서 스킨드 매쉬 최적화는 SRP-Batcher를 사용해라
반응형